forked from github-starred/komodo
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6919582f69 | ||
|
|
34a9f8eb9e | ||
|
|
494d01aeed | ||
|
|
084e2fec23 | ||
|
|
98d72fc908 | ||
|
|
20ac04fae5 | ||
|
|
a65fd4dca7 | ||
|
|
0873104b5a | ||
|
|
9a7b6ebd51 | ||
|
|
a4153fa28b |
Generated
+431
-354
File diff suppressed because it is too large
Load Diff
+19
-16
@@ -8,13 +8,16 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.19.1"
|
||||
version = "1.19.5"
|
||||
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" }
|
||||
@@ -33,7 +36,7 @@ git = { path = "lib/git" }
|
||||
|
||||
# MOGH
|
||||
run_command = { version = "0.0.6", features = ["async_tokio"] }
|
||||
serror = { version = "0.5.0", default-features = false }
|
||||
serror = { version = "0.5.1", 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"
|
||||
@@ -43,7 +46,7 @@ derive_variants = "1.0.0"
|
||||
mongo_indexed = "2.0.2"
|
||||
resolver_api = "3.0.0"
|
||||
toml_pretty = "1.2.0"
|
||||
mungos = "3.2.1"
|
||||
mungos = "3.2.2"
|
||||
svi = "1.2.0"
|
||||
|
||||
# ASYNC
|
||||
@@ -65,11 +68,12 @@ axum = { version = "0.8.4", features = ["ws", "json", "macros"] }
|
||||
|
||||
# SER/DE
|
||||
ipnetwork = { version = "0.21.1", features = ["serde"] }
|
||||
indexmap = { version = "2.10.0", features = ["serde"] }
|
||||
indexmap = { version = "2.11.1", 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.143"
|
||||
serde_json = "1.0.145"
|
||||
serde_qs = "0.15.0"
|
||||
toml = "0.9.5"
|
||||
|
||||
@@ -80,19 +84,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.19", features = ["json"] }
|
||||
tracing-subscriber = { version = "0.3.20", 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.45", features = ["derive"] }
|
||||
clap = { version = "4.5.47", features = ["derive"] }
|
||||
dotenvy = "0.15.7"
|
||||
envy = "0.4.2"
|
||||
|
||||
# CRYPTO / AUTH
|
||||
uuid = { version = "1.18.0", features = ["v4", "fast-rng", "serde"] }
|
||||
uuid = { version = "1.18.1", features = ["v4", "fast-rng", "serde"] }
|
||||
jsonwebtoken = { version = "9.3.1", default-features = false }
|
||||
openidconnect = "4.0.1"
|
||||
urlencoding = "2.1.3"
|
||||
@@ -111,26 +115,25 @@ bollard = "0.19.2"
|
||||
sysinfo = "0.37.0"
|
||||
|
||||
# CLOUD
|
||||
aws-config = "1.8.5"
|
||||
aws-sdk-ec2 = "1.160.0"
|
||||
aws-credential-types = "1.2.5"
|
||||
aws-config = "1.8.6"
|
||||
aws-sdk-ec2 = "1.167.0"
|
||||
aws-credential-types = "1.2.6"
|
||||
|
||||
## CRON
|
||||
english-to-cron = "0.1.6"
|
||||
chrono-tz = "0.10.4"
|
||||
chrono = "0.4.41"
|
||||
chrono = "0.4.42"
|
||||
croner = "3.0.0"
|
||||
|
||||
# MISC
|
||||
async-compression = { version = "0.4.27", features = ["tokio", "gzip"] }
|
||||
async-compression = { version = "0.4.30", features = ["tokio", "gzip"] }
|
||||
derive_builder = "0.20.2"
|
||||
comfy-table = "7.1.4"
|
||||
comfy-table = "7.2.1"
|
||||
typeshare = "1.0.4"
|
||||
octorust = "0.10.0"
|
||||
dashmap = "6.1.0"
|
||||
wildcard = "0.3.0"
|
||||
colored = "3.0.0"
|
||||
regex = "1.11.1"
|
||||
regex = "1.11.2"
|
||||
bytes = "1.10.1"
|
||||
bson = "2.15.0"
|
||||
shell-escape = "0.1.5"
|
||||
@@ -2,6 +2,7 @@
|
||||
## for a specific architecture.
|
||||
|
||||
FROM rust:1.89.0-bullseye AS builder
|
||||
RUN cargo install cargo-strip
|
||||
|
||||
WORKDIR /builder
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
@@ -16,7 +17,8 @@ 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 build -p komodo_cli --release && \
|
||||
cargo strip
|
||||
|
||||
# Copy just the binaries to scratch image
|
||||
FROM scratch
|
||||
|
||||
@@ -12,6 +12,7 @@ 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
|
||||
@@ -20,7 +21,8 @@ COPY . .
|
||||
RUN \
|
||||
cargo build --release --bin core && \
|
||||
cargo build --release --bin periphery && \
|
||||
cargo build --release --bin km
|
||||
cargo build --release --bin km && \
|
||||
cargo strip
|
||||
|
||||
# Copy just the binaries to scratch image
|
||||
FROM scratch
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
FROM rust:1.89.0-bullseye AS builder
|
||||
RUN cargo install cargo-strip
|
||||
|
||||
WORKDIR /builder
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
@@ -8,7 +9,7 @@ COPY ./client/periphery ./client/periphery
|
||||
COPY ./bin/cli ./bin/cli
|
||||
|
||||
# Compile bin
|
||||
RUN cargo build -p komodo_cli --release
|
||||
RUN cargo build -p komodo_cli --release && cargo strip
|
||||
|
||||
# Copy binaries to distroless base
|
||||
FROM gcr.io/distroless/cc
|
||||
|
||||
@@ -549,20 +549,20 @@ async fn poll_update_until_complete(
|
||||
} else {
|
||||
format!("{}/updates/{}", cli_config().host, update.id)
|
||||
};
|
||||
info!("Link: '{}'", link.bold());
|
||||
println!("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 {
|
||||
info!(
|
||||
println!(
|
||||
"FINISHED in {}: {}",
|
||||
format!("{:.1?}", timer.elapsed()).bold(),
|
||||
"EXECUTION SUCCESSFUL".green(),
|
||||
);
|
||||
} else {
|
||||
warn!(
|
||||
eprintln!(
|
||||
"FINISHED in {}: {}",
|
||||
format!("{:.1?}", timer.elapsed()).bold(),
|
||||
"EXECUTION FAILED".red(),
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
# Build Core
|
||||
FROM rust:1.89.0-bullseye AS core-builder
|
||||
RUN cargo install cargo-strip
|
||||
|
||||
WORKDIR /builder
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
@@ -13,7 +14,8 @@ COPY ./bin/cli ./bin/cli
|
||||
|
||||
# Compile app
|
||||
RUN cargo build -p komodo_core --release && \
|
||||
cargo build -p komodo_cli --release
|
||||
cargo build -p komodo_cli --release && \
|
||||
cargo strip
|
||||
|
||||
# Build Frontend
|
||||
FROM node:20.12-alpine AS frontend-builder
|
||||
|
||||
@@ -29,12 +29,12 @@ pub async fn send_alert(
|
||||
match alert.level {
|
||||
SeverityLevel::Ok => {
|
||||
format!(
|
||||
"{level} | **{name}** ({region}) | Server version now matches core version ✅\n{link}"
|
||||
"{level} | **{name}**{region} | Periphery version now matches Core version ✅\n{link}"
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
format!(
|
||||
"{level} | **{name}** ({region}) | Version mismatch detected ⚠️\nServer: **{server_version}** | Core: **{core_version}**\n{link}"
|
||||
"{level} | **{name}**{region} | Version mismatch detected ⚠️\nPeriphery: **{server_version}** | Core: **{core_version}**\n{link}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,12 +275,12 @@ fn standard_alert_content(alert: &Alert) -> String {
|
||||
match alert.level {
|
||||
SeverityLevel::Ok => {
|
||||
format!(
|
||||
"{level} | {name} ({region}) | Server version now matches core version ✅\n{link}"
|
||||
"{level} | {name}{region} | Periphery version now matches Core version ✅\n{link}"
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
format!(
|
||||
"{level} | {name} ({region}) | Version mismatch detected ⚠️\nServer: {server_version} | Core: {core_version}\n{link}"
|
||||
"{level} | {name}{region} | Version mismatch detected ⚠️\nPeriphery: {server_version} | Core: {core_version}\n{link}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,12 +34,12 @@ pub async fn send_alert(
|
||||
let text = match alert.level {
|
||||
SeverityLevel::Ok => {
|
||||
format!(
|
||||
"{level} | {name} ({region}) | Server version now matches core version ✅"
|
||||
"{level} | *{name}*{region} | Periphery version now matches Core version ✅"
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
format!(
|
||||
"{level} | {name} ({region}) | Version mismatch detected ⚠️\nServer: {server_version} | Core: {core_version}"
|
||||
"{level} | *{name}*{region} | Version mismatch detected ⚠️\nPeriphery: {server_version} | Core: {core_version}"
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,11 +3,12 @@ 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::Json;
|
||||
use serror::{AddStatusCode, Json};
|
||||
use typeshare::typeshare;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -152,7 +153,11 @@ impl Resolve<AuthArgs> for GetUser {
|
||||
self,
|
||||
AuthArgs { headers }: &AuthArgs,
|
||||
) -> serror::Result<User> {
|
||||
let user_id = get_user_id_from_headers(headers).await?;
|
||||
Ok(get_user(&user_id).await?)
|
||||
let user_id = get_user_id_from_headers(headers)
|
||||
.await
|
||||
.status_code(StatusCode::UNAUTHORIZED)?;
|
||||
get_user(&user_id)
|
||||
.await
|
||||
.status_code(StatusCode::UNAUTHORIZED)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,8 +92,11 @@ 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(|state| state.running = true)?;
|
||||
let _action_guard = action_state.update_custom(
|
||||
|state| state.running += 1,
|
||||
|state| state.running -= 1,
|
||||
false,
|
||||
)?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ use komodo_client::{
|
||||
build::{Build, BuildConfig},
|
||||
builder::{Builder, BuilderConfig},
|
||||
deployment::DeploymentState,
|
||||
komodo_timestamp,
|
||||
komodo_timestamp, optional_string,
|
||||
permission::PermissionLevel,
|
||||
repo::Repo,
|
||||
update::{Log, Update},
|
||||
@@ -290,12 +290,10 @@ impl Resolve<ExecuteArgs> for RunBuild {
|
||||
repo,
|
||||
registry_tokens,
|
||||
replacers: secret_replacers.into_iter().collect(),
|
||||
// Push a commit hash tagged image
|
||||
additional_tags: if update.commit_hash.is_empty() {
|
||||
Default::default()
|
||||
} else {
|
||||
vec![update.commit_hash.clone()]
|
||||
},
|
||||
// To push a commit hash tagged image
|
||||
commit_hash: optional_string(&update.commit_hash),
|
||||
// Unused for now
|
||||
additional_tags: Default::default(),
|
||||
}) => res.context("failed at call to periphery to build"),
|
||||
_ = cancel.cancelled() => {
|
||||
info!("build cancelled during build, cleaning up builder");
|
||||
|
||||
@@ -12,7 +12,7 @@ use komodo_client::{
|
||||
deployment::{
|
||||
Deployment, DeploymentImage, extract_registry_domain,
|
||||
},
|
||||
get_image_names, komodo_timestamp, optional_string,
|
||||
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 = get_image_names(&build);
|
||||
let image_names = build.get_image_names();
|
||||
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).await;
|
||||
update_cache_for_server(&server, true).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 = get_image_names(&build);
|
||||
let image_names = build.get_image_names();
|
||||
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).await;
|
||||
update_cache_for_server(server, true).await;
|
||||
anyhow::Ok(log)
|
||||
}
|
||||
.await;
|
||||
@@ -428,7 +428,7 @@ impl Resolve<ExecuteArgs> for StartDeployment {
|
||||
};
|
||||
|
||||
update.logs.push(log);
|
||||
update_cache_for_server(&server).await;
|
||||
update_cache_for_server(&server, true).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).await;
|
||||
update_cache_for_server(&server, true).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).await;
|
||||
update_cache_for_server(&server, true).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).await;
|
||||
update_cache_for_server(&server, true).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).await;
|
||||
update_cache_for_server(&server, true).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).await;
|
||||
update_cache_for_server(&server, true).await;
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
|
||||
@@ -49,7 +49,7 @@ impl Resolve<ExecuteArgs> for ClearRepoCache {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("This method is admin only.")
|
||||
.status_code(StatusCode::UNAUTHORIZED),
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ impl Resolve<ExecuteArgs> for BackupCoreDatabase {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("This method is admin only.")
|
||||
.status_code(StatusCode::UNAUTHORIZED),
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ impl Resolve<ExecuteArgs> for GlobalAutoUpdate {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("This method is admin only.")
|
||||
.status_code(StatusCode::UNAUTHORIZED),
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ impl Resolve<ExecuteArgs> for StartContainer {
|
||||
};
|
||||
|
||||
update.logs.push(log);
|
||||
update_cache_for_server(&server).await;
|
||||
update_cache_for_server(&server, true).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).await;
|
||||
update_cache_for_server(&server, true).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).await;
|
||||
update_cache_for_server(&server, true).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).await;
|
||||
update_cache_for_server(&server, true).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).await;
|
||||
update_cache_for_server(&server, true).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).await;
|
||||
update_cache_for_server(&server, true).await;
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
@@ -401,7 +401,7 @@ impl Resolve<ExecuteArgs> for StartAllContainers {
|
||||
);
|
||||
}
|
||||
|
||||
update_cache_for_server(&server).await;
|
||||
update_cache_for_server(&server, true).await;
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
@@ -453,7 +453,7 @@ impl Resolve<ExecuteArgs> for RestartAllContainers {
|
||||
);
|
||||
}
|
||||
|
||||
update_cache_for_server(&server).await;
|
||||
update_cache_for_server(&server, true).await;
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
@@ -503,7 +503,7 @@ impl Resolve<ExecuteArgs> for PauseAllContainers {
|
||||
);
|
||||
}
|
||||
|
||||
update_cache_for_server(&server).await;
|
||||
update_cache_for_server(&server, true).await;
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
@@ -555,7 +555,7 @@ impl Resolve<ExecuteArgs> for UnpauseAllContainers {
|
||||
);
|
||||
}
|
||||
|
||||
update_cache_for_server(&server).await;
|
||||
update_cache_for_server(&server, true).await;
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
@@ -605,7 +605,7 @@ impl Resolve<ExecuteArgs> for StopAllContainers {
|
||||
);
|
||||
}
|
||||
|
||||
update_cache_for_server(&server).await;
|
||||
update_cache_for_server(&server, true).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).await;
|
||||
update_cache_for_server(&server, true).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).await;
|
||||
update_cache_for_server(&server, true).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).await;
|
||||
update_cache_for_server(&server, true).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).await;
|
||||
update_cache_for_server(&server, true).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).await;
|
||||
update_cache_for_server(&server, true).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).await;
|
||||
update_cache_for_server(&server, true).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).await;
|
||||
update_cache_for_server(&server, true).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).await;
|
||||
update_cache_for_server(&server, true).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).await;
|
||||
update_cache_for_server(&server, true).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).await;
|
||||
update_cache_for_server(&server, true).await;
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
@@ -260,7 +260,7 @@ impl Resolve<ExecuteArgs> for DeployStack {
|
||||
}
|
||||
|
||||
// Ensure cached stack state up to date by updating server cache
|
||||
update_cache_for_server(&server).await;
|
||||
update_cache_for_server(&server, true).await;
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
@@ -761,7 +761,7 @@ pub async fn pull_stack_inner(
|
||||
.await?;
|
||||
|
||||
// Ensure cached stack state up to date by updating server cache
|
||||
update_cache_for_server(server).await;
|
||||
update_cache_for_server(server, true).await;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
@@ -1033,6 +1033,7 @@ 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,
|
||||
|
||||
@@ -77,10 +77,8 @@ impl Resolve<ExecuteArgs> for RunSync {
|
||||
};
|
||||
|
||||
// get the action state for the sync (or insert default).
|
||||
let action_state = action_states()
|
||||
.resource_sync
|
||||
.get_or_insert_default(&sync.id)
|
||||
.await;
|
||||
let action_state =
|
||||
action_states().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.
|
||||
|
||||
@@ -131,8 +131,8 @@ impl Resolve<ReadArgs> for GetActionsSummary {
|
||||
.unwrap_or_default()
|
||||
.get()?,
|
||||
) {
|
||||
(_, action_states) if action_states.running => {
|
||||
res.running += 1;
|
||||
(_, action_states) if action_states.running > 0 => {
|
||||
res.running += action_states.running;
|
||||
}
|
||||
(ActionState::Ok, _) => res.ok += 1,
|
||||
(ActionState::Failed, _) => res.failed += 1,
|
||||
|
||||
@@ -93,7 +93,7 @@ impl Resolve<ReadArgs> for GetResourceSyncActionState {
|
||||
)
|
||||
.await?;
|
||||
let action_state = action_states()
|
||||
.resource_sync
|
||||
.sync
|
||||
.get(&sync.id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
@@ -138,7 +138,7 @@ impl Resolve<ReadArgs> for GetResourceSyncsSummary {
|
||||
continue;
|
||||
}
|
||||
if action_states
|
||||
.resource_sync
|
||||
.sync
|
||||
.get(&resource_sync.id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
|
||||
@@ -16,10 +16,7 @@ impl Resolve<WriteArgs> for CreateAction {
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Action> {
|
||||
Ok(
|
||||
resource::create::<Action>(&self.name, self.config, user)
|
||||
.await?,
|
||||
)
|
||||
resource::create::<Action>(&self.name, self.config, user).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +32,7 @@ impl Resolve<WriteArgs> for CopyAction {
|
||||
PermissionLevel::Write.into(),
|
||||
)
|
||||
.await?;
|
||||
Ok(
|
||||
resource::create::<Action>(&self.name, config.into(), user)
|
||||
.await?,
|
||||
)
|
||||
resource::create::<Action>(&self.name, config.into(), user).await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,7 @@ impl Resolve<WriteArgs> for CreateAlerter {
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Alerter> {
|
||||
Ok(
|
||||
resource::create::<Alerter>(&self.name, self.config, user)
|
||||
.await?,
|
||||
)
|
||||
resource::create::<Alerter>(&self.name, self.config, user).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +32,7 @@ impl Resolve<WriteArgs> for CopyAlerter {
|
||||
PermissionLevel::Write.into(),
|
||||
)
|
||||
.await?;
|
||||
Ok(
|
||||
resource::create::<Alerter>(&self.name, config.into(), user)
|
||||
.await?,
|
||||
)
|
||||
resource::create::<Alerter>(&self.name, config.into(), user).await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,10 +50,7 @@ impl Resolve<WriteArgs> for CreateBuild {
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Build> {
|
||||
Ok(
|
||||
resource::create::<Build>(&self.name, self.config, user)
|
||||
.await?,
|
||||
)
|
||||
resource::create::<Build>(&self.name, self.config, user).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,10 +68,7 @@ impl Resolve<WriteArgs> for CopyBuild {
|
||||
.await?;
|
||||
// reset version to 0.0.0
|
||||
config.version = Default::default();
|
||||
Ok(
|
||||
resource::create::<Build>(&self.name, config.into(), user)
|
||||
.await?,
|
||||
)
|
||||
resource::create::<Build>(&self.name, config.into(), user).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +180,7 @@ async fn write_dockerfile_contents_git(
|
||||
) -> serror::Result<Update> {
|
||||
let WriteBuildFileContents { build: _, contents } = req;
|
||||
|
||||
let mut clone_args: RepoExecutionArgs = if !build
|
||||
let mut repo_args: RepoExecutionArgs = if !build
|
||||
.config
|
||||
.files_on_host
|
||||
&& !build.config.linked_repo.is_empty()
|
||||
@@ -196,8 +190,8 @@ async fn write_dockerfile_contents_git(
|
||||
} else {
|
||||
(&build).into()
|
||||
};
|
||||
let root = clone_args.unique_path(&core_config().repo_directory)?;
|
||||
clone_args.destination = Some(root.display().to_string());
|
||||
let root = repo_args.unique_path(&core_config().repo_directory)?;
|
||||
repo_args.destination = Some(root.display().to_string());
|
||||
|
||||
let build_path = build
|
||||
.config
|
||||
@@ -220,11 +214,11 @@ async fn write_dockerfile_contents_git(
|
||||
})?;
|
||||
}
|
||||
|
||||
let access_token = if let Some(account) = &clone_args.account {
|
||||
git_token(&clone_args.provider, account, |https| clone_args.https = https)
|
||||
let access_token = if let Some(account) = &repo_args.account {
|
||||
git_token(&repo_args.provider, account, |https| repo_args.https = https)
|
||||
.await
|
||||
.with_context(
|
||||
|| format!("Failed to get git token in call to db. Stopping run. | {} | {account}", clone_args.provider),
|
||||
|| format!("Failed to get git token in call to db. Stopping run. | {} | {account}", repo_args.provider),
|
||||
)?
|
||||
} else {
|
||||
None
|
||||
@@ -235,7 +229,7 @@ async fn write_dockerfile_contents_git(
|
||||
if !root.join(".git").exists() {
|
||||
git::init_folder_as_repo(
|
||||
&root,
|
||||
&clone_args,
|
||||
&repo_args,
|
||||
access_token.as_deref(),
|
||||
&mut update.logs,
|
||||
)
|
||||
@@ -249,9 +243,11 @@ 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(
|
||||
clone_args,
|
||||
repo_args,
|
||||
&core_config().repo_directory,
|
||||
access_token,
|
||||
)
|
||||
@@ -298,7 +294,7 @@ async fn write_dockerfile_contents_git(
|
||||
&format!("{}: Commit Dockerfile", args.user.username),
|
||||
&root,
|
||||
&build_path.join(&dockerfile_path),
|
||||
&build.config.branch,
|
||||
&branch,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -16,10 +16,7 @@ impl Resolve<WriteArgs> for CreateBuilder {
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Builder> {
|
||||
Ok(
|
||||
resource::create::<Builder>(&self.name, self.config, user)
|
||||
.await?,
|
||||
)
|
||||
resource::create::<Builder>(&self.name, self.config, user).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +32,7 @@ impl Resolve<WriteArgs> for CopyBuilder {
|
||||
PermissionLevel::Write.into(),
|
||||
)
|
||||
.await?;
|
||||
Ok(
|
||||
resource::create::<Builder>(&self.name, config.into(), user)
|
||||
.await?,
|
||||
)
|
||||
resource::create::<Builder>(&self.name, config.into(), user).await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,10 +38,8 @@ impl Resolve<WriteArgs> for CreateDeployment {
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Deployment> {
|
||||
Ok(
|
||||
resource::create::<Deployment>(&self.name, self.config, user)
|
||||
.await?,
|
||||
)
|
||||
resource::create::<Deployment>(&self.name, self.config, user)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,10 +56,8 @@ impl Resolve<WriteArgs> for CopyDeployment {
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Ok(
|
||||
resource::create::<Deployment>(&self.name, config.into(), user)
|
||||
.await?,
|
||||
)
|
||||
resource::create::<Deployment>(&self.name, config.into(), user)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,10 +149,7 @@ impl Resolve<WriteArgs> for CreateDeploymentFromContainer {
|
||||
});
|
||||
}
|
||||
|
||||
Ok(
|
||||
resource::create::<Deployment>(&self.name, config, user)
|
||||
.await?,
|
||||
)
|
||||
resource::create::<Deployment>(&self.name, config, user).await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,7 @@ impl Resolve<WriteArgs> for CreateProcedure {
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<CreateProcedureResponse> {
|
||||
Ok(
|
||||
resource::create::<Procedure>(&self.name, self.config, user)
|
||||
.await?,
|
||||
)
|
||||
resource::create::<Procedure>(&self.name, self.config, user).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,10 +33,8 @@ impl Resolve<WriteArgs> for CopyProcedure {
|
||||
PermissionLevel::Write.into(),
|
||||
)
|
||||
.await?;
|
||||
Ok(
|
||||
resource::create::<Procedure>(&self.name, config.into(), user)
|
||||
.await?,
|
||||
)
|
||||
resource::create::<Procedure>(&self.name, config.into(), user)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ impl Resolve<WriteArgs> for CreateRepo {
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Repo> {
|
||||
Ok(resource::create::<Repo>(&self.name, self.config, user).await?)
|
||||
resource::create::<Repo>(&self.name, self.config, user).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,10 +58,7 @@ impl Resolve<WriteArgs> for CopyRepo {
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Ok(
|
||||
resource::create::<Repo>(&self.name, config.into(), user)
|
||||
.await?,
|
||||
)
|
||||
resource::create::<Repo>(&self.name, config.into(), user).await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,10 +30,7 @@ impl Resolve<WriteArgs> for CreateServer {
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Server> {
|
||||
Ok(
|
||||
resource::create::<Server>(&self.name, self.config, user)
|
||||
.await?,
|
||||
)
|
||||
resource::create::<Server>(&self.name, self.config, user).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,10 +46,8 @@ impl Resolve<WriteArgs> for CopyServer {
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Ok(
|
||||
resource::create::<Server>(&self.name, config.into(), user)
|
||||
.await?,
|
||||
)
|
||||
|
||||
resource::create::<Server>(&self.name, config.into(), user).await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,10 +51,7 @@ impl Resolve<WriteArgs> for CreateStack {
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Stack> {
|
||||
Ok(
|
||||
resource::create::<Stack>(&self.name, self.config, user)
|
||||
.await?,
|
||||
)
|
||||
resource::create::<Stack>(&self.name, self.config, user).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,10 +67,8 @@ impl Resolve<WriteArgs> for CopyStack {
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Ok(
|
||||
resource::create::<Stack>(&self.name, config.into(), user)
|
||||
.await?,
|
||||
)
|
||||
|
||||
resource::create::<Stack>(&self.name, config.into(), user).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,6 +279,8 @@ 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,
|
||||
@@ -334,7 +331,7 @@ async fn write_stack_file_contents_git(
|
||||
&format!("{username}: Write Stack File"),
|
||||
&root,
|
||||
&file_path,
|
||||
&stack.config.branch,
|
||||
&branch,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -68,10 +68,8 @@ impl Resolve<WriteArgs> for CreateResourceSync {
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<ResourceSync> {
|
||||
Ok(
|
||||
resource::create::<ResourceSync>(&self.name, self.config, user)
|
||||
.await?,
|
||||
)
|
||||
resource::create::<ResourceSync>(&self.name, self.config, user)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,14 +86,8 @@ impl Resolve<WriteArgs> for CopyResourceSync {
|
||||
PermissionLevel::Write.into(),
|
||||
)
|
||||
.await?;
|
||||
Ok(
|
||||
resource::create::<ResourceSync>(
|
||||
&self.name,
|
||||
config.into(),
|
||||
user,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
resource::create::<ResourceSync>(&self.name, config.into(), user)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,6 +315,8 @@ 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,
|
||||
@@ -373,7 +367,7 @@ async fn write_sync_file_contents_git(
|
||||
&format!("{}: Commit Resource File", args.user.username),
|
||||
&root,
|
||||
&resource_path.join(&file_path),
|
||||
&sync.config.branch,
|
||||
&branch,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -13,9 +13,12 @@ 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,
|
||||
@@ -29,8 +32,18 @@ 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").into());
|
||||
return Err(
|
||||
anyhow!("Tag name cannot be ObjectId")
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
}
|
||||
|
||||
let mut tag = Tag {
|
||||
|
||||
@@ -32,7 +32,7 @@ impl Resolve<WriteArgs> for CreateLocalUser {
|
||||
if !admin.admin {
|
||||
return Err(
|
||||
anyhow!("This method is admin-only.")
|
||||
.status_code(StatusCode::UNAUTHORIZED),
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ impl Resolve<WriteArgs> for DeleteUser {
|
||||
if !admin.admin {
|
||||
return Err(
|
||||
anyhow!("This method is admin-only.")
|
||||
.status_code(StatusCode::UNAUTHORIZED),
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
if admin.username == self.user || admin.id == self.user {
|
||||
@@ -220,6 +220,14 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@ 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;
|
||||
|
||||
@@ -23,7 +25,10 @@ impl Resolve<WriteArgs> for CreateUserGroup {
|
||||
WriteArgs { user: admin }: &WriteArgs,
|
||||
) -> serror::Result<UserGroup> {
|
||||
if !admin.admin {
|
||||
return Err(anyhow!("This call is admin-only").into());
|
||||
return Err(
|
||||
anyhow!("This call is admin-only")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
let user_group = UserGroup {
|
||||
name: self.name,
|
||||
@@ -58,7 +63,10 @@ impl Resolve<WriteArgs> for RenameUserGroup {
|
||||
WriteArgs { user: admin }: &WriteArgs,
|
||||
) -> serror::Result<UserGroup> {
|
||||
if !admin.admin {
|
||||
return Err(anyhow!("This call is admin-only").into());
|
||||
return Err(
|
||||
anyhow!("This call is admin-only")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
let db = db_client();
|
||||
update_one_by_id(
|
||||
@@ -84,7 +92,10 @@ impl Resolve<WriteArgs> for DeleteUserGroup {
|
||||
WriteArgs { user: admin }: &WriteArgs,
|
||||
) -> serror::Result<UserGroup> {
|
||||
if !admin.admin {
|
||||
return Err(anyhow!("This call is admin-only").into());
|
||||
return Err(
|
||||
anyhow!("This call is admin-only")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
let db = db_client();
|
||||
@@ -117,7 +128,10 @@ impl Resolve<WriteArgs> for AddUserToUserGroup {
|
||||
WriteArgs { user: admin }: &WriteArgs,
|
||||
) -> serror::Result<UserGroup> {
|
||||
if !admin.admin {
|
||||
return Err(anyhow!("This call is admin-only").into());
|
||||
return Err(
|
||||
anyhow!("This call is admin-only")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
let db = db_client();
|
||||
@@ -161,7 +175,10 @@ impl Resolve<WriteArgs> for RemoveUserFromUserGroup {
|
||||
WriteArgs { user: admin }: &WriteArgs,
|
||||
) -> serror::Result<UserGroup> {
|
||||
if !admin.admin {
|
||||
return Err(anyhow!("This call is admin-only").into());
|
||||
return Err(
|
||||
anyhow!("This call is admin-only")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
let db = db_client();
|
||||
@@ -205,7 +222,10 @@ impl Resolve<WriteArgs> for SetUsersInUserGroup {
|
||||
WriteArgs { user: admin }: &WriteArgs,
|
||||
) -> serror::Result<UserGroup> {
|
||||
if !admin.admin {
|
||||
return Err(anyhow!("This call is admin-only").into());
|
||||
return Err(
|
||||
anyhow!("This call is admin-only")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
let db = db_client();
|
||||
@@ -252,7 +272,10 @@ impl Resolve<WriteArgs> for SetEveryoneUserGroup {
|
||||
WriteArgs { user: admin }: &WriteArgs,
|
||||
) -> serror::Result<UserGroup> {
|
||||
if !admin.admin {
|
||||
return Err(anyhow!("This call is admin-only").into());
|
||||
return Err(
|
||||
anyhow!("This call is admin-only")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
let db = db_client();
|
||||
|
||||
@@ -4,7 +4,9 @@ use komodo_client::{
|
||||
api::write::*,
|
||||
entities::{Operation, ResourceTarget, variable::Variable},
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCodeError;
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
@@ -22,6 +24,13 @@ 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,
|
||||
@@ -29,10 +38,6 @@ 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,
|
||||
@@ -44,7 +49,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(),
|
||||
@@ -69,7 +74,10 @@ impl Resolve<WriteArgs> for UpdateVariableValue {
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<UpdateVariableValueResponse> {
|
||||
if !user.admin {
|
||||
return Err(anyhow!("only admins can update variables").into());
|
||||
return Err(
|
||||
anyhow!("Only admins can update variables")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
let UpdateVariableValue { name, value } = self;
|
||||
@@ -87,7 +95,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(),
|
||||
@@ -107,7 +115,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?;
|
||||
@@ -123,7 +131,10 @@ impl Resolve<WriteArgs> for UpdateVariableDescription {
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<UpdateVariableDescriptionResponse> {
|
||||
if !user.admin {
|
||||
return Err(anyhow!("only admins can update variables").into());
|
||||
return Err(
|
||||
anyhow!("Only admins can update variables")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
db_client()
|
||||
.variables
|
||||
@@ -132,7 +143,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?)
|
||||
}
|
||||
}
|
||||
@@ -144,7 +155,10 @@ impl Resolve<WriteArgs> for UpdateVariableIsSecret {
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<UpdateVariableIsSecretResponse> {
|
||||
if !user.admin {
|
||||
return Err(anyhow!("only admins can update variables").into());
|
||||
return Err(
|
||||
anyhow!("Only admins can update variables")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
db_client()
|
||||
.variables
|
||||
@@ -153,7 +167,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?)
|
||||
}
|
||||
}
|
||||
@@ -164,14 +178,17 @@ impl Resolve<WriteArgs> for DeleteVariable {
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<DeleteVariableResponse> {
|
||||
if !user.admin {
|
||||
return Err(anyhow!("only admins can delete variables").into());
|
||||
return Err(
|
||||
anyhow!("Only admins can delete variables")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
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(),
|
||||
@@ -180,7 +197,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?;
|
||||
|
||||
@@ -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,5 +207,6 @@ pub struct GoogleUser {
|
||||
#[serde(rename = "sub")]
|
||||
pub id: String,
|
||||
pub email: String,
|
||||
#[serde(default)]
|
||||
pub picture: String,
|
||||
}
|
||||
|
||||
@@ -250,6 +250,7 @@ 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),
|
||||
|
||||
@@ -16,17 +16,16 @@ use super::cache::Cache;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ActionStates {
|
||||
pub build: Cache<String, Arc<ActionState<BuildActionState>>>,
|
||||
pub server: Cache<String, Arc<ActionState<ServerActionState>>>,
|
||||
pub stack: Cache<String, Arc<ActionState<StackActionState>>>,
|
||||
pub deployment:
|
||||
Cache<String, Arc<ActionState<DeploymentActionState>>>,
|
||||
pub server: Cache<String, Arc<ActionState<ServerActionState>>>,
|
||||
pub build: Cache<String, Arc<ActionState<BuildActionState>>>,
|
||||
pub repo: Cache<String, Arc<ActionState<RepoActionState>>>,
|
||||
pub procedure:
|
||||
Cache<String, Arc<ActionState<ProcedureActionState>>>,
|
||||
pub action: Cache<String, Arc<ActionState<ActionActionState>>>,
|
||||
pub resource_sync:
|
||||
Cache<String, Arc<ActionState<ResourceSyncActionState>>>,
|
||||
pub stack: Cache<String, Arc<ActionState<StackActionState>>>,
|
||||
pub sync: Cache<String, Arc<ActionState<ResourceSyncActionState>>>,
|
||||
}
|
||||
|
||||
/// Need to be able to check "busy" with write lock acquired.
|
||||
@@ -62,17 +61,33 @@ impl<States: Default + Busy + Copy + Send + 'static>
|
||||
/// Returns a guard that returns the states to default (not busy) when dropped.
|
||||
pub fn update(
|
||||
&self,
|
||||
handler: impl Fn(&mut States),
|
||||
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,
|
||||
) -> anyhow::Result<UpdateGuard<'_, States>> {
|
||||
let mut lock = self
|
||||
.0
|
||||
.lock()
|
||||
.map_err(|e| anyhow!("action state lock poisoned | {e:?}"))?;
|
||||
if lock.busy() {
|
||||
return Err(anyhow!("resource is busy"));
|
||||
.map_err(|e| anyhow!("Action state lock poisoned | {e:?}"))?;
|
||||
if busy_check && lock.busy() {
|
||||
return Err(anyhow!("Resource is busy"));
|
||||
}
|
||||
handler(&mut *lock);
|
||||
Ok(UpdateGuard(&self.0))
|
||||
update_fn(&mut *lock);
|
||||
Ok(UpdateGuard(&self.0, Box::new(return_fn)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +97,7 @@ 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
|
||||
@@ -95,6 +111,6 @@ impl<States: Default + Send + 'static> Drop
|
||||
return;
|
||||
}
|
||||
};
|
||||
*lock = States::default();
|
||||
self.1(&mut *lock);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,26 +13,31 @@ use database::mungos::{
|
||||
options::FindOneOptions,
|
||||
},
|
||||
};
|
||||
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 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 periphery_client::api::stats;
|
||||
use tokio::sync::Mutex;
|
||||
@@ -467,7 +472,7 @@ pub async fn get_action_state(id: &String) -> ActionState {
|
||||
.action
|
||||
.get(id)
|
||||
.await
|
||||
.map(|s| s.get().map(|s| s.running))
|
||||
.map(|s| s.get().map(|s| s.busy()))
|
||||
.transpose()
|
||||
.ok()
|
||||
.flatten()
|
||||
@@ -483,7 +488,7 @@ pub async fn get_procedure_state(id: &String) -> ProcedureState {
|
||||
.procedure
|
||||
.get(id)
|
||||
.await
|
||||
.map(|s| s.get().map(|s| s.running))
|
||||
.map(|s| s.get().map(|s| s.busy()))
|
||||
.transpose()
|
||||
.ok()
|
||||
.flatten()
|
||||
|
||||
@@ -45,10 +45,14 @@ async fn app() -> anyhow::Result<()> {
|
||||
|
||||
info!("Komodo Core version: v{}", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
if core_config().pretty_startup_config {
|
||||
info!("{:#?}", config.sanitized());
|
||||
} else {
|
||||
info!("{:?}", config.sanitized());
|
||||
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()),
|
||||
}
|
||||
|
||||
// Init jwt client to crash on failure
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
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;
|
||||
@@ -15,10 +17,11 @@ use komodo_client::entities::{
|
||||
};
|
||||
use periphery_client::api::{self, git::GetLatestCommit};
|
||||
use serror::Serror;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::periphery_client,
|
||||
helpers::{cache::Cache, periphery_client},
|
||||
monitor::{alert::check_alerts, record::record_server_stats},
|
||||
state::{db_client, deployment_status_cache, repo_status_cache},
|
||||
};
|
||||
@@ -110,14 +113,47 @@ async fn refresh_server_cache(ts: i64) {
|
||||
}
|
||||
};
|
||||
let futures = servers.into_iter().map(|server| async move {
|
||||
update_cache_for_server(&server).await;
|
||||
update_cache_for_server(&server, false).await;
|
||||
});
|
||||
join_all(futures).await;
|
||||
tokio::join!(check_alerts(ts), record_server_stats(ts));
|
||||
}
|
||||
|
||||
/// Makes sure cache for server doesn't update too frequently / simultaneously.
|
||||
/// 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) {
|
||||
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;
|
||||
|
||||
let (deployments, builds, repos, stacks) = tokio::join!(
|
||||
find_collect(
|
||||
&db_client().deployments,
|
||||
|
||||
@@ -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
|
||||
image.to_string()
|
||||
} else {
|
||||
format!("{image}:latest")
|
||||
}
|
||||
@@ -92,6 +92,9 @@ 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)
|
||||
@@ -250,20 +253,21 @@ 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
|
||||
image.to_string()
|
||||
} else {
|
||||
image + ":latest"
|
||||
format!("{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
|
||||
};
|
||||
|
||||
@@ -188,7 +188,7 @@ impl super::KomodoResource for Deployment {
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
update_cache_for_server(&server).await;
|
||||
update_cache_for_server(&server, true).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -34,8 +34,10 @@ 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},
|
||||
@@ -458,22 +460,31 @@ pub async fn create<T: KomodoResource>(
|
||||
name: &str,
|
||||
mut config: T::PartialConfig,
|
||||
user: &User,
|
||||
) -> anyhow::Result<Resource<T::Config, T::Info>> {
|
||||
) -> serror::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()
|
||||
));
|
||||
return Err(
|
||||
anyhow!(
|
||||
"User does not have permissions to create {}.",
|
||||
T::resource_type()
|
||||
)
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
if name.is_empty() {
|
||||
return Err(anyhow!("Must provide non-empty name for resource."));
|
||||
return Err(
|
||||
anyhow!("Must provide non-empty name for resource")
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
}
|
||||
|
||||
let name = T::validated_name(name);
|
||||
|
||||
if ObjectId::from_str(&name).is_ok() {
|
||||
return Err(anyhow!("valid ObjectIds cannot be used as names."));
|
||||
return Err(
|
||||
anyhow!("Valid ObjectIds cannot be used as names")
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure an existing resource with same name doesn't already exist
|
||||
@@ -489,7 +500,10 @@ pub async fn create<T: KomodoResource>(
|
||||
.into_iter()
|
||||
.any(|r| r.name == name)
|
||||
{
|
||||
return Err(anyhow!("Must provide unique name for resource."));
|
||||
return Err(
|
||||
anyhow!("Resource with name '{}' already exists", name)
|
||||
.status_code(StatusCode::CONFLICT),
|
||||
);
|
||||
}
|
||||
|
||||
let start_ts = komodo_timestamp();
|
||||
@@ -853,9 +867,15 @@ pub async fn delete<T: KomodoResource>(
|
||||
);
|
||||
update.push_simple_log("Deleted Toml", toml);
|
||||
|
||||
if let Err(e) = T::post_delete(&resource, &mut update).await {
|
||||
update.push_error_log("post delete", format_serror(&e.into()));
|
||||
}
|
||||
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)
|
||||
);
|
||||
|
||||
refresh_all_resources_cache().await;
|
||||
|
||||
@@ -865,6 +885,26 @@ 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")]
|
||||
|
||||
@@ -300,6 +300,7 @@ async fn get_repo_state_from_db(id: &str) -> RepoState {
|
||||
"$or": [
|
||||
{ "operation": "CloneRepo" },
|
||||
{ "operation": "PullRepo" },
|
||||
{ "operation": "BuildRepo" },
|
||||
],
|
||||
})
|
||||
.with_options(
|
||||
|
||||
@@ -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).await;
|
||||
update_cache_for_server(created, true).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ impl super::KomodoResource for Server {
|
||||
updated: &Self,
|
||||
_update: &mut Update,
|
||||
) -> anyhow::Result<()> {
|
||||
update_cache_for_server(updated).await;
|
||||
update_cache_for_server(updated, true).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -252,7 +252,7 @@ impl super::KomodoResource for Stack {
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
update_cache_for_server(&server).await;
|
||||
update_cache_for_server(&server, true).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ impl super::KomodoResource for ResourceSync {
|
||||
|
||||
async fn busy(id: &String) -> anyhow::Result<bool> {
|
||||
action_states()
|
||||
.resource_sync
|
||||
.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()
|
||||
.resource_sync
|
||||
.sync
|
||||
.get(id)
|
||||
.await
|
||||
.and_then(|s| {
|
||||
|
||||
@@ -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).await;
|
||||
update_cache_for_server(&server, true).await;
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
@@ -242,7 +242,7 @@ async fn ensure_first_server_and_builder() {
|
||||
return;
|
||||
};
|
||||
if let Err(e) = (CreateBuilder {
|
||||
name: String::from("Local"),
|
||||
name: config.first_server_name.clone(),
|
||||
config: PartialBuilderConfig::Server(
|
||||
PartialServerBuilderConfig {
|
||||
server_id: Some(server.id),
|
||||
|
||||
@@ -40,6 +40,7 @@ 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
|
||||
|
||||
@@ -147,6 +147,7 @@ pub trait ExecuteResourceSync: ResourceSyncTrait {
|
||||
sync_user(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.error)
|
||||
{
|
||||
Ok(resource) => resource.id,
|
||||
Err(e) => {
|
||||
|
||||
@@ -825,6 +825,7 @@ impl ExecuteResourceSync for Procedure {
|
||||
sync_user(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.error)
|
||||
{
|
||||
Ok(resource) => resource.id,
|
||||
Err(e) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
## 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 ./
|
||||
@@ -10,7 +11,7 @@ COPY ./client/periphery ./client/periphery
|
||||
COPY ./bin/periphery ./bin/periphery
|
||||
|
||||
# Compile app
|
||||
RUN cargo build -p komodo_periphery --release
|
||||
RUN cargo build -p komodo_periphery --release && cargo strip
|
||||
|
||||
# Final Image
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
@@ -12,7 +12,7 @@ use interpolate::Interpolator;
|
||||
use komodo_client::entities::{
|
||||
EnvironmentVar, all_logs_success,
|
||||
build::{Build, BuildConfig},
|
||||
environment_vars_from_str, get_image_names, optional_string,
|
||||
environment_vars_from_str, optional_string,
|
||||
to_path_compatible_name,
|
||||
update::Log,
|
||||
};
|
||||
@@ -25,9 +25,7 @@ use resolver_api::Resolve;
|
||||
use tokio::fs;
|
||||
|
||||
use crate::{
|
||||
build::{
|
||||
image_tags, parse_build_args, parse_secret_args, write_dockerfile,
|
||||
},
|
||||
build::{parse_build_args, parse_secret_args, write_dockerfile},
|
||||
config::periphery_config,
|
||||
docker::docker_login,
|
||||
helpers::{parse_extra_args, parse_labels},
|
||||
@@ -126,8 +124,9 @@ impl Resolve<super::Args> for build::Build {
|
||||
mut build,
|
||||
repo: linked_repo,
|
||||
registry_tokens,
|
||||
additional_tags,
|
||||
mut replacers,
|
||||
commit_hash,
|
||||
additional_tags,
|
||||
} = self;
|
||||
|
||||
let mut logs = Vec::new();
|
||||
@@ -145,8 +144,6 @@ impl Resolve<super::Args> for build::Build {
|
||||
name,
|
||||
config:
|
||||
BuildConfig {
|
||||
version,
|
||||
image_tag,
|
||||
build_path,
|
||||
dockerfile_path,
|
||||
build_args,
|
||||
@@ -265,8 +262,6 @@ 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")?;
|
||||
@@ -291,9 +286,9 @@ impl Resolve<super::Args> for build::Build {
|
||||
|
||||
let buildx = if *use_buildx { " buildx" } else { "" };
|
||||
|
||||
let image_tags =
|
||||
image_tags(&image_names, image_tag, version, &additional_tags)
|
||||
.context("Failed to parse image tags into command")?;
|
||||
let image_tags = build
|
||||
.get_image_tags_as_arg(commit_hash.as_deref(), &additional_tags)
|
||||
.context("Failed to parse image tags into command")?;
|
||||
|
||||
let maybe_push = if should_push { " --push" } else { "" };
|
||||
|
||||
|
||||
@@ -712,6 +712,7 @@ impl Resolve<super::Args> for ComposeRun {
|
||||
command,
|
||||
no_tty,
|
||||
no_deps,
|
||||
detach,
|
||||
service_ports,
|
||||
env,
|
||||
workdir,
|
||||
@@ -783,6 +784,9 @@ 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");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::{
|
||||
use anyhow::{Context, anyhow};
|
||||
use formatting::format_serror;
|
||||
use komodo_client::{
|
||||
entities::{EnvironmentVar, Version, update::Log},
|
||||
entities::{EnvironmentVar, update::Log},
|
||||
parsers::QUOTE_PATTERN,
|
||||
};
|
||||
|
||||
@@ -52,34 +52,6 @@ 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()
|
||||
|
||||
@@ -43,8 +43,9 @@ pub async fn validate_files(
|
||||
"Validate Files",
|
||||
format_serror(
|
||||
&anyhow!(
|
||||
"Ensure the run_directory and all file paths are correct."
|
||||
"Missing files: {}", res.missing_files.join(", ")
|
||||
)
|
||||
.context("Ensure the run_directory and all file paths are correct.")
|
||||
.context("A file doesn't exist after writing stack.")
|
||||
.into(),
|
||||
),
|
||||
|
||||
@@ -2,7 +2,8 @@ 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, SystemLoadAverage,
|
||||
SystemProcess, SystemStats,
|
||||
};
|
||||
use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
@@ -28,6 +28,7 @@ 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
|
||||
|
||||
@@ -379,6 +379,9 @@ 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>,
|
||||
|
||||
@@ -68,7 +68,7 @@ impl Busy for ProcedureActionState {
|
||||
|
||||
impl Busy for ActionActionState {
|
||||
fn busy(&self) -> bool {
|
||||
self.running
|
||||
self.running > 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use serde::{
|
||||
__private::de::{Content, ContentDeserializer},
|
||||
Deserialize, Deserializer,
|
||||
de::{IntoDeserializer, Visitor},
|
||||
};
|
||||
@@ -69,17 +68,15 @@ impl<'de, T: Deserialize<'de>> Visitor<'de>
|
||||
let mut res =
|
||||
Vec::with_capacity(seq.size_hint().unwrap_or_default());
|
||||
loop {
|
||||
match seq.next_element::<Content>() {
|
||||
Ok(Some(content)) => {
|
||||
match T::deserialize::<ContentDeserializer<'_, S::Error>>(
|
||||
content.clone().into_deserializer(),
|
||||
) {
|
||||
match seq.next_element::<serde_json::Value>() {
|
||||
Ok(Some(value)) => {
|
||||
match T::deserialize(value.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 | {content:?} | {e:?}",
|
||||
"WARN: failed to parse item in list | {value:?} | {e:?}",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,8 +222,8 @@ impl Default for ActionConfig {
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)]
|
||||
pub struct ActionActionState {
|
||||
/// Whether the action is currently running.
|
||||
pub running: bool,
|
||||
/// Number of instances of the Action currently running
|
||||
pub running: u32,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::sync::OnceLock;
|
||||
use std::{fmt::Write, sync::OnceLock};
|
||||
|
||||
use bson::{Document, doc};
|
||||
use derive_builder::Builder;
|
||||
@@ -26,6 +26,127 @@ 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>;
|
||||
|
||||
@@ -169,6 +290,24 @@ 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(
|
||||
@@ -344,6 +483,10 @@ fn default_auto_increment_version() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_include_tag() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_git_provider() -> String {
|
||||
String::from("github.com")
|
||||
}
|
||||
@@ -377,6 +520,9 @@ 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,6 +125,8 @@ 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>,
|
||||
@@ -509,6 +511,12 @@ 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 =
|
||||
// ===========
|
||||
@@ -729,6 +737,7 @@ 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(),
|
||||
@@ -772,6 +781,8 @@ 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,
|
||||
|
||||
@@ -5,7 +5,6 @@ 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};
|
||||
@@ -129,54 +128,6 @@ 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()
|
||||
}
|
||||
@@ -511,8 +462,17 @@ impl RepoExecutionArgs {
|
||||
&self,
|
||||
access_token: Option<&str>,
|
||||
) -> anyhow::Result<String> {
|
||||
let access_token_at = match &access_token {
|
||||
Some(token) => format!("token:{token}@"),
|
||||
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()))
|
||||
}
|
||||
},
|
||||
None => String::new(),
|
||||
};
|
||||
let protocol = if self.https { "https" } else { "http" };
|
||||
|
||||
@@ -191,4 +191,4 @@ pub struct SystemLoadAverage {
|
||||
pub five: f64,
|
||||
/// 15m load average
|
||||
pub fifteen: f64,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "komodo_client",
|
||||
"version": "1.19.1",
|
||||
"version": "1.19.5",
|
||||
"description": "Komodo client package",
|
||||
"homepage": "https://komo.do",
|
||||
"main": "dist/lib.js",
|
||||
|
||||
@@ -199,6 +199,11 @@ 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!);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -598,6 +598,12 @@ 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. */
|
||||
@@ -1351,8 +1357,8 @@ export type ExportResourcesToTomlResponse = TomlResponse;
|
||||
export type FindUserResponse = User;
|
||||
|
||||
export interface ActionActionState {
|
||||
/** Whether the action is currently running. */
|
||||
running: boolean;
|
||||
/** Number of instances of the Action currently running */
|
||||
running: number;
|
||||
}
|
||||
|
||||
export type GetActionActionStateResponse = ActionActionState;
|
||||
@@ -7713,6 +7719,8 @@ 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 */
|
||||
|
||||
@@ -18,6 +18,8 @@ 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>,
|
||||
|
||||
@@ -272,6 +272,9 @@ 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>,
|
||||
|
||||
@@ -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`,
|
||||
## This file is bundled into the official image, `ghcr.io/moghtech/komodo-core`,
|
||||
## as the default config at `/config/.default.config.toml`.
|
||||
## Komodo can start with no external config file mounted.
|
||||
## Komodo Core 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 preffered. You can convert here:
|
||||
## Can also use JSON or YAML if preferred. You can convert here:
|
||||
## - YAML: https://it-tools.tech/toml-to-yaml
|
||||
## - JSON: https://it-tools.tech/toml-to-json
|
||||
|
||||
|
||||
@@ -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 preffered. You can convert here:
|
||||
## You can also use JSON or YAML if preferred. You can convert here:
|
||||
## - YAML: https://it-tools.tech/toml-to-yaml
|
||||
## - JSON: https://it-tools.tech/toml-to-json
|
||||
|
||||
|
||||
@@ -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 preffered. You can convert here:
|
||||
## You can also use JSON or YAML if preferred. You can convert here:
|
||||
## - YAML: https://it-tools.tech/toml-to-yaml
|
||||
## - JSON: https://it-tools.tech/toml-to-json
|
||||
|
||||
|
||||
@@ -15,4 +15,5 @@ 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)
|
||||
- [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)
|
||||
|
||||
@@ -71,7 +71,7 @@ permissions = [
|
||||
|
||||
## Administration
|
||||
|
||||
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}`.
|
||||
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}`.
|
||||
|
||||
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.
|
||||
|
||||
@@ -14,6 +14,10 @@ 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
|
||||
|
||||
@@ -5,10 +5,12 @@ 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)
|
||||
- Lower RAM usage, Higher CPU usage.
|
||||
|
||||
:::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.
|
||||
:::
|
||||
|
||||
:::info
|
||||
**FerretDB v1** users:
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"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",
|
||||
|
||||
@@ -88,6 +88,9 @@ 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);
|
||||
}
|
||||
};
|
||||
|
||||
Vendored
+10
-2
@@ -595,6 +595,12 @@ 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. */
|
||||
@@ -1457,8 +1463,8 @@ export type ExportAllResourcesToTomlResponse = TomlResponse;
|
||||
export type ExportResourcesToTomlResponse = TomlResponse;
|
||||
export type FindUserResponse = User;
|
||||
export interface ActionActionState {
|
||||
/** Whether the action is currently running. */
|
||||
running: boolean;
|
||||
/** Number of instances of the Action currently running */
|
||||
running: number;
|
||||
}
|
||||
export type GetActionActionStateResponse = ActionActionState;
|
||||
export type GetActionResponse = Action;
|
||||
@@ -7306,6 +7312,8 @@ 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
@@ -136,12 +136,12 @@ const GroupActionDialog = ({
|
||||
|
||||
return (
|
||||
<Dialog open={!!action} onOpenChange={(o) => !o && onClose()}>
|
||||
<DialogContent>
|
||||
<DialogContent className="max-h-[85vh] overflow-y-auto">
|
||||
<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">
|
||||
<ul className="p-4 bg-accent text-sm list-disc list-inside max-h-[300px] overflow-y-auto">
|
||||
{selected.map((resource) => (
|
||||
<li key={resource}>{resource}</li>
|
||||
))}
|
||||
|
||||
@@ -170,6 +170,7 @@ interface SectionProps {
|
||||
actions?: ReactNode;
|
||||
// otherwise items-start
|
||||
itemsCenterTitleRow?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Section = ({
|
||||
@@ -180,8 +181,9 @@ export const Section = ({
|
||||
actions,
|
||||
children,
|
||||
itemsCenterTitleRow,
|
||||
className,
|
||||
}: SectionProps) => (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className={cn("flex flex-col gap-4", className)}>
|
||||
{(title || icon || titleRight || titleOther || actions) && (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -222,6 +224,7 @@ export const NewLayout = ({
|
||||
}) => {
|
||||
const [open, set] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
@@ -248,9 +251,17 @@ export const NewLayout = ({
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
await onConfirm();
|
||||
setLoading(false);
|
||||
set(false);
|
||||
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);
|
||||
}
|
||||
}}
|
||||
disabled={!enabled || loading}
|
||||
>
|
||||
|
||||
@@ -56,12 +56,14 @@ 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;
|
||||
@@ -139,13 +141,7 @@ export const MonacoEditor = ({
|
||||
)}px`;
|
||||
}, [editor, line_count]);
|
||||
|
||||
const { theme: _theme } = useTheme();
|
||||
const theme =
|
||||
_theme === "system"
|
||||
? window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light"
|
||||
: _theme;
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
const options: monaco.editor.IStandaloneEditorConstructionOptions = {
|
||||
minimap: { enabled: false },
|
||||
@@ -169,7 +165,8 @@ export const MonacoEditor = ({
|
||||
<Editor
|
||||
language={language}
|
||||
value={value}
|
||||
theme={theme}
|
||||
theme={currentTheme}
|
||||
defaultPath={defaultPath(filename)}
|
||||
options={options}
|
||||
onChange={(v) => onValueChange?.(v ?? "")}
|
||||
onMount={(editor) => setEditor(editor)}
|
||||
@@ -178,6 +175,14 @@ 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;
|
||||
|
||||
@@ -222,13 +227,7 @@ export const MonacoDiffEditor = ({
|
||||
)}px`;
|
||||
}, [editor, line_count]);
|
||||
|
||||
const { theme: _theme } = useTheme();
|
||||
const theme =
|
||||
_theme === "system"
|
||||
? window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light"
|
||||
: _theme;
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
const options: monaco.editor.IStandaloneDiffEditorConstructionOptions = {
|
||||
minimap: { enabled: true },
|
||||
@@ -251,7 +250,7 @@ export const MonacoDiffEditor = ({
|
||||
language={language}
|
||||
original={original}
|
||||
modified={modified}
|
||||
theme={theme}
|
||||
theme={currentTheme}
|
||||
options={options}
|
||||
onMount={(editor) => {
|
||||
const modifiedEditor = editor.getModifiedEditor();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useAllResources, useLocalStorage, useRead, useUser } from "@lib/hooks";
|
||||
import { useAllResources, useLocalStorage, useRead, useSettingsView, useUser } from "@lib/hooks";
|
||||
import { Button } from "@ui/button";
|
||||
import {
|
||||
CommandDialog,
|
||||
@@ -126,6 +126,7 @@ const useOmniItems = (
|
||||
): Record<string, OmniItem[]> => {
|
||||
const user = useUser().data;
|
||||
const resources = useAllResources();
|
||||
const [_, setSettingsView] = useSettingsView();
|
||||
return useMemo(() => {
|
||||
const searchTerms = search
|
||||
.toLowerCase()
|
||||
@@ -166,7 +167,10 @@ const useOmniItems = (
|
||||
type: "Server" as UsableResource,
|
||||
label: "Users",
|
||||
icon: <User className="w-4 h-4" />,
|
||||
onSelect: () => nav("/users"),
|
||||
onSelect: () => {
|
||||
setSettingsView("Users");
|
||||
nav("/settings");
|
||||
},
|
||||
template: false,
|
||||
}) as OmniItem,
|
||||
]
|
||||
|
||||
@@ -119,11 +119,12 @@ export const ActionComponents: RequiredResourceComponents = {
|
||||
|
||||
Actions: {
|
||||
RunAction: ({ id }) => {
|
||||
const running = useRead(
|
||||
"GetActionActionState",
|
||||
{ action: id },
|
||||
{ refetchInterval: 5000 }
|
||||
).data?.running;
|
||||
const running =
|
||||
(useRead(
|
||||
"GetActionActionState",
|
||||
{ action: id },
|
||||
{ refetchInterval: 5000 }
|
||||
).data?.running ?? 0) > 0;
|
||||
const { mutate, isPending } = useExecute("RunAction");
|
||||
const action = useAction(id);
|
||||
if (!action) return null;
|
||||
|
||||
@@ -163,6 +163,7 @@ export const BuildConfig = ({
|
||||
|
||||
const version_component: ConfigComponent<Types.BuildConfig> = {
|
||||
label: "Version",
|
||||
labelHidden: true,
|
||||
components: {
|
||||
version: (_version, set) => {
|
||||
const version =
|
||||
@@ -173,6 +174,7 @@ 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}
|
||||
@@ -228,6 +230,8 @@ 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>[] = [
|
||||
{
|
||||
@@ -262,12 +266,7 @@ export const BuildConfig = ({
|
||||
|
||||
{image_registries?.map((registry, index) => (
|
||||
<ImageRegistryConfig
|
||||
key={
|
||||
(registry.domain ?? "") +
|
||||
(registry.organization ?? "") +
|
||||
(registry.account ?? "") +
|
||||
index
|
||||
}
|
||||
key={index}
|
||||
registry={registry}
|
||||
imageName={imageName}
|
||||
setRegistry={(registry) =>
|
||||
@@ -292,6 +291,29 @@ 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,
|
||||
@@ -314,19 +336,6 @@ 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:
|
||||
|
||||
@@ -439,12 +439,23 @@ export const CopyResource = ({
|
||||
|
||||
const nav = useNavigate();
|
||||
const inv = useInvalidate();
|
||||
const { mutate } = useWrite(`Copy${type}`, {
|
||||
onSuccess: (res) => {
|
||||
const { mutateAsync: copy } = useWrite(`Copy${type}`);
|
||||
|
||||
const onConfirm = async () => {
|
||||
if (!name) return;
|
||||
try {
|
||||
const res = await copy({ id, name });
|
||||
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}>
|
||||
@@ -472,9 +483,8 @@ export const CopyResource = ({
|
||||
title="Copy"
|
||||
icon={<Check className="w-4 h-4" />}
|
||||
disabled={!name}
|
||||
onClick={() => {
|
||||
mutate({ id, name });
|
||||
setOpen(false);
|
||||
onClick={async () => {
|
||||
await onConfirm();
|
||||
}}
|
||||
/>
|
||||
</DialogFooter>
|
||||
@@ -526,10 +536,13 @@ export const NewResource = ({
|
||||
: {};
|
||||
const onConfirm = async () => {
|
||||
if (!name) toast({ title: "Name cannot be empty" });
|
||||
const id = templateId
|
||||
? (await copy({ name, id: templateId }))._id?.$oid!
|
||||
: (await create({ name, config }))._id?.$oid!;
|
||||
nav(`/${usableResourcePath(type)}/${id}`);
|
||||
const result = templateId
|
||||
? await copy({ name, id: templateId })
|
||||
: await create({ name, config });
|
||||
const resourceId = result._id?.$oid;
|
||||
if (resourceId) {
|
||||
nav(`/${usableResourcePath(type)}/${resourceId}`);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<NewLayout
|
||||
@@ -547,7 +560,7 @@ export const NewResource = ({
|
||||
onKeyDown={(e) => {
|
||||
if (!name) return;
|
||||
if (e.key === "Enter") {
|
||||
onConfirm();
|
||||
onConfirm().catch(() => {});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1132,6 +1132,7 @@ const TARGET_COMPONENTS: ExecutionConfigs = {
|
||||
command: undefined,
|
||||
no_tty: undefined,
|
||||
no_deps: undefined,
|
||||
detach: undefined,
|
||||
service_ports: undefined,
|
||||
env: undefined,
|
||||
workdir: undefined,
|
||||
@@ -1151,6 +1152,7 @@ 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 ?? "");
|
||||
@@ -1175,6 +1177,7 @@ 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 ?? "");
|
||||
@@ -1216,6 +1219,7 @@ const TARGET_COMPONENTS: ExecutionConfigs = {
|
||||
user: user || undefined,
|
||||
entrypoint: entrypoint || undefined,
|
||||
pull: pull ? true : undefined,
|
||||
detach: detach ? true : undefined,
|
||||
env,
|
||||
} as any);
|
||||
setOpen(false);
|
||||
@@ -1273,6 +1277,10 @@ 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, usePendingView } from ".";
|
||||
import { useFullResourceSync, useResourceSyncTabsView } from ".";
|
||||
|
||||
export const RefreshSync = ({ id }: { id: string }) => {
|
||||
const inv = useInvalidate();
|
||||
@@ -34,11 +34,10 @@ export const ExecuteSync = ({ id }: { id: string }) => {
|
||||
{ refetchInterval: 5000 }
|
||||
).data?.syncing;
|
||||
const sync = useFullResourceSync(id);
|
||||
const [_pendingView] = usePendingView();
|
||||
const pendingView = sync?.config?.managed ? _pendingView : "Execute";
|
||||
const { view } = useResourceSyncTabsView(sync);
|
||||
|
||||
if (
|
||||
pendingView === "Commit" ||
|
||||
view !== "Execute" ||
|
||||
!sync ||
|
||||
sync_no_changes(sync) ||
|
||||
!sync.info?.remote_contents
|
||||
@@ -73,11 +72,12 @@ 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 (pendingView === "Execute" || !canWrite || !sync) return null;
|
||||
if (view !== "Commit" || !canWrite || !sync) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const freshSync =
|
||||
!sync.config?.files_on_host &&
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { atomWithStorage, useLocalStorage, useRead, useUser } from "@lib/hooks";
|
||||
import { atomWithStorage, useRead, useUser } from "@lib/hooks";
|
||||
import { RequiredResourceComponents } from "@types";
|
||||
import { Card } from "@ui/card";
|
||||
import { Clock, FolderSync } from "lucide-react";
|
||||
@@ -45,23 +45,16 @@ const ResourceSyncIcon = ({ id, size }: { id?: string; size: number }) => {
|
||||
return <FolderSync className={cn(`w-${size} h-${size}`, state && color)} />;
|
||||
};
|
||||
|
||||
const pendingViewAtom = atomWithStorage<"Execute" | "Commit">(
|
||||
"sync-view-v1",
|
||||
"Execute"
|
||||
type ResourceSyncTabsView = "Config" | "Info" | "Execute" | "Commit";
|
||||
const syncTabsViewAtom = atomWithStorage<ResourceSyncTabsView>(
|
||||
"sync-tabs-v4",
|
||||
"Config"
|
||||
);
|
||||
export const usePendingView = () => {
|
||||
return useAtom(pendingViewAtom) as [
|
||||
"Execute" | "Commit",
|
||||
(view: "Execute" | "Commit") => void,
|
||||
];
|
||||
};
|
||||
|
||||
const ConfigInfoPending = ({ id }: { id: string }) => {
|
||||
const [_view, setView] = useLocalStorage<"Config" | "Info" | "Pending">(
|
||||
"sync-tabs-v3",
|
||||
"Config"
|
||||
);
|
||||
const sync = useFullResourceSync(id);
|
||||
export const useResourceSyncTabsView = (
|
||||
sync: Types.ResourceSync | undefined
|
||||
) => {
|
||||
const [_view, setView] = useAtom<ResourceSyncTabsView>(syncTabsViewAtom);
|
||||
|
||||
const hideInfo = sync?.config?.files_on_host
|
||||
? false
|
||||
@@ -75,13 +68,28 @@ const ConfigInfoPending = ({ id }: { id: string }) => {
|
||||
const view =
|
||||
_view === "Info" && hideInfo
|
||||
? "Config"
|
||||
: _view === "Pending" && !showPending
|
||||
: (_view === "Execute" || _view === "Commit") && !showPending
|
||||
? sync?.config?.files_on_host ||
|
||||
sync?.config?.repo ||
|
||||
sync?.config?.linked_repo
|
||||
? "Info"
|
||||
: "Config"
|
||||
: _view;
|
||||
: _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);
|
||||
|
||||
const title = (
|
||||
<TabsList className="justify-start w-fit">
|
||||
@@ -96,12 +104,21 @@ const ConfigInfoPending = ({ id }: { id: string }) => {
|
||||
Info
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="Pending"
|
||||
value="Execute"
|
||||
className="w-[110px]"
|
||||
disabled={!showPending}
|
||||
>
|
||||
Pending
|
||||
Execute
|
||||
</TabsTrigger>
|
||||
{sync?.config?.managed && (
|
||||
<TabsTrigger
|
||||
value="Commit"
|
||||
className="w-[110px]"
|
||||
disabled={!showPending}
|
||||
>
|
||||
Commit
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
);
|
||||
return (
|
||||
@@ -112,7 +129,10 @@ const ConfigInfoPending = ({ id }: { id: string }) => {
|
||||
<TabsContent value="Info">
|
||||
<ResourceSyncInfo id={id} titleOther={title} />
|
||||
</TabsContent>
|
||||
<TabsContent value="Pending">
|
||||
<TabsContent value="Execute">
|
||||
<ResourceSyncPending id={id} titleOther={title} />
|
||||
</TabsContent>
|
||||
<TabsContent value="Commit">
|
||||
<ResourceSyncPending id={id} titleOther={title} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
@@ -164,7 +184,7 @@ export const ResourceSyncComponents: RequiredResourceComponents = {
|
||||
},
|
||||
|
||||
GroupActions: () => (
|
||||
<GroupActions type="ResourceSync" actions={["RunSync"]} />
|
||||
<GroupActions type="ResourceSync" actions={["RunSync", "CommitSync"]} />
|
||||
),
|
||||
|
||||
Table: ({ resources }) => (
|
||||
|
||||
@@ -10,8 +10,7 @@ import { cn, sanitizeOnlySpan } from "@lib/utils";
|
||||
import { ConfirmButton } from "@components/util";
|
||||
import { SquarePlay } from "lucide-react";
|
||||
import { usePermissions } from "@lib/hooks";
|
||||
import { useFullResourceSync, usePendingView } from ".";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@ui/tabs";
|
||||
import { useFullResourceSync, useResourceSyncTabsView } from ".";
|
||||
import { ResourceDiff } from "komodo_client/dist/types";
|
||||
|
||||
export const ResourceSyncPending = ({
|
||||
@@ -24,31 +23,16 @@ 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}
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
{pendingView === "Execute" && (
|
||||
{view === "Execute" && (
|
||||
<>
|
||||
Update resources in the
|
||||
<div className="font-bold">UI</div>
|
||||
@@ -56,7 +40,7 @@ export const ResourceSyncPending = ({
|
||||
<div className="font-bold">file changes.</div>
|
||||
</>
|
||||
)}
|
||||
{pendingView === "Commit" && (
|
||||
{view === "Commit" && (
|
||||
<>
|
||||
Update resources in the
|
||||
<div className="font-bold">file</div>
|
||||
@@ -89,7 +73,7 @@ export const ResourceSyncPending = ({
|
||||
) : undefined}
|
||||
|
||||
{/* Pending Deploy */}
|
||||
{pendingView === "Execute" && sync?.info?.pending_deploy?.to_deploy ? (
|
||||
{view === "Execute" && sync?.info?.pending_deploy?.to_deploy ? (
|
||||
<Card>
|
||||
<CardHeader
|
||||
className={cn(
|
||||
@@ -118,13 +102,10 @@ 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,
|
||||
pendingView === "Commit"
|
||||
)
|
||||
diff_type_intention(update.data.type, view === "Commit")
|
||||
)}
|
||||
>
|
||||
{pendingView === "Commit"
|
||||
{view === "Commit"
|
||||
? reverse_pending_type(update.data.type)
|
||||
: update.data.type}{" "}
|
||||
{update.target.type}
|
||||
@@ -139,7 +120,7 @@ export const ResourceSyncPending = ({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{canExecute && pendingView === "Execute" && (
|
||||
{canExecute && view === "Execute" && (
|
||||
<ConfirmButton
|
||||
title="Execute Change"
|
||||
icon={<SquarePlay className="w-4 h-4" />}
|
||||
@@ -168,7 +149,7 @@ export const ResourceSyncPending = ({
|
||||
)}
|
||||
{update.data.type === "Update" && (
|
||||
<>
|
||||
{pendingView === "Execute" && (
|
||||
{view === "Execute" && (
|
||||
<MonacoDiffEditor
|
||||
original={update.data.data.current}
|
||||
modified={update.data.data.proposed}
|
||||
@@ -176,7 +157,7 @@ export const ResourceSyncPending = ({
|
||||
readOnly
|
||||
/>
|
||||
)}
|
||||
{pendingView === "Commit" && (
|
||||
{view === "Commit" && (
|
||||
<MonacoDiffEditor
|
||||
original={update.data.data.proposed}
|
||||
modified={update.data.data.current}
|
||||
@@ -205,13 +186,11 @@ export const ResourceSyncPending = ({
|
||||
className={cn(
|
||||
"font-mono pb-2",
|
||||
text_color_class_by_intention(
|
||||
diff_type_intention(data.type, pendingView === "Commit")
|
||||
diff_type_intention(data.type, view === "Commit")
|
||||
)
|
||||
)}
|
||||
>
|
||||
{pendingView === "Commit"
|
||||
? reverse_pending_type(data.type)
|
||||
: data.type}{" "}
|
||||
{view === "Commit" ? reverse_pending_type(data.type) : data.type}{" "}
|
||||
Variable
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -224,7 +203,7 @@ export const ResourceSyncPending = ({
|
||||
)}
|
||||
{data.type === "Update" && (
|
||||
<>
|
||||
{pendingView === "Execute" && (
|
||||
{view === "Execute" && (
|
||||
<MonacoDiffEditor
|
||||
original={data.data.current}
|
||||
modified={data.data.proposed}
|
||||
@@ -232,7 +211,7 @@ export const ResourceSyncPending = ({
|
||||
readOnly
|
||||
/>
|
||||
)}
|
||||
{pendingView === "Commit" && (
|
||||
{view === "Commit" && (
|
||||
<MonacoDiffEditor
|
||||
original={data.data.proposed}
|
||||
modified={data.data.current}
|
||||
@@ -261,13 +240,11 @@ export const ResourceSyncPending = ({
|
||||
className={cn(
|
||||
"font-mono pb-2",
|
||||
text_color_class_by_intention(
|
||||
diff_type_intention(data.type, pendingView === "Commit")
|
||||
diff_type_intention(data.type, view === "Commit")
|
||||
)
|
||||
)}
|
||||
>
|
||||
{pendingView === "Commit"
|
||||
? reverse_pending_type(data.type)
|
||||
: data.type}{" "}
|
||||
{view === "Commit" ? reverse_pending_type(data.type) : data.type}{" "}
|
||||
User Group
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -280,7 +257,7 @@ export const ResourceSyncPending = ({
|
||||
)}
|
||||
{data.type === "Update" && (
|
||||
<>
|
||||
{pendingView === "Execute" && (
|
||||
{view === "Execute" && (
|
||||
<MonacoDiffEditor
|
||||
original={data.data.current}
|
||||
modified={data.data.proposed}
|
||||
@@ -288,7 +265,7 @@ export const ResourceSyncPending = ({
|
||||
readOnly
|
||||
/>
|
||||
)}
|
||||
{pendingView === "Commit" && (
|
||||
{view === "Commit" && (
|
||||
<MonacoDiffEditor
|
||||
original={data.data.proposed}
|
||||
modified={data.data.current}
|
||||
|
||||
@@ -44,6 +44,12 @@ 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;
|
||||
|
||||
@@ -51,24 +57,26 @@ 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)
|
||||
)
|
||||
)}
|
||||
/>
|
||||
);
|
||||
@@ -137,10 +145,7 @@ 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>
|
||||
@@ -221,10 +226,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 (
|
||||
@@ -242,13 +247,14 @@ 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>
|
||||
@@ -306,7 +312,11 @@ export const ServerComponents: RequiredResourceComponents = {
|
||||
),
|
||||
|
||||
Dashboard: () => {
|
||||
const summary = useRead("GetServersSummary", {}, { refetchInterval: 15_000 }).data;
|
||||
const summary = useRead(
|
||||
"GetServersSummary",
|
||||
{},
|
||||
{ refetchInterval: 15_000 }
|
||||
).data;
|
||||
return (
|
||||
<DashboardPieChart
|
||||
data={[
|
||||
@@ -363,18 +373,19 @@ 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)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
@@ -384,13 +395,13 @@ export const ServerComponents: RequiredResourceComponents = {
|
||||
Info: {
|
||||
Version: ServerVersion,
|
||||
Cpu: ({ id }) => {
|
||||
const server = useServer(id);
|
||||
const isServerAvailable = useIsServerAvailable(id);
|
||||
const core_count =
|
||||
useRead(
|
||||
"GetSystemInformation",
|
||||
{ server: id },
|
||||
{
|
||||
enabled: server ? server.info.state !== "Disabled" : false,
|
||||
enabled: isServerAvailable,
|
||||
refetchInterval: 5000,
|
||||
}
|
||||
).data?.core_count ?? 0;
|
||||
@@ -402,19 +413,19 @@ export const ServerComponents: RequiredResourceComponents = {
|
||||
);
|
||||
},
|
||||
LoadAvg: ({ id }) => {
|
||||
const server = useServer(id);
|
||||
const isServerAvailable = useIsServerAvailable(id);
|
||||
const stats = useRead(
|
||||
"GetSystemStats",
|
||||
{ server: id },
|
||||
{
|
||||
enabled: server ? server.info.state !== "Disabled" : false,
|
||||
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" />
|
||||
@@ -423,12 +434,12 @@ export const ServerComponents: RequiredResourceComponents = {
|
||||
);
|
||||
},
|
||||
Mem: ({ id }) => {
|
||||
const server = useServer(id);
|
||||
const isServerAvailable = useIsServerAvailable(id);
|
||||
const stats = useRead(
|
||||
"GetSystemStats",
|
||||
{ server: id },
|
||||
{
|
||||
enabled: server ? server.info.state !== "Disabled" : false,
|
||||
enabled: isServerAvailable,
|
||||
refetchInterval: 5000,
|
||||
}
|
||||
).data;
|
||||
@@ -440,12 +451,12 @@ export const ServerComponents: RequiredResourceComponents = {
|
||||
);
|
||||
},
|
||||
Disk: ({ id }) => {
|
||||
const server = useServer(id);
|
||||
const isServerAvailable = useIsServerAvailable(id);
|
||||
const stats = useRead(
|
||||
"GetSystemStats",
|
||||
{ server: id },
|
||||
{
|
||||
enabled: server ? server.info.state !== "Disabled" : false,
|
||||
enabled: isServerAvailable,
|
||||
refetchInterval: 5000,
|
||||
}
|
||||
).data;
|
||||
@@ -616,11 +627,12 @@ 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
|
||||
|
||||
@@ -3,6 +3,7 @@ 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;
|
||||
@@ -73,11 +74,19 @@ export const ServerMonitoringTable = ({ search = "" }: { search?: string }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const useStats = (id: string) =>
|
||||
useRead("GetSystemStats", { server: id }, { refetchInterval: 10_000 }).data;
|
||||
const useStats = (id: string) => {
|
||||
const isServerAvailable = useIsServerAvailable(id);
|
||||
return useRead("GetSystemStats", { server: id }, {
|
||||
enabled: isServerAvailable,
|
||||
refetchInterval: 10_000
|
||||
}).data;
|
||||
};
|
||||
|
||||
const useServerThresholds = (id: string) => {
|
||||
const config = useRead("GetServer", { server: id }).data?.config as any;
|
||||
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,
|
||||
|
||||
@@ -3,13 +3,19 @@ import { useRead } from "@lib/hooks";
|
||||
import { Types } from "komodo_client";
|
||||
import { useMemo } from "react";
|
||||
import { useStatsGranularity } from "./hooks";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { Loader2, OctagonAlert } 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"
|
||||
| "Load Average";
|
||||
|
||||
type StatDatapoint = { date: number; value: number };
|
||||
|
||||
@@ -35,15 +41,15 @@ export const StatChart = ({
|
||||
if (type === "Load Average") {
|
||||
const one = records.map((s) => ({
|
||||
date: convertTsMsToLocalUnixTsInMs(s.ts),
|
||||
value: (s.load_average?.one ?? 0),
|
||||
value: s.load_average?.one ?? 0,
|
||||
}));
|
||||
const five = records.map((s) => ({
|
||||
date: convertTsMsToLocalUnixTsInMs(s.ts),
|
||||
value: (s.load_average?.five ?? 0),
|
||||
value: s.load_average?.five ?? 0,
|
||||
}));
|
||||
const fifteen = records.map((s) => ({
|
||||
date: convertTsMsToLocalUnixTsInMs(s.ts),
|
||||
value: (s.load_average?.fifteen ?? 0),
|
||||
value: s.load_average?.fifteen ?? 0,
|
||||
}));
|
||||
return [
|
||||
{ label: "1m", data: one },
|
||||
@@ -65,9 +71,13 @@ export const StatChart = ({
|
||||
<div className="w-full max-w-full h-full flex items-center justify-center">
|
||||
<Loader2 className="w-8 h-8 animate-spin" />
|
||||
</div>
|
||||
) : seriesData.length > 0 ? (
|
||||
<InnerStatChart type={type} stats={seriesData.flatMap((s) => s.data)} seriesData={seriesData} />
|
||||
) : null}
|
||||
) : (
|
||||
<InnerStatChart
|
||||
type={type}
|
||||
stats={seriesData.flatMap((s) => s.data)}
|
||||
seriesData={seriesData}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -85,13 +95,7 @@ export const InnerStatChart = ({
|
||||
stats: StatDatapoint[] | undefined;
|
||||
seriesData?: { label: string; data: StatDatapoint[] }[];
|
||||
}) => {
|
||||
const { theme: _theme } = useTheme();
|
||||
const theme =
|
||||
_theme === "system"
|
||||
? window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light"
|
||||
: _theme;
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
const min = stats?.[0]?.date ?? 0;
|
||||
const max = stats?.[stats.length - 1]?.date ?? 0;
|
||||
@@ -113,10 +117,12 @@ 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 allValues = (seriesData ?? [{ data: stats ?? [] }]).flatMap((s) =>
|
||||
s.data.map((d) => d.value)
|
||||
);
|
||||
const maxStatValue = Math.max(...(allValues.length ? allValues : [0]));
|
||||
|
||||
const { unit, maxUnitValue } = useMemo(() => {
|
||||
@@ -133,7 +139,10 @@ export const InnerStatChart = ({
|
||||
}
|
||||
if (type === "Load Average") {
|
||||
// Leave unitless; set max slightly above observed
|
||||
return { unit: "", maxUnitValue: maxStatValue === 0 ? 1 : maxStatValue * 1.2 };
|
||||
return {
|
||||
unit: "",
|
||||
maxUnitValue: maxStatValue === 0 ? 1 : maxStatValue * 1.2,
|
||||
};
|
||||
}
|
||||
return { unit: "", maxUnitValue: 100 }; // Default for CPU, memory, disk
|
||||
}, [type, maxStatValue]);
|
||||
@@ -161,6 +170,16 @@ 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={{
|
||||
@@ -175,7 +194,7 @@ export const InnerStatChart = ({
|
||||
hex_color_by_intention("Unknown"),
|
||||
]
|
||||
: [getColor(type)],
|
||||
dark: theme === "dark",
|
||||
dark: currentTheme === "dark",
|
||||
padding: {
|
||||
left: 10,
|
||||
right: 10,
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
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";
|
||||
@@ -24,6 +18,7 @@ import {
|
||||
} from "@ui/select";
|
||||
import { DockerResourceLink, ShowHideButton } from "@components/util";
|
||||
import { filterBySplit } from "@lib/utils";
|
||||
import { useIsServerAvailable } from ".";
|
||||
|
||||
export const ServerStats = ({
|
||||
id,
|
||||
@@ -35,17 +30,23 @@ export const ServerStats = ({
|
||||
const [interval, setInterval] = useStatsGranularity();
|
||||
|
||||
const { specific } = usePermissions({ type: "Server", id });
|
||||
const isServerAvailable = useIsServerAvailable(id);
|
||||
|
||||
const stats = useRead(
|
||||
"GetSystemStats",
|
||||
{ server: id },
|
||||
{ refetchInterval: 10_000 }
|
||||
{
|
||||
enabled: isServerAvailable,
|
||||
refetchInterval: 10_000
|
||||
}
|
||||
).data;
|
||||
const info = useRead("GetSystemInformation", { server: id }).data;
|
||||
const info = useRead("GetSystemInformation", { server: id }, { enabled: isServerAvailable }).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",
|
||||
@@ -336,6 +337,11 @@ 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}
|
||||
@@ -347,11 +353,6 @@ export const ServerStats = ({
|
||||
type="Disk"
|
||||
className="w-full h-[250px]"
|
||||
/>
|
||||
<StatChart
|
||||
server_id={id}
|
||||
type="Load Average"
|
||||
className="w-full h-[250px]"
|
||||
/>
|
||||
<StatChart
|
||||
server_id={id}
|
||||
type="Network Ingress"
|
||||
@@ -501,16 +502,28 @@ const CPU = ({ stats }: { stats: Types.SystemStats | undefined }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const LOAD_AVERAGE = ({ id, stats }: { id: string; 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 cores = useRead("GetSystemInformation", { server: id }).data?.core_count;
|
||||
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 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 p <= 50
|
||||
? "text-green-600"
|
||||
: p <= 80
|
||||
? "text-yellow-600"
|
||||
: "text-red-600";
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -524,15 +537,18 @@ const LOAD_AVERAGE = ({ id, stats }: { id: string; stats: Types.SystemStats | un
|
||||
{/* 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-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"}
|
||||
{cores && cores > 0
|
||||
? `${(pct(one) ?? 0).toFixed(0)}% of ${cores} cores`
|
||||
: "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={pct(one) ?? 0}
|
||||
className="h-2"
|
||||
/>
|
||||
<Progress value={pct(one) ?? 0} className="h-2" />
|
||||
</div>
|
||||
|
||||
{/* Time Intervals */}
|
||||
@@ -546,14 +562,13 @@ const LOAD_AVERAGE = ({ id, stats }: { id: string; stats: Types.SystemStats | un
|
||||
<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)}`}>
|
||||
<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"
|
||||
/>
|
||||
<Progress value={pct(value as number) ?? 0} className="h-1" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -879,6 +879,7 @@ export const StackConfig = ({
|
||||
value={
|
||||
show_default ? DEFAULT_STACK_FILE_CONTENTS : file_contents
|
||||
}
|
||||
filename="compose.yaml"
|
||||
onValueChange={(file_contents) => set({ file_contents })}
|
||||
language="yaml"
|
||||
readOnly={disabled}
|
||||
@@ -1004,7 +1005,7 @@ const ConfigFiles = ({
|
||||
/>
|
||||
|
||||
<RequiresSelector
|
||||
requires={requires ?? Types.StackFileRequires.Redeploy}
|
||||
requires={requires ?? Types.StackFileRequires.None}
|
||||
set={(requires) => {
|
||||
values[i] = { ...values[i], requires };
|
||||
set({ config_files: [...values] });
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user