mirror of
https://github.com/moghtech/komodo.git
synced 2026-11-02 00:02:23 -06:00
Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c99958cba | ||
|
|
7288f067c5 | ||
|
|
5d0f7de9fb | ||
|
|
4129abcfd0 | ||
|
|
4275e903eb | ||
|
|
c57c321cbb | ||
|
|
9a7d49b35e | ||
|
|
d6aaef24cd | ||
|
|
3dce84c1f5 | ||
|
|
8b16f97caf | ||
|
|
c98a809db4 | ||
|
|
c066b4fe16 | ||
|
|
b54fa235bc | ||
|
|
05fd1aba2c | ||
|
|
1a5290e124 | ||
|
|
538e11ef3b | ||
|
|
9208e9bf86 | ||
|
|
31640e3df8 | ||
|
|
42751d1b90 | ||
|
|
673ab5c17d | ||
|
|
ad00f38758 | ||
|
|
b2459b5a64 | ||
|
|
5923dd0e22 | ||
|
|
51294edd73 |
Generated
+23
-21
@@ -1131,7 +1131,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "command"
|
||||
version = "2.0.0-dev-111"
|
||||
version = "2.0.0-dev-116"
|
||||
dependencies = [
|
||||
"komodo_client",
|
||||
"shlex",
|
||||
@@ -1462,7 +1462,7 @@ checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
|
||||
|
||||
[[package]]
|
||||
name = "database"
|
||||
version = "2.0.0-dev-111"
|
||||
version = "2.0.0-dev-116"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-compression",
|
||||
@@ -1732,7 +1732,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "encoding"
|
||||
version = "2.0.0-dev-111"
|
||||
version = "2.0.0-dev-116"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -1774,7 +1774,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "environment"
|
||||
version = "2.0.0-dev-111"
|
||||
version = "2.0.0-dev-116"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"formatting",
|
||||
@@ -1903,7 +1903,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "formatting"
|
||||
version = "2.0.0-dev-111"
|
||||
version = "2.0.0-dev-116"
|
||||
dependencies = [
|
||||
"mogh_error",
|
||||
]
|
||||
@@ -2069,7 +2069,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "git"
|
||||
version = "2.0.0-dev-111"
|
||||
version = "2.0.0-dev-116"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"command",
|
||||
@@ -2663,7 +2663,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "interpolate"
|
||||
version = "2.0.0-dev-111"
|
||||
version = "2.0.0-dev-116"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"komodo_client",
|
||||
@@ -2772,9 +2772,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "jsonwebtoken"
|
||||
version = "10.2.0"
|
||||
version = "10.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c76e1c7d7df3e34443b3621b459b066a7b79644f059fc8b2db7070c825fd417e"
|
||||
checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"base64 0.22.1",
|
||||
@@ -2789,7 +2789,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_cli"
|
||||
version = "2.0.0-dev-111"
|
||||
version = "2.0.0-dev-116"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -2817,7 +2817,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_client"
|
||||
version = "2.0.0-dev-111"
|
||||
version = "2.0.0-dev-116"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -2856,7 +2856,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_core"
|
||||
version = "2.0.0-dev-111"
|
||||
version = "2.0.0-dev-116"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -2929,7 +2929,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_periphery"
|
||||
version = "2.0.0-dev-111"
|
||||
version = "2.0.0-dev-116"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -3173,9 +3173,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mogh_auth_server"
|
||||
version = "1.2.4"
|
||||
version = "1.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2eeb8c898da571c59ec9f4997b6869c5a3b31303d95c765ee50713cb4c045be9"
|
||||
checksum = "fa05efbe7caf986807ba12d9dcafb722a7c0f2fb51f6dafd87fa2529122a3b87"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -3341,13 +3341,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mogh_server"
|
||||
version = "1.1.2"
|
||||
version = "1.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c6c3aa20123c38b4f2cb63f42099352f9f4003ff72e744f26f4cd72023c4274"
|
||||
checksum = "9601ac6b48b8a932d5944aa68944b03147b79187dd046d78dcc077d5c64089c2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"axum-server",
|
||||
"data-encoding",
|
||||
"sha2",
|
||||
"tower-http",
|
||||
"tower-sessions",
|
||||
"tracing",
|
||||
@@ -3977,7 +3979,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "periphery_client"
|
||||
version = "2.0.0-dev-111"
|
||||
version = "2.0.0-dev-116"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"encoding",
|
||||
@@ -5881,7 +5883,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "transport"
|
||||
version = "2.0.0-dev-111"
|
||||
version = "2.0.0-dev-116"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -6133,9 +6135,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.19.0"
|
||||
version = "1.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a"
|
||||
checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"js-sys",
|
||||
|
||||
+3
-6
@@ -8,7 +8,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "2.0.0-dev-111"
|
||||
version = "2.0.0-dev-116"
|
||||
edition = "2024"
|
||||
authors = ["mbecker20 <becker.maxh@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
@@ -18,9 +18,6 @@ homepage = "https://komo.do"
|
||||
[profile.release]
|
||||
strip = "debuginfo"
|
||||
|
||||
# [patch.crates-io]
|
||||
# mogh_logger = { path = "../lib/crates/logger" }
|
||||
|
||||
[workspace.dependencies]
|
||||
# LOCAL
|
||||
komodo_client = { path = "client/core/rs" }
|
||||
@@ -40,7 +37,7 @@ mogh_error = { version = "1.0.3", default-features = false }
|
||||
derive_default_builder = "0.1.8"
|
||||
async_timing_util = "1.1.0"
|
||||
mogh_auth_client = "1.2.1"
|
||||
mogh_auth_server = "1.2.4"
|
||||
mogh_auth_server = "1.2.5"
|
||||
mogh_secret_file = "1.0.0"
|
||||
mogh_validations = "1.0.0"
|
||||
mogh_rate_limit = "1.0.0"
|
||||
@@ -49,7 +46,7 @@ mongo_indexed = "2.0.2"
|
||||
mogh_resolver = "1.0.0"
|
||||
mogh_config = "1.0.1"
|
||||
mogh_logger = "1.3.0"
|
||||
mogh_server = "1.1.2"
|
||||
mogh_server = "1.4.3"
|
||||
toml_pretty = "2.0.0"
|
||||
mogh_cache = "1.1.1"
|
||||
mogh_pki = "1.1.0"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
## Builds the Komodo Core, Periphery, and Util binaries
|
||||
## for a specific architecture. Requires OpenSSL 3 or later.
|
||||
|
||||
FROM rust:1.91.1-bookworm AS builder
|
||||
FROM rust:1.93.0-bookworm AS builder
|
||||
RUN cargo install cargo-strip
|
||||
|
||||
WORKDIR /builder
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM rust:1.91.1-bullseye AS builder
|
||||
FROM rust:1.93.0-bullseye AS builder
|
||||
RUN cargo install cargo-strip
|
||||
|
||||
WORKDIR /builder
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
## All in one, multi stage compile + runtime Docker build for your architecture.
|
||||
|
||||
# Build Core
|
||||
FROM rust:1.91.1-trixie AS core-builder
|
||||
FROM rust:1.93.0-trixie AS core-builder
|
||||
RUN cargo install cargo-strip
|
||||
|
||||
WORKDIR /builder
|
||||
@@ -18,7 +18,7 @@ RUN cargo build -p komodo_core --release && \
|
||||
cargo strip
|
||||
|
||||
# Build Frontend
|
||||
FROM node:20.12-alpine AS frontend-builder
|
||||
FROM node:22.12-alpine AS frontend-builder
|
||||
WORKDIR /builder
|
||||
COPY ./frontend ./frontend
|
||||
COPY ./client/core/ts ./client
|
||||
|
||||
@@ -7,7 +7,7 @@ ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest
|
||||
FROM ${BINARIES_IMAGE} AS binaries
|
||||
|
||||
# Build Frontend
|
||||
FROM node:20.12-alpine AS frontend-builder
|
||||
FROM node:22.12-alpine AS frontend-builder
|
||||
WORKDIR /builder
|
||||
COPY ./frontend ./frontend
|
||||
COPY ./client/core/ts ./client
|
||||
|
||||
@@ -325,6 +325,7 @@ impl Resolve<ExecuteArgs> for GlobalAutoUpdate {
|
||||
}
|
||||
|
||||
let name = deployment.name.clone();
|
||||
|
||||
if let Err(e) = check_deployment_for_update_inner(
|
||||
deployment,
|
||||
&swarm_or_server,
|
||||
|
||||
@@ -38,7 +38,10 @@ pub fn app() -> Router {
|
||||
.nest("/ws", ws::router())
|
||||
.nest("/client", ts_client::router())
|
||||
.layer(memory_session_layer(config))
|
||||
.fallback_service(serve_static_ui(&config.frontend_path))
|
||||
.fallback_service(serve_static_ui(
|
||||
&config.frontend_path,
|
||||
config.ui_index_force_no_cache,
|
||||
))
|
||||
.layer(cors_layer(config))
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,11 @@ use std::sync::OnceLock;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use database::mungos::{by_id::update_one_by_id, mongodb::bson::doc};
|
||||
use futures_util::{StreamExt as _, stream::FuturesOrdered};
|
||||
use komodo_client::{
|
||||
api::{execute::Deploy, write::*},
|
||||
entities::{
|
||||
NoData, Operation, ResourceTarget, SwarmOrServer,
|
||||
Operation, ResourceTarget, SwarmOrServer,
|
||||
alert::{Alert, AlertData, SeverityLevel},
|
||||
deployment::{
|
||||
Deployment, DeploymentImage, DeploymentInfo, DeploymentState,
|
||||
@@ -26,15 +27,18 @@ use periphery_client::api::{self, container::InspectContainer};
|
||||
|
||||
use crate::{
|
||||
alert::send_alerts,
|
||||
api::execute::{self, ExecuteRequest},
|
||||
api::execute::{self, ExecuteRequest, ExecutionResult},
|
||||
helpers::{
|
||||
periphery_client,
|
||||
query::get_deployment_state,
|
||||
query::{get_deployment_state, get_swarm_or_server},
|
||||
registry_token,
|
||||
update::{add_update, make_update},
|
||||
update::{add_update, make_update, poll_update_until_complete},
|
||||
},
|
||||
permission::get_check_permissions,
|
||||
resource::{self, setup_deployment_execution},
|
||||
resource::{
|
||||
self, list_full_for_user_using_pattern,
|
||||
setup_deployment_execution,
|
||||
},
|
||||
state::{
|
||||
action_states, db_client, deployment_status_cache,
|
||||
image_digest_cache, server_status_cache,
|
||||
@@ -359,12 +363,14 @@ impl Resolve<WriteArgs> for CheckDeploymentForUpdate {
|
||||
fields(
|
||||
operator = user.id,
|
||||
deployment = self.deployment,
|
||||
skip_auto_update = self.skip_auto_update,
|
||||
wait_for_auto_update = self.wait_for_auto_update,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> mogh_error::Result<NoData> {
|
||||
) -> mogh_error::Result<Self::Response> {
|
||||
// Even though this is a write request, this doesn't change any config. Anyone that can execute the
|
||||
// deployment should be able to do this.
|
||||
let (deployment, swarm_or_server) = setup_deployment_execution(
|
||||
@@ -382,9 +388,8 @@ impl Resolve<WriteArgs> for CheckDeploymentForUpdate {
|
||||
self.skip_auto_update,
|
||||
self.wait_for_auto_update,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(NoData {})
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,6 +414,8 @@ fn deployment_alert_sent_cache() -> &'static SetCache<String> {
|
||||
skip_all,
|
||||
fields(
|
||||
deployment = deployment.id,
|
||||
skip_auto_update,
|
||||
wait_for_auto_update,
|
||||
)
|
||||
)]
|
||||
pub async fn check_deployment_for_update_inner(
|
||||
@@ -417,11 +424,18 @@ pub async fn check_deployment_for_update_inner(
|
||||
skip_auto_update: bool,
|
||||
// Otherwise spawns task to run in background
|
||||
wait_for_auto_update: bool,
|
||||
) -> anyhow::Result<bool> {
|
||||
) -> anyhow::Result<CheckDeploymentForUpdateResponse> {
|
||||
let alert_cache = deployment_alert_sent_cache();
|
||||
|
||||
let (image, account, token) = match deployment.config.image {
|
||||
let (image, account, token) = match &deployment.config.image {
|
||||
DeploymentImage::Image { image } => {
|
||||
if image.contains('@') {
|
||||
// Images with a hardcoded digest can't have update.
|
||||
return Ok(CheckDeploymentForUpdateResponse {
|
||||
deployment: deployment.id,
|
||||
update_available: false,
|
||||
});
|
||||
}
|
||||
let domain = extract_registry_domain(&image)?;
|
||||
let account =
|
||||
optional_string(&deployment.config.image_registry_account);
|
||||
@@ -436,7 +450,10 @@ pub async fn check_deployment_for_update_inner(
|
||||
alert_cache.remove(&deployment.id).await;
|
||||
// This method not used for build based deployments
|
||||
// as deployed version vs built version can be inferred from Updates.
|
||||
return Ok(false);
|
||||
return Ok(CheckDeploymentForUpdateResponse {
|
||||
deployment: deployment.id,
|
||||
update_available: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -452,39 +469,50 @@ pub async fn check_deployment_for_update_inner(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let Some((state, Some(current_digest))) = deployment_status_cache()
|
||||
.get(&deployment.id)
|
||||
.await
|
||||
.map(|s| (s.curr.state, s.curr.image_digest.clone()))
|
||||
let Some((state, Some(current_digests))) =
|
||||
deployment_status_cache()
|
||||
.get(&deployment.id)
|
||||
.await
|
||||
.map(|s| (s.curr.state, s.curr.image_digests.clone()))
|
||||
else {
|
||||
alert_cache.remove(&deployment.id).await;
|
||||
return Ok(false);
|
||||
return Ok(CheckDeploymentForUpdateResponse {
|
||||
deployment: deployment.id,
|
||||
update_available: false,
|
||||
});
|
||||
};
|
||||
|
||||
// If not running or latest digest matches current, early return
|
||||
if !matches!(state, DeploymentState::Running)
|
||||
|| !latest_digest.update_available(¤t_digest)
|
||||
|| !latest_digest.update_available(¤t_digests)
|
||||
{
|
||||
alert_cache.remove(&deployment.id).await;
|
||||
return Ok(false);
|
||||
return Ok(CheckDeploymentForUpdateResponse {
|
||||
deployment: deployment.id,
|
||||
update_available: false,
|
||||
});
|
||||
}
|
||||
|
||||
if !skip_auto_update && deployment.config.auto_update {
|
||||
// Trigger deploy + alert
|
||||
let swarm_id = swarm_or_server.swarm_id().map(str::to_string);
|
||||
let swarm_name = swarm_or_server.swarm_name().map(str::to_string);
|
||||
let server_id = swarm_or_server.server_id().map(str::to_string);
|
||||
let server_name =
|
||||
swarm_or_server.server_name().map(str::to_string);
|
||||
|
||||
// Conservatively remove from alert cache so 'skip_auto_update'
|
||||
// doesn't cause alerts not to be sent on subsequent calls.
|
||||
alert_cache.remove(&deployment.id).await;
|
||||
|
||||
let swarm_id = swarm_or_server.swarm_id().map(str::to_string);
|
||||
let swarm_name = swarm_or_server.swarm_name().map(str::to_string);
|
||||
let server_id = swarm_or_server.server_id().map(str::to_string);
|
||||
let server_name =
|
||||
swarm_or_server.server_name().map(str::to_string);
|
||||
let id = deployment.id.clone();
|
||||
let name = deployment.name.clone();
|
||||
let image = image.clone();
|
||||
|
||||
let run = async move {
|
||||
match execute::inner_handler(
|
||||
ExecuteRequest::Deploy(Deploy {
|
||||
deployment: deployment.name.clone(),
|
||||
deployment: name.clone(),
|
||||
stop_signal: None,
|
||||
stop_time: None,
|
||||
}),
|
||||
@@ -492,38 +520,45 @@ pub async fn check_deployment_for_update_inner(
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
let ts = komodo_timestamp();
|
||||
let alert = Alert {
|
||||
id: Default::default(),
|
||||
ts,
|
||||
resolved: true,
|
||||
resolved_ts: ts.into(),
|
||||
level: SeverityLevel::Ok,
|
||||
target: ResourceTarget::Deployment(deployment.id.clone()),
|
||||
data: AlertData::DeploymentAutoUpdated {
|
||||
id: deployment.id.clone(),
|
||||
name: deployment.name.clone(),
|
||||
swarm_id,
|
||||
swarm_name,
|
||||
server_id,
|
||||
server_name,
|
||||
image: image.clone(),
|
||||
},
|
||||
Ok(res) => {
|
||||
let ExecutionResult::Single(update) = res else {
|
||||
unreachable!()
|
||||
};
|
||||
let res = db_client().alerts.insert_one(&alert).await;
|
||||
if let Err(e) = res {
|
||||
error!(
|
||||
"Failed to record DeploymentAutoUpdated to db | {e:#}"
|
||||
);
|
||||
let Ok(update) =
|
||||
poll_update_until_complete(&update.id).await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if update.success {
|
||||
let ts = komodo_timestamp();
|
||||
let alert = Alert {
|
||||
id: Default::default(),
|
||||
ts,
|
||||
resolved: true,
|
||||
resolved_ts: ts.into(),
|
||||
level: SeverityLevel::Ok,
|
||||
target: ResourceTarget::Deployment(id.clone()),
|
||||
data: AlertData::DeploymentAutoUpdated {
|
||||
id,
|
||||
name,
|
||||
swarm_id,
|
||||
swarm_name,
|
||||
server_id,
|
||||
server_name,
|
||||
image,
|
||||
},
|
||||
};
|
||||
let res = db_client().alerts.insert_one(&alert).await;
|
||||
if let Err(e) = res {
|
||||
error!(
|
||||
"Failed to record DeploymentAutoUpdated to db | {e:#}"
|
||||
);
|
||||
}
|
||||
send_alerts(&[alert]).await;
|
||||
}
|
||||
send_alerts(&[alert]).await;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Failed to auto update Deployment {} | {e:#}",
|
||||
deployment.name
|
||||
)
|
||||
warn!("Failed to auto update Deployment {name} | {e:#}",)
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -536,7 +571,10 @@ pub async fn check_deployment_for_update_inner(
|
||||
} else {
|
||||
// Avoid spamming alerts
|
||||
if alert_cache.contains(&deployment.id).await {
|
||||
return Ok(true);
|
||||
return Ok(CheckDeploymentForUpdateResponse {
|
||||
deployment: deployment.id,
|
||||
update_available: true,
|
||||
});
|
||||
}
|
||||
alert_cache.insert(deployment.id.clone()).await;
|
||||
let ts = komodo_timestamp();
|
||||
@@ -568,5 +606,69 @@ pub async fn check_deployment_for_update_inner(
|
||||
send_alerts(&[alert]).await;
|
||||
}
|
||||
|
||||
Ok(!deployment.config.auto_update)
|
||||
Ok(CheckDeploymentForUpdateResponse {
|
||||
deployment: deployment.id,
|
||||
update_available: !deployment.config.auto_update,
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<WriteArgs> for BatchCheckDeploymentForUpdate {
|
||||
#[instrument(
|
||||
"BatchCheckDeploymentForUpdate",
|
||||
skip_all,
|
||||
fields(
|
||||
operator = user.id,
|
||||
pattern = self.pattern,
|
||||
skip_auto_update = self.skip_auto_update,
|
||||
wait_for_auto_update = self.wait_for_auto_update,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let deployments = list_full_for_user_using_pattern::<Deployment>(
|
||||
&self.pattern,
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let res = deployments
|
||||
.into_iter()
|
||||
.map(|deployment| async move {
|
||||
let swarm_or_server = get_swarm_or_server(
|
||||
&deployment.config.swarm_id,
|
||||
&deployment.config.server_id,
|
||||
)
|
||||
.await?;
|
||||
swarm_or_server.verify_has_target().map_err(|e| e.error)?;
|
||||
check_deployment_for_update_inner(
|
||||
deployment,
|
||||
&swarm_or_server,
|
||||
self.skip_auto_update,
|
||||
self.wait_for_auto_update,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.collect::<FuturesOrdered<_>>()
|
||||
.collect::<Vec<_>>()
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|res| {
|
||||
res
|
||||
.inspect_err(|e| {
|
||||
warn!(
|
||||
"Failed to check deployment for update in batch run | {e:#}"
|
||||
)
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.collect();
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ pub enum WriteRequest {
|
||||
WriteStackFileContents(WriteStackFileContents),
|
||||
RefreshStackCache(RefreshStackCache),
|
||||
CheckStackForUpdate(CheckStackForUpdate),
|
||||
BatchCheckStackForUpdate(BatchCheckStackForUpdate),
|
||||
|
||||
// ==== DEPLOYMENT ====
|
||||
CreateDeployment(CreateDeployment),
|
||||
@@ -104,6 +105,7 @@ pub enum WriteRequest {
|
||||
UpdateDeployment(UpdateDeployment),
|
||||
RenameDeployment(RenameDeployment),
|
||||
CheckDeploymentForUpdate(CheckDeploymentForUpdate),
|
||||
BatchCheckDeploymentForUpdate(BatchCheckDeploymentForUpdate),
|
||||
|
||||
// ==== BUILD ====
|
||||
CreateBuild(CreateBuild),
|
||||
|
||||
+143
-48
@@ -9,6 +9,7 @@ use database::{
|
||||
},
|
||||
};
|
||||
use formatting::format_serror;
|
||||
use futures_util::{StreamExt as _, stream::FuturesOrdered};
|
||||
use komodo_client::{
|
||||
api::{execute::DeployStack, write::*},
|
||||
entities::{
|
||||
@@ -32,15 +33,15 @@ use periphery_client::api::compose::{
|
||||
|
||||
use crate::{
|
||||
alert::send_alerts,
|
||||
api::execute::{self, ExecuteRequest},
|
||||
api::execute::{self, ExecuteRequest, ExecutionResult},
|
||||
config::core_config,
|
||||
helpers::{
|
||||
query::get_swarm_or_server,
|
||||
stack_git_token, swarm_or_server_request,
|
||||
update::{add_update, make_update},
|
||||
update::{add_update, make_update, poll_update_until_complete},
|
||||
},
|
||||
permission::get_check_permissions,
|
||||
resource,
|
||||
resource::{self, list_full_for_user_using_pattern},
|
||||
stack::{
|
||||
remote::{RemoteComposeContents, get_repo_compose_contents},
|
||||
services::{
|
||||
@@ -676,7 +677,7 @@ impl Resolve<WriteArgs> for CheckStackForUpdate {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> mogh_error::Result<NoData> {
|
||||
) -> mogh_error::Result<Self::Response> {
|
||||
// Even though this is a write request, this doesn't change any config. Anyone that can execute the
|
||||
// stack should be able to do this.
|
||||
let (stack, swarm_or_server) = setup_stack_execution(
|
||||
@@ -695,9 +696,8 @@ impl Resolve<WriteArgs> for CheckStackForUpdate {
|
||||
self.wait_for_auto_update,
|
||||
self.skip_cache_refresh,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(NoData {})
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -731,7 +731,7 @@ pub async fn check_stack_for_update_inner(
|
||||
// Otherwise spawns task to run in background
|
||||
wait_for_auto_update: bool,
|
||||
skip_cache_refresh: bool,
|
||||
) -> anyhow::Result<Vec<StackServiceWithUpdate>> {
|
||||
) -> anyhow::Result<CheckStackForUpdateResponse> {
|
||||
if !skip_cache_refresh {
|
||||
(RefreshStackCache {
|
||||
stack: stack.clone(),
|
||||
@@ -750,14 +750,28 @@ pub async fn check_stack_for_update_inner(
|
||||
let cache = image_digest_cache();
|
||||
|
||||
for service in &mut stack.info.latest_services {
|
||||
if service.image.is_empty() {
|
||||
// Prefer the image coming from deployed services
|
||||
// so it will be after any interpolation.
|
||||
let image = stack
|
||||
.info
|
||||
.deployed_services
|
||||
.as_ref()
|
||||
.and_then(|services| {
|
||||
services.iter().find_map(|deployed| {
|
||||
(deployed.service_name == service.service_name)
|
||||
.then_some(&deployed.image)
|
||||
})
|
||||
})
|
||||
.unwrap_or(&service.image);
|
||||
|
||||
if image.is_empty() ||
|
||||
// Images with a hardcoded digest can't have update.
|
||||
image.contains('@')
|
||||
{
|
||||
service.image_digest = None;
|
||||
continue;
|
||||
}
|
||||
match cache
|
||||
.get(&swarm_or_server, &service.image, None, None)
|
||||
.await
|
||||
{
|
||||
match cache.get(&swarm_or_server, image, None, None).await {
|
||||
Ok(digest) => service.image_digest = Some(digest),
|
||||
Err(e) => {
|
||||
warn!(
|
||||
@@ -787,8 +801,8 @@ pub async fn check_stack_for_update_inner(
|
||||
alert_cache
|
||||
.retain(|(stack_id, _)| stack_id != &stack.id)
|
||||
.await;
|
||||
return Ok(
|
||||
extract_services_from_stack(&stack)
|
||||
return Ok(CheckStackForUpdateResponse {
|
||||
services: extract_services_from_stack(&stack)
|
||||
.into_iter()
|
||||
.map(|service| StackServiceWithUpdate {
|
||||
service: service.service_name,
|
||||
@@ -796,15 +810,17 @@ pub async fn check_stack_for_update_inner(
|
||||
update_available: false,
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
stack: stack.id,
|
||||
});
|
||||
};
|
||||
|
||||
let StackState::Running = status.curr.state else {
|
||||
alert_cache
|
||||
.retain(|(stack_id, _)| stack_id != &stack.id)
|
||||
.await;
|
||||
return Ok(
|
||||
status
|
||||
return Ok(CheckStackForUpdateResponse {
|
||||
stack: stack.id,
|
||||
services: status
|
||||
.curr
|
||||
.services
|
||||
.iter()
|
||||
@@ -814,7 +830,7 @@ pub async fn check_stack_for_update_inner(
|
||||
update_available: false,
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
let mut services = Vec::new();
|
||||
@@ -826,7 +842,7 @@ pub async fn check_stack_for_update_inner(
|
||||
update_available: false,
|
||||
};
|
||||
|
||||
let Some(current_digest) = &service.image_digest else {
|
||||
let Some(current_digests) = &service.image_digests else {
|
||||
services.push(service_with_update);
|
||||
continue;
|
||||
};
|
||||
@@ -845,7 +861,7 @@ pub async fn check_stack_for_update_inner(
|
||||
};
|
||||
|
||||
service_with_update.update_available =
|
||||
latest_digest.update_available(¤t_digest.0);
|
||||
latest_digest.update_available(¤t_digests);
|
||||
|
||||
if service_with_update.update_available
|
||||
&& (skip_auto_update || !stack.config.auto_update)
|
||||
@@ -902,7 +918,10 @@ pub async fn check_stack_for_update_inner(
|
||||
|| !stack.config.auto_update
|
||||
|| services_with_update.is_empty()
|
||||
{
|
||||
return Ok(services);
|
||||
return Ok(CheckStackForUpdateResponse {
|
||||
stack: stack.id,
|
||||
services,
|
||||
});
|
||||
}
|
||||
|
||||
// Conservatively remove from alert cache so 'skip_auto_update'
|
||||
@@ -925,6 +944,8 @@ pub async fn check_stack_for_update_inner(
|
||||
let server_id = swarm_or_server.server_id().map(str::to_string);
|
||||
let server_name = swarm_or_server.server_name().map(str::to_string);
|
||||
|
||||
let stack_id = stack.id.clone();
|
||||
|
||||
let run = async move {
|
||||
match execute::inner_handler(
|
||||
ExecuteRequest::DeployStack(DeployStack {
|
||||
@@ -936,33 +957,42 @@ pub async fn check_stack_for_update_inner(
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
let ts = komodo_timestamp();
|
||||
let alert = Alert {
|
||||
id: Default::default(),
|
||||
ts,
|
||||
resolved: true,
|
||||
resolved_ts: ts.into(),
|
||||
level: SeverityLevel::Ok,
|
||||
target: ResourceTarget::Stack(stack.id.clone()),
|
||||
data: AlertData::StackAutoUpdated {
|
||||
id: stack.id.clone(),
|
||||
name: stack.name.clone(),
|
||||
swarm_id,
|
||||
swarm_name,
|
||||
server_id,
|
||||
server_name,
|
||||
images: services_with_update
|
||||
.iter()
|
||||
.map(|service| service.image.clone())
|
||||
.collect(),
|
||||
},
|
||||
Ok(res) => {
|
||||
let ExecutionResult::Single(update) = res else {
|
||||
unreachable!()
|
||||
};
|
||||
let res = db_client().alerts.insert_one(&alert).await;
|
||||
if let Err(e) = res {
|
||||
error!("Failed to record StackAutoUpdated to db | {e:#}");
|
||||
let Ok(update) = poll_update_until_complete(&update.id).await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if update.success {
|
||||
let ts = komodo_timestamp();
|
||||
let alert = Alert {
|
||||
id: Default::default(),
|
||||
ts,
|
||||
resolved: true,
|
||||
resolved_ts: ts.into(),
|
||||
level: SeverityLevel::Ok,
|
||||
target: ResourceTarget::Stack(stack.id.clone()),
|
||||
data: AlertData::StackAutoUpdated {
|
||||
id: stack.id.clone(),
|
||||
name: stack.name.clone(),
|
||||
swarm_id,
|
||||
swarm_name,
|
||||
server_id,
|
||||
server_name,
|
||||
images: services_with_update
|
||||
.iter()
|
||||
.map(|service| service.image.clone())
|
||||
.collect(),
|
||||
},
|
||||
};
|
||||
let res = db_client().alerts.insert_one(&alert).await;
|
||||
if let Err(e) = res {
|
||||
error!("Failed to record StackAutoUpdated to db | {e:#}");
|
||||
}
|
||||
send_alerts(&[alert]).await;
|
||||
}
|
||||
send_alerts(&[alert]).await;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to auto update Stack {} | {e:#}", stack.name)
|
||||
@@ -976,5 +1006,70 @@ pub async fn check_stack_for_update_inner(
|
||||
tokio::spawn(run);
|
||||
}
|
||||
|
||||
Ok(services)
|
||||
Ok(CheckStackForUpdateResponse {
|
||||
stack: stack_id,
|
||||
services,
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<WriteArgs> for BatchCheckStackForUpdate {
|
||||
#[instrument(
|
||||
"BatchCheckStackForUpdate",
|
||||
skip_all,
|
||||
fields(
|
||||
operator = user.id,
|
||||
pattern = self.pattern,
|
||||
skip_auto_update = self.skip_auto_update,
|
||||
wait_for_auto_update = self.wait_for_auto_update,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let stacks = list_full_for_user_using_pattern::<Stack>(
|
||||
&self.pattern,
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let res = stacks
|
||||
.into_iter()
|
||||
.map(|stack| async move {
|
||||
let swarm_or_server = get_swarm_or_server(
|
||||
&stack.config.swarm_id,
|
||||
&stack.config.server_id,
|
||||
)
|
||||
.await?;
|
||||
swarm_or_server.verify_has_target().map_err(|e| e.error)?;
|
||||
check_stack_for_update_inner(
|
||||
stack.id,
|
||||
&swarm_or_server,
|
||||
self.skip_auto_update,
|
||||
self.wait_for_auto_update,
|
||||
self.skip_cache_refresh,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.collect::<FuturesOrdered<_>>()
|
||||
.collect::<Vec<_>>()
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|res| {
|
||||
res
|
||||
.inspect_err(|e| {
|
||||
warn!(
|
||||
"Failed to check stack for update in batch run | {e:#}"
|
||||
)
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.collect();
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,9 +143,7 @@ impl AuthImpl for KomodoAuthImpl {
|
||||
}
|
||||
|
||||
fn host(&self) -> &str {
|
||||
static AUTH_HOST: LazyLock<String> =
|
||||
LazyLock::new(|| format!("{}/auth", core_config().host));
|
||||
&AUTH_HOST
|
||||
&core_config().host
|
||||
}
|
||||
|
||||
fn post_link_redirect(&self) -> &str {
|
||||
|
||||
+18
-12
@@ -293,9 +293,6 @@ pub fn core_config() -> &'static CoreConfig {
|
||||
first_server_name: env
|
||||
.komodo_first_server_name
|
||||
.or(config.first_server_name),
|
||||
frontend_path: env
|
||||
.komodo_frontend_path
|
||||
.unwrap_or(config.frontend_path),
|
||||
jwt_ttl: env.komodo_jwt_ttl.unwrap_or(config.jwt_ttl),
|
||||
auth_rate_limit_disabled: env
|
||||
.komodo_auth_rate_limit_disabled
|
||||
@@ -312,15 +309,9 @@ pub fn core_config() -> &'static CoreConfig {
|
||||
cors_allow_credentials: env
|
||||
.komodo_cors_allow_credentials
|
||||
.unwrap_or(config.cors_allow_credentials),
|
||||
sync_directory: env
|
||||
.komodo_sync_directory
|
||||
.unwrap_or(config.sync_directory),
|
||||
repo_directory: env
|
||||
.komodo_repo_directory
|
||||
.unwrap_or(config.repo_directory),
|
||||
action_directory: env
|
||||
.komodo_action_directory
|
||||
.unwrap_or(config.action_directory),
|
||||
session_allow_cross_site: env
|
||||
.komodo_session_allow_cross_site
|
||||
.unwrap_or(config.session_allow_cross_site),
|
||||
resource_poll_interval: env
|
||||
.komodo_resource_poll_interval
|
||||
.unwrap_or(config.resource_poll_interval),
|
||||
@@ -412,6 +403,21 @@ pub fn core_config() -> &'static CoreConfig {
|
||||
ssl_cert_file: env
|
||||
.komodo_ssl_cert_file
|
||||
.unwrap_or(config.ssl_cert_file),
|
||||
frontend_path: env
|
||||
.komodo_frontend_path
|
||||
.unwrap_or(config.frontend_path),
|
||||
ui_index_force_no_cache: env
|
||||
.komodo_ui_index_force_no_cache
|
||||
.unwrap_or(config.ui_index_force_no_cache),
|
||||
sync_directory: env
|
||||
.komodo_sync_directory
|
||||
.unwrap_or(config.sync_directory),
|
||||
repo_directory: env
|
||||
.komodo_repo_directory
|
||||
.unwrap_or(config.repo_directory),
|
||||
action_directory: env
|
||||
.komodo_action_directory
|
||||
.unwrap_or(config.action_directory),
|
||||
|
||||
// These can't be overridden on env
|
||||
secrets: config.secrets,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context;
|
||||
use database::mungos::{
|
||||
by_id::{find_one_by_id, update_one_by_id},
|
||||
@@ -16,7 +18,7 @@ use komodo_client::entities::{
|
||||
stack::Stack,
|
||||
swarm::Swarm,
|
||||
sync::ResourceSync,
|
||||
update::{Update, UpdateListItem},
|
||||
update::{Update, UpdateListItem, UpdateStatus},
|
||||
user::User,
|
||||
};
|
||||
|
||||
@@ -591,3 +593,17 @@ pub async fn init_execution_update(
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
|
||||
pub async fn poll_update_until_complete(
|
||||
update_id: &str,
|
||||
) -> anyhow::Result<Update> {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
let update = find_one_by_id(&db_client().updates, update_id)
|
||||
.await?
|
||||
.context("No update found at given ID")?;
|
||||
if matches!(update.status, UpdateStatus::Complete) {
|
||||
return Ok(update);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ pub async fn insert_deployments_status_unknown(
|
||||
state: DeploymentState::Unknown,
|
||||
container: None,
|
||||
service: None,
|
||||
image_digest: None,
|
||||
image_digests: None,
|
||||
},
|
||||
prev,
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ use komodo_client::entities::{
|
||||
container::ContainerListItem, image::ImageListItem,
|
||||
service::SwarmServiceListItem, stack::SwarmStackListItem,
|
||||
},
|
||||
optional_string,
|
||||
stack::{Stack, StackService, StackServiceNames, StackState},
|
||||
swarm::SwarmState,
|
||||
};
|
||||
@@ -80,7 +79,7 @@ pub async fn update_swarm_stack_cache(
|
||||
image: image.clone(),
|
||||
container: None,
|
||||
swarm_service,
|
||||
image_digest: None,
|
||||
image_digests: Default::default(),
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -131,7 +130,7 @@ pub async fn update_server_stack_cache(
|
||||
}.is_match(&container.name)
|
||||
}).cloned();
|
||||
|
||||
let (image, image_digest) = container
|
||||
let (image, image_digests) = container
|
||||
.as_ref()
|
||||
.and_then(|container| container.image_id.as_ref())
|
||||
.and_then(|image_id| {
|
||||
@@ -141,7 +140,7 @@ pub async fn update_server_stack_cache(
|
||||
})
|
||||
.map(|image| (
|
||||
image.name.clone(),
|
||||
optional_string(&image.digest).map(ImageDigest)
|
||||
Some(ImageDigest::vec(&image.digests)),
|
||||
))
|
||||
.unwrap_or((
|
||||
if image.contains(':') {
|
||||
@@ -157,7 +156,7 @@ pub async fn update_server_stack_cache(
|
||||
image: image.clone(),
|
||||
container,
|
||||
swarm_service: None,
|
||||
image_digest,
|
||||
image_digests,
|
||||
}
|
||||
}).collect::<Vec<_>>();
|
||||
|
||||
@@ -230,7 +229,7 @@ pub async fn update_swarm_deployment_cache(
|
||||
state: current_state,
|
||||
service,
|
||||
container: None,
|
||||
image_digest: None,
|
||||
image_digests: None,
|
||||
},
|
||||
prev: prev_state,
|
||||
}
|
||||
@@ -252,13 +251,13 @@ pub async fn update_server_deployment_cache(
|
||||
.iter()
|
||||
.find(|container| container.name == deployment.name)
|
||||
.cloned();
|
||||
let image_digest = container
|
||||
let image_digests = container
|
||||
.as_ref()
|
||||
.and_then(|container| container.image_id.as_ref())
|
||||
.and_then(|image_id| {
|
||||
images.iter().find_map(|image| {
|
||||
if &image.id == image_id {
|
||||
optional_string(&image.digest)
|
||||
Some(ImageDigest::vec(&image.digests))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -282,7 +281,7 @@ pub async fn update_server_deployment_cache(
|
||||
state: current_state,
|
||||
container,
|
||||
service: None,
|
||||
image_digest,
|
||||
image_digests,
|
||||
},
|
||||
prev: prev_state,
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ impl super::KomodoResource for Deployment {
|
||||
}
|
||||
DeploymentImage::Image { image } => (image, None),
|
||||
};
|
||||
let (image, digest) = status
|
||||
let (image, current_digests) = status
|
||||
.as_ref()
|
||||
.map(|s| {
|
||||
(
|
||||
@@ -138,14 +138,17 @@ impl super::KomodoResource for Deployment {
|
||||
.unwrap_or_else(|| String::from("Unknown"))
|
||||
})
|
||||
}),
|
||||
s.curr.image_digest.as_ref(),
|
||||
s.curr.image_digests.as_ref(),
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let image = image.unwrap_or(build_image);
|
||||
let update_available = digest
|
||||
.map(|digest| {
|
||||
deployment.info.latest_image_digest.update_available(digest)
|
||||
let update_available = current_digests
|
||||
.map(|current_digests| {
|
||||
deployment
|
||||
.info
|
||||
.latest_image_digest
|
||||
.update_available(current_digests)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
DeploymentListItem {
|
||||
|
||||
@@ -112,9 +112,9 @@ impl super::KomodoResource for Stack {
|
||||
.iter()
|
||||
.map(|current_service| {
|
||||
let update_available = current_service
|
||||
.image_digest
|
||||
.image_digests
|
||||
.as_ref()
|
||||
.map(|current_digest| {
|
||||
.map(|current_digests| {
|
||||
stack
|
||||
.info
|
||||
.latest_services
|
||||
@@ -126,7 +126,7 @@ impl super::KomodoResource for Stack {
|
||||
latest_service
|
||||
.image_digest
|
||||
.as_ref()?
|
||||
.update_available(¤t_digest.0)
|
||||
.update_available(current_digests)
|
||||
.into()
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::sync::{Arc, OnceLock};
|
||||
use anyhow::{Context, anyhow};
|
||||
use arc_swap::ArcSwap;
|
||||
use komodo_client::entities::{
|
||||
ImageDigest,
|
||||
action::ActionState,
|
||||
build::BuildState,
|
||||
deployment::DeploymentState,
|
||||
@@ -146,7 +147,7 @@ pub struct CachedDeploymentStatus {
|
||||
pub state: DeploymentState,
|
||||
pub container: Option<ContainerListItem>,
|
||||
pub service: Option<SwarmServiceListItem>,
|
||||
pub image_digest: Option<String>,
|
||||
pub image_digests: Option<Vec<ImageDigest>>,
|
||||
}
|
||||
|
||||
/// Cache of ids to status
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## All in one, multi stage compile + runtime Docker build for your architecture.
|
||||
|
||||
FROM rust:1.91.1-trixie AS builder
|
||||
FROM rust:1.93.0-trixie AS builder
|
||||
RUN cargo install cargo-strip
|
||||
|
||||
WORKDIR /builder
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::Context as _;
|
||||
use formatting::format_serror;
|
||||
use komodo_client::entities::{
|
||||
docker::config::SwarmConfig, update::Log,
|
||||
docker::config::SwarmConfigDetails, update::Log,
|
||||
};
|
||||
use mogh_resolver::Resolve;
|
||||
use periphery_client::api::swarm::*;
|
||||
@@ -17,7 +17,7 @@ impl Resolve<crate::api::Args> for InspectSwarmConfig {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &crate::api::Args,
|
||||
) -> anyhow::Result<SwarmConfig> {
|
||||
) -> anyhow::Result<SwarmConfigDetails> {
|
||||
inspect_swarm_config(&self.config).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use futures_util::{TryStreamExt as _, stream::FuturesUnordered};
|
||||
use komodo_client::entities::{
|
||||
all_logs_success,
|
||||
docker::{
|
||||
config::{SwarmConfig, SwarmConfigListItem},
|
||||
config::{SwarmConfigDetails, SwarmConfigListItem},
|
||||
service::{SwarmService, SwarmServiceListItem},
|
||||
},
|
||||
random_string,
|
||||
@@ -65,7 +65,7 @@ pub async fn list_swarm_configs(
|
||||
|
||||
pub async fn inspect_swarm_config(
|
||||
config: &str,
|
||||
) -> anyhow::Result<SwarmConfig> {
|
||||
) -> anyhow::Result<SwarmConfigDetails> {
|
||||
let res = run_komodo_standard_command(
|
||||
"Inspect Swarm Config",
|
||||
None,
|
||||
@@ -79,14 +79,15 @@ pub async fn inspect_swarm_config(
|
||||
)));
|
||||
}
|
||||
|
||||
let mut res = serde_json::from_str::<Vec<SwarmConfig>>(&res.stdout)
|
||||
.context(
|
||||
"Failed to parse 'docker config inspect' response from json",
|
||||
)?
|
||||
.pop()
|
||||
.with_context(|| {
|
||||
format!("Did not find any config matching {config}")
|
||||
})?;
|
||||
let mut res =
|
||||
serde_json::from_str::<Vec<SwarmConfigDetails>>(&res.stdout)
|
||||
.context(
|
||||
"Failed to parse 'docker config inspect' response from json",
|
||||
)?
|
||||
.pop()
|
||||
.with_context(|| {
|
||||
format!("Did not find any config matching {config}")
|
||||
})?;
|
||||
|
||||
// Convert data back to readable / editable format
|
||||
res.spec.iter_mut().next().map(|spec| {
|
||||
|
||||
@@ -27,19 +27,18 @@ impl DockerClient {
|
||||
.map(|id| id == &image.id)
|
||||
.unwrap_or_default()
|
||||
});
|
||||
let name = image
|
||||
.repo_tags
|
||||
.iter()
|
||||
.next()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| image.id.clone());
|
||||
ImageListItem {
|
||||
name: image
|
||||
.repo_tags
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap_or_else(|| image.id.clone()),
|
||||
digest: image
|
||||
.repo_digests
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap_or_default(),
|
||||
id: image.id,
|
||||
parent_id: image.parent_id,
|
||||
name,
|
||||
tags: image.repo_tags,
|
||||
digests: image.repo_digests,
|
||||
created: image.created,
|
||||
size: image.size,
|
||||
in_use,
|
||||
|
||||
@@ -5,7 +5,7 @@ use typeshare::typeshare;
|
||||
use crate::entities::{
|
||||
SearchCombinator, U64,
|
||||
docker::{
|
||||
config::{SwarmConfig, SwarmConfigListItem},
|
||||
config::{SwarmConfigDetails, SwarmConfigListItem},
|
||||
network::NetworkListItem,
|
||||
node::{SwarmNode, SwarmNodeListItem},
|
||||
secret::{SwarmSecret, SwarmSecretListItem},
|
||||
@@ -643,7 +643,7 @@ pub struct InspectSwarmConfig {
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
pub type InspectSwarmConfigResponse = SwarmConfig;
|
||||
pub type InspectSwarmConfigResponse = SwarmConfigDetails;
|
||||
|
||||
//
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ use serde::{Deserialize, Serialize};
|
||||
use typeshare::typeshare;
|
||||
|
||||
use crate::entities::{
|
||||
NoData,
|
||||
deployment::{_PartialDeploymentConfig, Deployment},
|
||||
update::Update,
|
||||
};
|
||||
@@ -234,4 +233,62 @@ pub struct CheckDeploymentForUpdate {
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
pub type CheckDeploymentForUpdateResponse = NoData;
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
pub struct CheckDeploymentForUpdateResponse {
|
||||
/// The deployment ID
|
||||
pub deployment: String,
|
||||
/// Whether update is available
|
||||
pub update_available: bool,
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
#[cfg(feature = "utoipa")]
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/BatchCheckDeploymentForUpdate",
|
||||
description = "Checks for newer image than what is deployed.",
|
||||
request_body(content = BatchCheckDeploymentForUpdate),
|
||||
responses(
|
||||
(status = 200, description = "Per deployment result", body = BatchCheckDeploymentForUpdateResponse),
|
||||
),
|
||||
)]
|
||||
pub fn batch_check_deployment_for_update() {}
|
||||
|
||||
/// Checks for newer image than what is deployed. Response: [BatchCheckDeploymentForUpdateResponse]
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, PartialEq, Resolve,
|
||||
)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[empty_traits(KomodoWriteRequest)]
|
||||
#[response(BatchCheckDeploymentForUpdateResponse)]
|
||||
#[error(mogh_error::Error)]
|
||||
pub struct BatchCheckDeploymentForUpdate {
|
||||
/// Id or name or wildcard pattern or regex.
|
||||
/// Supports multiline and comma delineated combinations of the above.
|
||||
///
|
||||
/// Example:
|
||||
/// ```text
|
||||
/// # match all foo-* deployments
|
||||
/// foo-*
|
||||
/// # add some more
|
||||
/// extra-deployment-1, extra-deployment-2
|
||||
/// ```
|
||||
pub pattern: String,
|
||||
/// Normally resources with 'auto_update' will be
|
||||
/// redeployed immediately if updates are found.
|
||||
/// With this enabled, convert this into an UpdateAvailable alert.
|
||||
#[serde(default)]
|
||||
pub skip_auto_update: bool,
|
||||
/// If check triggers auto deploy,
|
||||
/// whether this call should wait on the auto deploy,
|
||||
/// or run it in the background.
|
||||
#[serde(default)]
|
||||
pub wait_for_auto_update: bool,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
pub type BatchCheckDeploymentForUpdateResponse =
|
||||
Vec<CheckDeploymentForUpdateResponse>;
|
||||
|
||||
@@ -4,7 +4,7 @@ use typeshare::typeshare;
|
||||
|
||||
use crate::entities::{
|
||||
NoData,
|
||||
stack::{_PartialStackConfig, Stack},
|
||||
stack::{_PartialStackConfig, Stack, StackServiceWithUpdate},
|
||||
update::Update,
|
||||
};
|
||||
|
||||
@@ -266,7 +266,69 @@ pub struct CheckStackForUpdate {
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
pub type CheckStackForUpdateResponse = NoData;
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
pub struct CheckStackForUpdateResponse {
|
||||
/// The stack ID
|
||||
pub stack: String,
|
||||
/// The stack services with update available status
|
||||
pub services: Vec<StackServiceWithUpdate>,
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
#[cfg(feature = "utoipa")]
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/BatchCheckStackForUpdate",
|
||||
description = "Checks for new images.",
|
||||
request_body(content = BatchCheckStackForUpdate),
|
||||
responses(
|
||||
(status = 200, description = "Stack / service level results", body = BatchCheckStackForUpdateResponse),
|
||||
),
|
||||
)]
|
||||
pub fn batch_check_stack_for_update() {}
|
||||
|
||||
/// Checks for new images. Response: [BatchCheckStackForUpdateResponse]
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, PartialEq, Resolve,
|
||||
)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[empty_traits(KomodoWriteRequest)]
|
||||
#[response(BatchCheckStackForUpdateResponse)]
|
||||
#[error(mogh_error::Error)]
|
||||
pub struct BatchCheckStackForUpdate {
|
||||
/// Id or name or wildcard pattern or regex.
|
||||
/// Supports multiline and comma delineated combinations of the above.
|
||||
///
|
||||
/// Example:
|
||||
/// ```text
|
||||
/// # match all foo-* stacks
|
||||
/// foo-*
|
||||
/// # add some more
|
||||
/// extra-stack-1, extra-stack-2
|
||||
/// ```
|
||||
pub pattern: String,
|
||||
/// Normally resources with 'auto_update' will be
|
||||
/// redeployed immediately if updates are found.
|
||||
/// With this enabled, convert this into an UpdateAvailable alert.
|
||||
#[serde(default)]
|
||||
pub skip_auto_update: bool,
|
||||
/// If check triggers auto deploy,
|
||||
/// whether this call should wait on the auto deploy,
|
||||
/// or run it in the background.
|
||||
#[serde(default)]
|
||||
pub wait_for_auto_update: bool,
|
||||
/// Usually will refresh the stack cache before checking for updates.
|
||||
/// Skip with this option.
|
||||
#[serde(default)]
|
||||
pub skip_cache_refresh: bool,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
pub type BatchCheckStackForUpdateResponse =
|
||||
Vec<CheckStackForUpdateResponse>;
|
||||
|
||||
//
|
||||
|
||||
|
||||
@@ -94,20 +94,13 @@ pub struct Env {
|
||||
/// Override `first_server_address`
|
||||
#[serde(alias = "komodo_first_server")]
|
||||
pub komodo_first_server_address: Option<String>,
|
||||
/// Override `frontend_path`
|
||||
pub komodo_frontend_path: Option<String>,
|
||||
/// Override `jwt_secret`
|
||||
pub komodo_jwt_secret: Option<String>,
|
||||
/// Override `jwt_secret` from file
|
||||
pub komodo_jwt_secret_file: Option<PathBuf>,
|
||||
/// Override `jwt_ttl`
|
||||
pub komodo_jwt_ttl: Option<Timelength>,
|
||||
/// Override `sync_directory`
|
||||
pub komodo_sync_directory: Option<PathBuf>,
|
||||
/// Override `repo_directory`
|
||||
pub komodo_repo_directory: Option<PathBuf>,
|
||||
/// Override `action_directory`
|
||||
pub komodo_action_directory: Option<PathBuf>,
|
||||
|
||||
/// Override `resource_poll_interval`
|
||||
pub komodo_resource_poll_interval: Option<Timelength>,
|
||||
/// Override `monitoring_interval`
|
||||
@@ -232,6 +225,8 @@ pub struct Env {
|
||||
pub komodo_cors_allowed_origins: Option<Vec<String>>,
|
||||
/// Override `cors_allow_credentials`
|
||||
pub komodo_cors_allow_credentials: Option<bool>,
|
||||
/// Override `session_allow_cross_site`
|
||||
pub komodo_session_allow_cross_site: Option<bool>,
|
||||
|
||||
/// Override `database.uri`
|
||||
#[serde(alias = "komodo_mongo_uri")]
|
||||
@@ -279,6 +274,17 @@ pub struct Env {
|
||||
pub komodo_ssl_key_file: Option<String>,
|
||||
/// Override `ssl_cert_file`
|
||||
pub komodo_ssl_cert_file: Option<String>,
|
||||
|
||||
/// Override `frontend_path`
|
||||
pub komodo_frontend_path: Option<String>,
|
||||
/// Override `ui_index_force_no_cache`
|
||||
pub komodo_ui_index_force_no_cache: Option<bool>,
|
||||
/// Override `sync_directory`
|
||||
pub komodo_sync_directory: Option<PathBuf>,
|
||||
/// Override `repo_directory`
|
||||
pub komodo_repo_directory: Option<PathBuf>,
|
||||
/// Override `action_directory`
|
||||
pub komodo_action_directory: Option<PathBuf>,
|
||||
}
|
||||
|
||||
fn default_core_config_paths() -> Vec<PathBuf> {
|
||||
@@ -409,10 +415,6 @@ pub struct CoreConfig {
|
||||
)]
|
||||
pub first_server_address: Option<String>,
|
||||
|
||||
/// The path to the built frontend folder.
|
||||
#[serde(default = "default_frontend_path")]
|
||||
pub frontend_path: String,
|
||||
|
||||
/// Configure database connection
|
||||
#[serde(default, alias = "mongo")]
|
||||
pub database: DatabaseConfig,
|
||||
@@ -561,6 +563,12 @@ pub struct CoreConfig {
|
||||
#[serde(default)]
|
||||
pub cors_allow_credentials: bool,
|
||||
|
||||
/// Use SameSite=None (actually allows samesite) instead of SameSite=Lax.
|
||||
/// The third option, SameSite=Strict, won't work with external login,
|
||||
/// as the session cookie will be lost on redirect with auth provider.
|
||||
#[serde(default)]
|
||||
pub session_allow_cross_site: bool,
|
||||
|
||||
// ============
|
||||
// = Webhooks =
|
||||
// ============
|
||||
@@ -701,6 +709,16 @@ pub struct CoreConfig {
|
||||
/// Default: `/action-cache`
|
||||
#[serde(default = "default_action_directory")]
|
||||
pub action_directory: PathBuf,
|
||||
|
||||
/// The path to the built frontend folder.
|
||||
#[serde(default = "default_frontend_path")]
|
||||
pub frontend_path: String,
|
||||
|
||||
/// Force the `index.html` to served with
|
||||
/// 'Cache-Content: no-cache' header instead
|
||||
/// of using content hash as ETag.
|
||||
#[serde(default)]
|
||||
pub ui_index_force_no_cache: bool,
|
||||
}
|
||||
|
||||
fn default_title() -> String {
|
||||
@@ -798,7 +816,6 @@ impl Default for CoreConfig {
|
||||
enable_fancy_toml: Default::default(),
|
||||
first_server_address: Default::default(),
|
||||
first_server_name: Default::default(),
|
||||
frontend_path: default_frontend_path(),
|
||||
database: Default::default(),
|
||||
local_auth: Default::default(),
|
||||
min_password_length: default_min_password_length(),
|
||||
@@ -827,6 +844,7 @@ impl Default for CoreConfig {
|
||||
default_auth_rate_limit_window_seconds(),
|
||||
cors_allowed_origins: Default::default(),
|
||||
cors_allow_credentials: Default::default(),
|
||||
session_allow_cross_site: Default::default(),
|
||||
webhook_secret: Default::default(),
|
||||
webhook_base_url: Default::default(),
|
||||
logging: Default::default(),
|
||||
@@ -843,6 +861,8 @@ impl Default for CoreConfig {
|
||||
ssl_enabled: Default::default(),
|
||||
ssl_key_file: default_ssl_key_file(),
|
||||
ssl_cert_file: default_ssl_cert_file(),
|
||||
frontend_path: default_frontend_path(),
|
||||
ui_index_force_no_cache: Default::default(),
|
||||
sync_directory: default_sync_directory(),
|
||||
repo_directory: default_repo_directory(),
|
||||
action_directory: default_action_directory(),
|
||||
@@ -868,12 +888,8 @@ impl CoreConfig {
|
||||
timezone: config.timezone,
|
||||
first_server_address: config.first_server_address,
|
||||
first_server_name: config.first_server_name,
|
||||
frontend_path: config.frontend_path,
|
||||
jwt_secret: empty_or_redacted(&config.jwt_secret),
|
||||
jwt_ttl: config.jwt_ttl,
|
||||
repo_directory: config.repo_directory,
|
||||
action_directory: config.action_directory,
|
||||
sync_directory: config.sync_directory,
|
||||
internet_interface: config.internet_interface,
|
||||
resource_poll_interval: config.resource_poll_interval,
|
||||
monitoring_interval: config.monitoring_interval,
|
||||
@@ -935,6 +951,7 @@ impl CoreConfig {
|
||||
.auth_rate_limit_window_seconds,
|
||||
cors_allowed_origins: config.cors_allowed_origins,
|
||||
cors_allow_credentials: config.cors_allow_credentials,
|
||||
session_allow_cross_site: config.session_allow_cross_site,
|
||||
webhook_secret: empty_or_redacted(&config.webhook_secret),
|
||||
webhook_base_url: config.webhook_base_url,
|
||||
database: config.database.sanitized(),
|
||||
@@ -973,6 +990,11 @@ impl CoreConfig {
|
||||
ssl_enabled: config.ssl_enabled,
|
||||
ssl_key_file: config.ssl_key_file,
|
||||
ssl_cert_file: config.ssl_cert_file,
|
||||
frontend_path: config.frontend_path,
|
||||
ui_index_force_no_cache: config.ui_index_force_no_cache,
|
||||
repo_directory: config.repo_directory,
|
||||
action_directory: config.action_directory,
|
||||
sync_directory: config.sync_directory,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1011,9 +1033,6 @@ impl mogh_server::ServerConfig for &CoreConfig {
|
||||
}
|
||||
|
||||
impl mogh_server::cors::CorsConfig for &CoreConfig {
|
||||
fn allowed_origins_env_field(&self) -> &'static str {
|
||||
"KOMODO_CORS_ALLOWED_ORIGINS"
|
||||
}
|
||||
fn allowed_origins(&self) -> &[String] {
|
||||
&self.cors_allowed_origins
|
||||
}
|
||||
@@ -1023,13 +1042,16 @@ impl mogh_server::cors::CorsConfig for &CoreConfig {
|
||||
}
|
||||
|
||||
impl mogh_server::session::SessionConfig for &CoreConfig {
|
||||
fn expiry_seconds(&self) -> i64 {
|
||||
60
|
||||
fn host(&self) -> &str {
|
||||
&self.host
|
||||
}
|
||||
fn host_env_field(&self) -> &str {
|
||||
"KOMODO_HOST"
|
||||
}
|
||||
fn host(&self) -> &str {
|
||||
&self.host
|
||||
fn expiry_seconds(&self) -> i64 {
|
||||
60
|
||||
}
|
||||
fn allow_cross_site(&self) -> bool {
|
||||
self.session_allow_cross_site
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,12 +37,16 @@ pub struct SwarmConfigListItem {
|
||||
}
|
||||
|
||||
/// Swarm config details.
|
||||
///
|
||||
/// This would be just "SwarmConfig", but that would
|
||||
/// conflict with the Swarm (Komodo resource) Config type,
|
||||
/// which is also SwarmConfig.
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
|
||||
)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
pub struct SwarmConfig {
|
||||
pub struct SwarmConfigDetails {
|
||||
#[serde(rename = "ID")]
|
||||
pub id: Option<String>,
|
||||
|
||||
|
||||
@@ -13,14 +13,22 @@ use super::GraphDriverData;
|
||||
)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
pub struct ImageListItem {
|
||||
/// ID is the content-addressable ID of an image.
|
||||
/// This identifier is a content-addressable digest calculated from the image's configuration (which includes the digests of layers used by the image).
|
||||
/// Note that this digest differs from the `digests` below, which holds digests of image manifests that reference the image.
|
||||
pub id: String,
|
||||
/// ID of the parent image.
|
||||
/// Depending on how the image was created, this field may be empty and is only set for images that were built/created locally.
|
||||
/// This field is empty if the image was pulled from an image registry.
|
||||
pub parent_id: String,
|
||||
/// The first tag in `repo_tags`, or Id if no tags.
|
||||
pub name: String,
|
||||
/// The first digest in `repo_digests`, or empty if no digests.
|
||||
pub digest: String,
|
||||
/// ID is the content-addressable ID of an image. This identifier is a content-addressable digest calculated from the image's configuration (which includes the digests of layers used by the image). Note that this digest differs from the `RepoDigests` below, which holds digests of image manifests that reference the image.
|
||||
pub id: String,
|
||||
/// ID of the parent image. Depending on how the image was created, this field may be empty and is only set for images that were built/created locally. This field is empty if the image was pulled from an image registry.
|
||||
pub parent_id: String,
|
||||
/// The unchanged `RepoTags`.
|
||||
#[serde(default)]
|
||||
pub tags: Vec<String>,
|
||||
/// The unchanged `RepoDigests`.
|
||||
#[serde(default)]
|
||||
pub digests: Vec<String>,
|
||||
/// Date and time at which the image was created as a Unix timestamp (number of seconds sinds EPOCH).
|
||||
pub created: I64,
|
||||
/// Total size of the image including all layers it is composed of.
|
||||
|
||||
@@ -402,24 +402,66 @@ pub struct FileContents {
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
pub struct ImageDigest(pub String);
|
||||
pub struct ImageDigest(String);
|
||||
|
||||
impl ImageDigest {
|
||||
pub fn new(image: &str, digest: &str) -> Self {
|
||||
Self(format!("{image}@{digest}"))
|
||||
}
|
||||
|
||||
/// Handles incoming forms:
|
||||
/// - image-name@sha256:HASH
|
||||
/// - sha256:HASH
|
||||
pub fn parse(digest: &str) -> Option<Self> {
|
||||
if digest.contains('@') {
|
||||
Some(Self(digest.to_string()))
|
||||
} else if digest.starts_with("sha256:") {
|
||||
Some(Self(format!("__unknown__@{digest}")))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles incoming forms:
|
||||
/// - image-name@sha256:HASH
|
||||
/// - sha256:HASH (replaces image with __unknown__)
|
||||
pub fn vec(image_digests: &[String]) -> Vec<Self> {
|
||||
image_digests
|
||||
.iter()
|
||||
.filter_map(|digest| ImageDigest::parse(digest))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> String {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
|
||||
/// Assumes this ImageDigest represents latest.
|
||||
pub fn update_available(&self, current_digest: &str) -> bool {
|
||||
/// Checks all the digests against latest, if none are equal
|
||||
/// then there is an update available.
|
||||
pub fn update_available(
|
||||
&self,
|
||||
current_digests: &[ImageDigest],
|
||||
) -> bool {
|
||||
let Some(latest_digest) = self.digest() else {
|
||||
// There is no "latest" to compare to.
|
||||
return false;
|
||||
};
|
||||
let current_digest = current_digest
|
||||
.split_once('@')
|
||||
.map(|r| r.1)
|
||||
.unwrap_or(current_digest);
|
||||
latest_digest != current_digest
|
||||
let digests = current_digests
|
||||
.iter()
|
||||
.filter_map(|digest| digest.digest())
|
||||
.collect::<Vec<_>>();
|
||||
// No valid digests to compare to,
|
||||
// avoid potentially false positive
|
||||
if digests.is_empty() {
|
||||
return false;
|
||||
}
|
||||
// If the image digests do not contain latest
|
||||
// digest, then there is update available.
|
||||
!digests.contains(&latest_digest)
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
|
||||
@@ -770,8 +770,8 @@ pub struct StackService {
|
||||
pub container: Option<ContainerListItem>,
|
||||
/// The service (Swarm mode)
|
||||
pub swarm_service: Option<SwarmServiceListItem>,
|
||||
/// The service image digest (When deployed)
|
||||
pub image_digest: Option<ImageDigest>,
|
||||
/// The service image digests
|
||||
pub image_digests: Option<Vec<ImageDigest>>,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
|
||||
@@ -226,6 +226,7 @@ export type WriteResponses = {
|
||||
WriteStackFileContents: Types.Update;
|
||||
RefreshStackCache: Types.NoData;
|
||||
CheckStackForUpdate: Types.CheckStackForUpdateResponse;
|
||||
BatchCheckStackForUpdate: Types.BatchCheckStackForUpdateResponse;
|
||||
|
||||
// ==== DEPLOYMENT ====
|
||||
CreateDeployment: Types.Deployment;
|
||||
@@ -235,6 +236,7 @@ export type WriteResponses = {
|
||||
UpdateDeployment: Types.Deployment;
|
||||
RenameDeployment: Types.Update;
|
||||
CheckDeploymentForUpdate: Types.CheckDeploymentForUpdateResponse;
|
||||
BatchCheckDeploymentForUpdate: Types.BatchCheckDeploymentForUpdateResponse;
|
||||
|
||||
// ==== BUILD ====
|
||||
CreateBuild: Types.Build;
|
||||
|
||||
+149
-64
@@ -339,6 +339,32 @@ export interface AlerterQuerySpecifics {
|
||||
|
||||
export type AlerterQuery = ResourceQuery<AlerterQuerySpecifics>;
|
||||
|
||||
export interface CheckDeploymentForUpdateResponse {
|
||||
/** The deployment ID */
|
||||
deployment: string;
|
||||
/** Whether update is available */
|
||||
update_available: boolean;
|
||||
}
|
||||
|
||||
export type BatchCheckDeploymentForUpdateResponse = CheckDeploymentForUpdateResponse[];
|
||||
|
||||
export interface StackServiceWithUpdate {
|
||||
service: string;
|
||||
/** The service's image */
|
||||
image: string;
|
||||
/** Whether there is a newer image available for this service */
|
||||
update_available: boolean;
|
||||
}
|
||||
|
||||
export interface CheckStackForUpdateResponse {
|
||||
/** The stack ID */
|
||||
stack: string;
|
||||
/** The stack services with update available status */
|
||||
services: StackServiceWithUpdate[];
|
||||
}
|
||||
|
||||
export type BatchCheckStackForUpdateResponse = CheckStackForUpdateResponse[];
|
||||
|
||||
export type BatchExecutionResponseItem =
|
||||
| { status: "Ok", data: Update }
|
||||
| { status: "Err", data: BatchExecutionResponseItemErr };
|
||||
@@ -825,10 +851,6 @@ export interface BuilderQuerySpecifics {
|
||||
|
||||
export type BuilderQuery = ResourceQuery<BuilderQuerySpecifics>;
|
||||
|
||||
export type CheckDeploymentForUpdateResponse = NoData;
|
||||
|
||||
export type CheckStackForUpdateResponse = NoData;
|
||||
|
||||
/** A wrapper for all Komodo exections. */
|
||||
export type Execution =
|
||||
/** The "null" execution. Does nothing. */
|
||||
@@ -2716,43 +2738,19 @@ export interface SwarmActionState {
|
||||
|
||||
export type GetSwarmActionStateResponse = SwarmActionState;
|
||||
|
||||
export type U64 = number;
|
||||
|
||||
/** The version number of the object such as node, service, etc. This is needed to avoid conflicting writes. The client must send the version number along with the modified specification when updating these objects. This approach ensures safe concurrency and determinism in that the change on the object may not be applied if the version number has changed from the last read. In other words, if two update requests specify the same base version, only one of the requests can succeed. As a result, two separate update requests that happen at the same time will not unintentionally overwrite each other. */
|
||||
export interface ObjectVersion {
|
||||
Index?: U64;
|
||||
}
|
||||
|
||||
/** Driver represents a driver (network, logging, secrets). */
|
||||
export interface Driver {
|
||||
/** Name of the driver. */
|
||||
Name: string;
|
||||
/** Key/value map of driver-specific options. */
|
||||
Options?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ConfigSpec {
|
||||
/** User-defined name of the config. */
|
||||
Name?: string;
|
||||
/** User-defined key/value metadata. */
|
||||
Labels?: Record<string, string>;
|
||||
/**
|
||||
* Data is the data to store as a config, formatted as a Base64-url-safe-encoded ([RFC 4648](https://tools.ietf.org/html/rfc4648#section-5)) string.
|
||||
* It must be empty if the Driver field is set, in which case the data is loaded from an external secret store.
|
||||
* The maximum allowed size is 500KB, as defined in [MaxSecretSize](https://pkg.go.dev/github.com/moby/swarmkit/v2@v2.0.0-20250103191802-8c1959736554/api/validation#MaxSecretSize).
|
||||
*/
|
||||
Data?: string;
|
||||
/** Templating driver, if applicable Templating controls whether and how to evaluate the config payload as a template. If no driver is set, no templating is used. */
|
||||
Templating?: Driver;
|
||||
}
|
||||
|
||||
/** Swarm config details. */
|
||||
export interface SwarmConfig {
|
||||
ID?: string;
|
||||
Version?: ObjectVersion;
|
||||
CreatedAt?: string;
|
||||
UpdatedAt?: string;
|
||||
Spec?: ConfigSpec;
|
||||
/**
|
||||
* The Servers which are swarm manager nodes.
|
||||
* If a Server is not reachable or gives error,
|
||||
* tries the next Server.
|
||||
*/
|
||||
server_ids?: string[];
|
||||
/** Configure quick links that are displayed in the resource header */
|
||||
links?: string[];
|
||||
/** Whether to send alerts about the swarm health. */
|
||||
send_unhealthy_alerts: boolean;
|
||||
/** Scheduled maintenance windows during which alerts will be suppressed. */
|
||||
maintenance_windows?: MaintenanceWindow[];
|
||||
}
|
||||
|
||||
export interface SwarmInfo {
|
||||
@@ -3530,6 +3528,13 @@ export enum SwarmState {
|
||||
Unknown = "Unknown",
|
||||
}
|
||||
|
||||
export type U64 = number;
|
||||
|
||||
/** The version number of the object such as node, service, etc. This is needed to avoid conflicting writes. The client must send the version number along with the modified specification when updating these objects. This approach ensures safe concurrency and determinism in that the change on the object may not be applied if the version number has changed from the last read. In other words, if two update requests specify the same base version, only one of the requests can succeed. As a result, two separate update requests that happen at the same time will not unintentionally overwrite each other. */
|
||||
export interface ObjectVersion {
|
||||
Index?: U64;
|
||||
}
|
||||
|
||||
/** Describes a permission the user has to accept upon installing the plugin. */
|
||||
export interface PluginPrivilege {
|
||||
Name?: string;
|
||||
@@ -4516,7 +4521,45 @@ export type InspectStackSwarmInfoResponse = SwarmStack;
|
||||
|
||||
export type InspectStackSwarmServiceResponse = SwarmService;
|
||||
|
||||
export type InspectSwarmConfigResponse = SwarmConfig;
|
||||
/** Driver represents a driver (network, logging, secrets). */
|
||||
export interface Driver {
|
||||
/** Name of the driver. */
|
||||
Name: string;
|
||||
/** Key/value map of driver-specific options. */
|
||||
Options?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ConfigSpec {
|
||||
/** User-defined name of the config. */
|
||||
Name?: string;
|
||||
/** User-defined key/value metadata. */
|
||||
Labels?: Record<string, string>;
|
||||
/**
|
||||
* Data is the data to store as a config, formatted as a Base64-url-safe-encoded ([RFC 4648](https://tools.ietf.org/html/rfc4648#section-5)) string.
|
||||
* It must be empty if the Driver field is set, in which case the data is loaded from an external secret store.
|
||||
* The maximum allowed size is 500KB, as defined in [MaxSecretSize](https://pkg.go.dev/github.com/moby/swarmkit/v2@v2.0.0-20250103191802-8c1959736554/api/validation#MaxSecretSize).
|
||||
*/
|
||||
Data?: string;
|
||||
/** Templating driver, if applicable Templating controls whether and how to evaluate the config payload as a template. If no driver is set, no templating is used. */
|
||||
Templating?: Driver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Swarm config details.
|
||||
*
|
||||
* This would be just "SwarmConfig", but that would
|
||||
* conflict with the Swarm (Komodo resource) Config type,
|
||||
* which is also SwarmConfig.
|
||||
*/
|
||||
export interface SwarmConfigDetails {
|
||||
ID?: string;
|
||||
Version?: ObjectVersion;
|
||||
CreatedAt?: string;
|
||||
UpdatedAt?: string;
|
||||
Spec?: ConfigSpec;
|
||||
}
|
||||
|
||||
export type InspectSwarmConfigResponse = SwarmConfigDetails;
|
||||
|
||||
export enum NodeSpecRoleEnum {
|
||||
EMPTY = "",
|
||||
@@ -5352,14 +5395,6 @@ export enum StackState {
|
||||
Unknown = "unknown",
|
||||
}
|
||||
|
||||
export interface StackServiceWithUpdate {
|
||||
service: string;
|
||||
/** The service's image */
|
||||
image: string;
|
||||
/** Whether there is a newer image available for this service */
|
||||
update_available: boolean;
|
||||
}
|
||||
|
||||
export interface StackListItemInfo {
|
||||
/** The swarm that stack is deployed on, when in Swarm mode. */
|
||||
swarm_id: string;
|
||||
@@ -5825,6 +5860,69 @@ export interface BatchBuildRepo {
|
||||
pattern: string;
|
||||
}
|
||||
|
||||
/** Checks for newer image than what is deployed. Response: [BatchCheckDeploymentForUpdateResponse] */
|
||||
export interface BatchCheckDeploymentForUpdate {
|
||||
/**
|
||||
* Id or name or wildcard pattern or regex.
|
||||
* Supports multiline and comma delineated combinations of the above.
|
||||
*
|
||||
* Example:
|
||||
* ```text
|
||||
* # match all foo-* deployments
|
||||
* foo-*
|
||||
* # add some more
|
||||
* extra-deployment-1, extra-deployment-2
|
||||
* ```
|
||||
*/
|
||||
pattern: string;
|
||||
/**
|
||||
* Normally resources with 'auto_update' will be
|
||||
* redeployed immediately if updates are found.
|
||||
* With this enabled, convert this into an UpdateAvailable alert.
|
||||
*/
|
||||
skip_auto_update?: boolean;
|
||||
/**
|
||||
* If check triggers auto deploy,
|
||||
* whether this call should wait on the auto deploy,
|
||||
* or run it in the background.
|
||||
*/
|
||||
wait_for_auto_update?: boolean;
|
||||
}
|
||||
|
||||
/** Checks for new images. Response: [BatchCheckStackForUpdateResponse] */
|
||||
export interface BatchCheckStackForUpdate {
|
||||
/**
|
||||
* Id or name or wildcard pattern or regex.
|
||||
* Supports multiline and comma delineated combinations of the above.
|
||||
*
|
||||
* Example:
|
||||
* ```text
|
||||
* # match all foo-* stacks
|
||||
* foo-*
|
||||
* # add some more
|
||||
* extra-stack-1, extra-stack-2
|
||||
* ```
|
||||
*/
|
||||
pattern: string;
|
||||
/**
|
||||
* Normally resources with 'auto_update' will be
|
||||
* redeployed immediately if updates are found.
|
||||
* With this enabled, convert this into an UpdateAvailable alert.
|
||||
*/
|
||||
skip_auto_update?: boolean;
|
||||
/**
|
||||
* If check triggers auto deploy,
|
||||
* whether this call should wait on the auto deploy,
|
||||
* or run it in the background.
|
||||
*/
|
||||
wait_for_auto_update?: boolean;
|
||||
/**
|
||||
* Usually will refresh the stack cache before checking for updates.
|
||||
* Skip with this option.
|
||||
*/
|
||||
skip_cache_refresh?: boolean;
|
||||
}
|
||||
|
||||
/** Clones multiple Repos in parallel that match pattern. Response: [BatchExecutionResponse]. */
|
||||
export interface BatchCloneRepo {
|
||||
/**
|
||||
@@ -9910,21 +10008,6 @@ export interface StopStack {
|
||||
services?: string[];
|
||||
}
|
||||
|
||||
export interface SwarmConfig {
|
||||
/**
|
||||
* The Servers which are swarm manager nodes.
|
||||
* If a Server is not reachable or gives error,
|
||||
* tries the next Server.
|
||||
*/
|
||||
server_ids?: string[];
|
||||
/** Configure quick links that are displayed in the resource header */
|
||||
links?: string[];
|
||||
/** Whether to send alerts about the swarm health. */
|
||||
send_unhealthy_alerts: boolean;
|
||||
/** Scheduled maintenance windows during which alerts will be suppressed. */
|
||||
maintenance_windows?: MaintenanceWindow[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Swarm stack service list item.
|
||||
* Returned by `docker stack services --format json <NAME>`
|
||||
@@ -10858,6 +10941,7 @@ export type WriteRequest =
|
||||
| { type: "WriteStackFileContents", params: WriteStackFileContents }
|
||||
| { type: "RefreshStackCache", params: RefreshStackCache }
|
||||
| { type: "CheckStackForUpdate", params: CheckStackForUpdate }
|
||||
| { type: "BatchCheckStackForUpdate", params: BatchCheckStackForUpdate }
|
||||
| { type: "CreateDeployment", params: CreateDeployment }
|
||||
| { type: "CopyDeployment", params: CopyDeployment }
|
||||
| { type: "CreateDeploymentFromContainer", params: CreateDeploymentFromContainer }
|
||||
@@ -10865,6 +10949,7 @@ export type WriteRequest =
|
||||
| { type: "UpdateDeployment", params: UpdateDeployment }
|
||||
| { type: "RenameDeployment", params: RenameDeployment }
|
||||
| { type: "CheckDeploymentForUpdate", params: CheckDeploymentForUpdate }
|
||||
| { type: "BatchCheckDeploymentForUpdate", params: BatchCheckDeploymentForUpdate }
|
||||
| { type: "CreateBuild", params: CreateBuild }
|
||||
| { type: "CopyBuild", params: CopyBuild }
|
||||
| { type: "DeleteBuild", params: DeleteBuild }
|
||||
|
||||
@@ -5,7 +5,7 @@ use komodo_client::entities::{
|
||||
deployment::Deployment,
|
||||
docker::{
|
||||
SwarmLists,
|
||||
config::SwarmConfig,
|
||||
config::SwarmConfigDetails,
|
||||
node::{NodeSpecAvailabilityEnum, NodeSpecRoleEnum, SwarmNode},
|
||||
secret::SwarmSecret,
|
||||
service::SwarmService,
|
||||
@@ -265,7 +265,7 @@ pub struct InspectSwarmTask {
|
||||
// ========
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]
|
||||
#[response(SwarmConfig)]
|
||||
#[response(SwarmConfigDetails)]
|
||||
#[error(anyhow::Error)]
|
||||
pub struct InspectSwarmConfig {
|
||||
pub config: String,
|
||||
|
||||
@@ -324,9 +324,9 @@ auth_rate_limit_max_attempts = 5
|
||||
## Default: 15
|
||||
auth_rate_limit_window_seconds = 15
|
||||
|
||||
########
|
||||
# CORS #
|
||||
########
|
||||
##################
|
||||
# CORS / SESSION #
|
||||
##################
|
||||
|
||||
## Specifically set list of CORS allowed origins.
|
||||
## If empty, allows all origins (`*`).
|
||||
@@ -341,6 +341,10 @@ cors_allowed_origins = []
|
||||
## Default: false
|
||||
cors_allow_credentials = false
|
||||
|
||||
## Enabling this sets 'SameSite=None', which allows externally
|
||||
## hosted frontends to use the login flows.
|
||||
session_allow_cross_site = false
|
||||
|
||||
##################
|
||||
# POLL INTERVALS #
|
||||
##################
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM rust:1.91.1-bullseye as builder
|
||||
FROM rust:1.93.0-bullseye as builder
|
||||
WORKDIR /builder
|
||||
|
||||
COPY . .
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM rust:1.91.1-bullseye as builder
|
||||
FROM rust:1.93.0-bullseye as builder
|
||||
WORKDIR /builder
|
||||
|
||||
COPY . .
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM node:20.12-alpine AS builder
|
||||
FROM node:22.12-alpine AS builder
|
||||
|
||||
WORKDIR /builder
|
||||
|
||||
|
||||
+45
-44
@@ -11,66 +11,67 @@
|
||||
"build-client": "cd ../client/core/ts && yarn && yarn build && yarn link"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "0.27.9",
|
||||
"@floating-ui/react": "0.27.16",
|
||||
"@monaco-editor/react": "4.7.0",
|
||||
"@radix-ui/react-checkbox": "1.3.2",
|
||||
"@radix-ui/react-dialog": "1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "2.1.15",
|
||||
"@radix-ui/react-hover-card": "1.1.14",
|
||||
"@radix-ui/react-checkbox": "1.3.3",
|
||||
"@radix-ui/react-dialog": "1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "2.1.16",
|
||||
"@radix-ui/react-hover-card": "1.1.15",
|
||||
"@radix-ui/react-icons": "1.3.2",
|
||||
"@radix-ui/react-label": "2.1.7",
|
||||
"@radix-ui/react-popover": "1.1.14",
|
||||
"@radix-ui/react-progress": "1.1.7",
|
||||
"@radix-ui/react-select": "2.2.5",
|
||||
"@radix-ui/react-separator": "1.1.7",
|
||||
"@radix-ui/react-slot": "1.2.3",
|
||||
"@radix-ui/react-switch": "1.2.5",
|
||||
"@radix-ui/react-tabs": "1.1.12",
|
||||
"@radix-ui/react-toast": "1.2.14",
|
||||
"@radix-ui/react-toggle": "1.1.9",
|
||||
"@radix-ui/react-toggle-group": "1.1.10",
|
||||
"@tanstack/react-query": "5.77.2",
|
||||
"@radix-ui/react-label": "2.1.8",
|
||||
"@radix-ui/react-popover": "1.1.15",
|
||||
"@radix-ui/react-progress": "1.1.8",
|
||||
"@radix-ui/react-select": "2.2.6",
|
||||
"@radix-ui/react-separator": "1.1.8",
|
||||
"@radix-ui/react-slot": "1.2.4",
|
||||
"@radix-ui/react-switch": "1.2.6",
|
||||
"@radix-ui/react-tabs": "1.1.13",
|
||||
"@radix-ui/react-toast": "1.2.15",
|
||||
"@radix-ui/react-toggle": "1.1.10",
|
||||
"@radix-ui/react-toggle-group": "1.1.11",
|
||||
"@tanstack/react-query": "5.90.20",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"@xterm/addon-fit": "0.10.0",
|
||||
"@xterm/xterm": "5.5.0",
|
||||
"@types/node": "^25.0.10",
|
||||
"@xterm/addon-fit": "0.11.0",
|
||||
"@xterm/xterm": "6.0.0",
|
||||
"ansi-to-html": "0.7.2",
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
"cmdk": "1.1.1",
|
||||
"jotai": "2.12.5",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lucide-react": "0.511.0",
|
||||
"monaco-editor": "0.52.2",
|
||||
"jotai": "2.16.2",
|
||||
"lucide-react": "0.563.0",
|
||||
"monaco-editor": "0.55.1",
|
||||
"monaco-yaml": "5.4.0",
|
||||
"prettier": "3.5.3",
|
||||
"react": "19.1.0",
|
||||
"prettier": "3.8.1",
|
||||
"react": "19.2.3",
|
||||
"react-charts": "3.0.0-beta.57",
|
||||
"react-dom": "19.1.0",
|
||||
"react-minimal-pie-chart": "9.1.0",
|
||||
"react-router-dom": "7.6.1",
|
||||
"react-dom": "19.2.3",
|
||||
"react-minimal-pie-chart": "9.1.2",
|
||||
"react-router-dom": "7.13.0",
|
||||
"react-xtermjs": "1.0.10",
|
||||
"sanitize-html": "2.17.0",
|
||||
"shell-quote": "1.8.1",
|
||||
"shell-quote": "1.8.3",
|
||||
"tailwind-merge": "2.6.0",
|
||||
"tailwindcss-animate": "1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "19.1.6",
|
||||
"@types/react-dom": "19.1.5",
|
||||
"@types/react": "19.2.9",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@types/sanitize-html": "2.16.0",
|
||||
"@types/shell-quote": "^1.7.5",
|
||||
"@typescript-eslint/eslint-plugin": "8.33.0",
|
||||
"@typescript-eslint/parser": "8.33.0",
|
||||
"@vitejs/plugin-react": "4.5.0",
|
||||
"autoprefixer": "10.4.21",
|
||||
"eslint": "9.27.0",
|
||||
"eslint-plugin-react-hooks": "5.2.0",
|
||||
"eslint-plugin-react-refresh": "0.4.20",
|
||||
"postcss": "8.5.3",
|
||||
"tailwindcss": "3.4.17",
|
||||
"typescript": "5.8.3",
|
||||
"vite": "6.0.7",
|
||||
"vite-tsconfig-paths": "5.1.4"
|
||||
"@types/shell-quote": "1.7.5",
|
||||
"@typescript-eslint/eslint-plugin": "8.53.1",
|
||||
"@typescript-eslint/parser": "8.53.1",
|
||||
"@vitejs/plugin-react": "5.1.2",
|
||||
"autoprefixer": "10.4.23",
|
||||
"dotenv": "17.2.3",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-plugin-react-hooks": "7.0.1",
|
||||
"eslint-plugin-react-refresh": "0.4.26",
|
||||
"postcss": "8.5.6",
|
||||
"tailwindcss": "3.4.19",
|
||||
"typescript": "5.9.3",
|
||||
"vite": "7.3.1",
|
||||
"vite-tsconfig-paths": "6.0.5"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
||||
+2
@@ -167,6 +167,7 @@ export type WriteResponses = {
|
||||
WriteStackFileContents: Types.Update;
|
||||
RefreshStackCache: Types.NoData;
|
||||
CheckStackForUpdate: Types.CheckStackForUpdateResponse;
|
||||
BatchCheckStackForUpdate: Types.BatchCheckStackForUpdateResponse;
|
||||
CreateDeployment: Types.Deployment;
|
||||
CopyDeployment: Types.Deployment;
|
||||
CreateDeploymentFromContainer: Types.Deployment;
|
||||
@@ -174,6 +175,7 @@ export type WriteResponses = {
|
||||
UpdateDeployment: Types.Deployment;
|
||||
RenameDeployment: Types.Update;
|
||||
CheckDeploymentForUpdate: Types.CheckDeploymentForUpdateResponse;
|
||||
BatchCheckDeploymentForUpdate: Types.BatchCheckDeploymentForUpdateResponse;
|
||||
CreateBuild: Types.Build;
|
||||
CopyBuild: Types.Build;
|
||||
DeleteBuild: Types.Build;
|
||||
|
||||
Vendored
+141
-56
@@ -344,6 +344,27 @@ export interface AlerterQuerySpecifics {
|
||||
types: AlerterEndpoint["type"][];
|
||||
}
|
||||
export type AlerterQuery = ResourceQuery<AlerterQuerySpecifics>;
|
||||
export interface CheckDeploymentForUpdateResponse {
|
||||
/** The deployment ID */
|
||||
deployment: string;
|
||||
/** Whether update is available */
|
||||
update_available: boolean;
|
||||
}
|
||||
export type BatchCheckDeploymentForUpdateResponse = CheckDeploymentForUpdateResponse[];
|
||||
export interface StackServiceWithUpdate {
|
||||
service: string;
|
||||
/** The service's image */
|
||||
image: string;
|
||||
/** Whether there is a newer image available for this service */
|
||||
update_available: boolean;
|
||||
}
|
||||
export interface CheckStackForUpdateResponse {
|
||||
/** The stack ID */
|
||||
stack: string;
|
||||
/** The stack services with update available status */
|
||||
services: StackServiceWithUpdate[];
|
||||
}
|
||||
export type BatchCheckStackForUpdateResponse = CheckStackForUpdateResponse[];
|
||||
export type BatchExecutionResponseItem = {
|
||||
status: "Ok";
|
||||
data: Update;
|
||||
@@ -819,8 +840,6 @@ export type BuilderListItem = ResourceListItem<BuilderListItemInfo>;
|
||||
export interface BuilderQuerySpecifics {
|
||||
}
|
||||
export type BuilderQuery = ResourceQuery<BuilderQuerySpecifics>;
|
||||
export type CheckDeploymentForUpdateResponse = NoData;
|
||||
export type CheckStackForUpdateResponse = NoData;
|
||||
/** A wrapper for all Komodo exections. */
|
||||
export type Execution =
|
||||
/** The "null" execution. Does nothing. */
|
||||
@@ -2852,39 +2871,19 @@ export type GetStackResponse = Stack;
|
||||
export interface SwarmActionState {
|
||||
}
|
||||
export type GetSwarmActionStateResponse = SwarmActionState;
|
||||
export type U64 = number;
|
||||
/** The version number of the object such as node, service, etc. This is needed to avoid conflicting writes. The client must send the version number along with the modified specification when updating these objects. This approach ensures safe concurrency and determinism in that the change on the object may not be applied if the version number has changed from the last read. In other words, if two update requests specify the same base version, only one of the requests can succeed. As a result, two separate update requests that happen at the same time will not unintentionally overwrite each other. */
|
||||
export interface ObjectVersion {
|
||||
Index?: U64;
|
||||
}
|
||||
/** Driver represents a driver (network, logging, secrets). */
|
||||
export interface Driver {
|
||||
/** Name of the driver. */
|
||||
Name: string;
|
||||
/** Key/value map of driver-specific options. */
|
||||
Options?: Record<string, string>;
|
||||
}
|
||||
export interface ConfigSpec {
|
||||
/** User-defined name of the config. */
|
||||
Name?: string;
|
||||
/** User-defined key/value metadata. */
|
||||
Labels?: Record<string, string>;
|
||||
/**
|
||||
* Data is the data to store as a config, formatted as a Base64-url-safe-encoded ([RFC 4648](https://tools.ietf.org/html/rfc4648#section-5)) string.
|
||||
* It must be empty if the Driver field is set, in which case the data is loaded from an external secret store.
|
||||
* The maximum allowed size is 500KB, as defined in [MaxSecretSize](https://pkg.go.dev/github.com/moby/swarmkit/v2@v2.0.0-20250103191802-8c1959736554/api/validation#MaxSecretSize).
|
||||
*/
|
||||
Data?: string;
|
||||
/** Templating driver, if applicable Templating controls whether and how to evaluate the config payload as a template. If no driver is set, no templating is used. */
|
||||
Templating?: Driver;
|
||||
}
|
||||
/** Swarm config details. */
|
||||
export interface SwarmConfig {
|
||||
ID?: string;
|
||||
Version?: ObjectVersion;
|
||||
CreatedAt?: string;
|
||||
UpdatedAt?: string;
|
||||
Spec?: ConfigSpec;
|
||||
/**
|
||||
* The Servers which are swarm manager nodes.
|
||||
* If a Server is not reachable or gives error,
|
||||
* tries the next Server.
|
||||
*/
|
||||
server_ids?: string[];
|
||||
/** Configure quick links that are displayed in the resource header */
|
||||
links?: string[];
|
||||
/** Whether to send alerts about the swarm health. */
|
||||
send_unhealthy_alerts: boolean;
|
||||
/** Scheduled maintenance windows during which alerts will be suppressed. */
|
||||
maintenance_windows?: MaintenanceWindow[];
|
||||
}
|
||||
export interface SwarmInfo {
|
||||
}
|
||||
@@ -3608,6 +3607,11 @@ export declare enum SwarmState {
|
||||
/** Unknown case */
|
||||
Unknown = "Unknown"
|
||||
}
|
||||
export type U64 = number;
|
||||
/** The version number of the object such as node, service, etc. This is needed to avoid conflicting writes. The client must send the version number along with the modified specification when updating these objects. This approach ensures safe concurrency and determinism in that the change on the object may not be applied if the version number has changed from the last read. In other words, if two update requests specify the same base version, only one of the requests can succeed. As a result, two separate update requests that happen at the same time will not unintentionally overwrite each other. */
|
||||
export interface ObjectVersion {
|
||||
Index?: U64;
|
||||
}
|
||||
/** Describes a permission the user has to accept upon installing the plugin. */
|
||||
export interface PluginPrivilege {
|
||||
Name?: string;
|
||||
@@ -4500,7 +4504,42 @@ export interface SwarmStack {
|
||||
}
|
||||
export type InspectStackSwarmInfoResponse = SwarmStack;
|
||||
export type InspectStackSwarmServiceResponse = SwarmService;
|
||||
export type InspectSwarmConfigResponse = SwarmConfig;
|
||||
/** Driver represents a driver (network, logging, secrets). */
|
||||
export interface Driver {
|
||||
/** Name of the driver. */
|
||||
Name: string;
|
||||
/** Key/value map of driver-specific options. */
|
||||
Options?: Record<string, string>;
|
||||
}
|
||||
export interface ConfigSpec {
|
||||
/** User-defined name of the config. */
|
||||
Name?: string;
|
||||
/** User-defined key/value metadata. */
|
||||
Labels?: Record<string, string>;
|
||||
/**
|
||||
* Data is the data to store as a config, formatted as a Base64-url-safe-encoded ([RFC 4648](https://tools.ietf.org/html/rfc4648#section-5)) string.
|
||||
* It must be empty if the Driver field is set, in which case the data is loaded from an external secret store.
|
||||
* The maximum allowed size is 500KB, as defined in [MaxSecretSize](https://pkg.go.dev/github.com/moby/swarmkit/v2@v2.0.0-20250103191802-8c1959736554/api/validation#MaxSecretSize).
|
||||
*/
|
||||
Data?: string;
|
||||
/** Templating driver, if applicable Templating controls whether and how to evaluate the config payload as a template. If no driver is set, no templating is used. */
|
||||
Templating?: Driver;
|
||||
}
|
||||
/**
|
||||
* Swarm config details.
|
||||
*
|
||||
* This would be just "SwarmConfig", but that would
|
||||
* conflict with the Swarm (Komodo resource) Config type,
|
||||
* which is also SwarmConfig.
|
||||
*/
|
||||
export interface SwarmConfigDetails {
|
||||
ID?: string;
|
||||
Version?: ObjectVersion;
|
||||
CreatedAt?: string;
|
||||
UpdatedAt?: string;
|
||||
Spec?: ConfigSpec;
|
||||
}
|
||||
export type InspectSwarmConfigResponse = SwarmConfigDetails;
|
||||
export declare enum NodeSpecRoleEnum {
|
||||
EMPTY = "",
|
||||
WORKER = "worker",
|
||||
@@ -5231,13 +5270,6 @@ export declare enum StackState {
|
||||
/** Server not reachable for status */
|
||||
Unknown = "unknown"
|
||||
}
|
||||
export interface StackServiceWithUpdate {
|
||||
service: string;
|
||||
/** The service's image */
|
||||
image: string;
|
||||
/** Whether there is a newer image available for this service */
|
||||
update_available: boolean;
|
||||
}
|
||||
export interface StackListItemInfo {
|
||||
/** The swarm that stack is deployed on, when in Swarm mode. */
|
||||
swarm_id: string;
|
||||
@@ -5629,6 +5661,67 @@ export interface BatchBuildRepo {
|
||||
*/
|
||||
pattern: string;
|
||||
}
|
||||
/** Checks for newer image than what is deployed. Response: [BatchCheckDeploymentForUpdateResponse] */
|
||||
export interface BatchCheckDeploymentForUpdate {
|
||||
/**
|
||||
* Id or name or wildcard pattern or regex.
|
||||
* Supports multiline and comma delineated combinations of the above.
|
||||
*
|
||||
* Example:
|
||||
* ```text
|
||||
* # match all foo-* deployments
|
||||
* foo-*
|
||||
* # add some more
|
||||
* extra-deployment-1, extra-deployment-2
|
||||
* ```
|
||||
*/
|
||||
pattern: string;
|
||||
/**
|
||||
* Normally resources with 'auto_update' will be
|
||||
* redeployed immediately if updates are found.
|
||||
* With this enabled, convert this into an UpdateAvailable alert.
|
||||
*/
|
||||
skip_auto_update?: boolean;
|
||||
/**
|
||||
* If check triggers auto deploy,
|
||||
* whether this call should wait on the auto deploy,
|
||||
* or run it in the background.
|
||||
*/
|
||||
wait_for_auto_update?: boolean;
|
||||
}
|
||||
/** Checks for new images. Response: [BatchCheckStackForUpdateResponse] */
|
||||
export interface BatchCheckStackForUpdate {
|
||||
/**
|
||||
* Id or name or wildcard pattern or regex.
|
||||
* Supports multiline and comma delineated combinations of the above.
|
||||
*
|
||||
* Example:
|
||||
* ```text
|
||||
* # match all foo-* stacks
|
||||
* foo-*
|
||||
* # add some more
|
||||
* extra-stack-1, extra-stack-2
|
||||
* ```
|
||||
*/
|
||||
pattern: string;
|
||||
/**
|
||||
* Normally resources with 'auto_update' will be
|
||||
* redeployed immediately if updates are found.
|
||||
* With this enabled, convert this into an UpdateAvailable alert.
|
||||
*/
|
||||
skip_auto_update?: boolean;
|
||||
/**
|
||||
* If check triggers auto deploy,
|
||||
* whether this call should wait on the auto deploy,
|
||||
* or run it in the background.
|
||||
*/
|
||||
wait_for_auto_update?: boolean;
|
||||
/**
|
||||
* Usually will refresh the stack cache before checking for updates.
|
||||
* Skip with this option.
|
||||
*/
|
||||
skip_cache_refresh?: boolean;
|
||||
}
|
||||
/** Clones multiple Repos in parallel that match pattern. Response: [BatchExecutionResponse]. */
|
||||
export interface BatchCloneRepo {
|
||||
/**
|
||||
@@ -9362,20 +9455,6 @@ export interface StopStack {
|
||||
*/
|
||||
services?: string[];
|
||||
}
|
||||
export interface SwarmConfig {
|
||||
/**
|
||||
* The Servers which are swarm manager nodes.
|
||||
* If a Server is not reachable or gives error,
|
||||
* tries the next Server.
|
||||
*/
|
||||
server_ids?: string[];
|
||||
/** Configure quick links that are displayed in the resource header */
|
||||
links?: string[];
|
||||
/** Whether to send alerts about the swarm health. */
|
||||
send_unhealthy_alerts: boolean;
|
||||
/** Scheduled maintenance windows during which alerts will be suppressed. */
|
||||
maintenance_windows?: MaintenanceWindow[];
|
||||
}
|
||||
/**
|
||||
* Swarm stack service list item.
|
||||
* Returned by `docker stack services --format json <NAME>`
|
||||
@@ -10743,6 +10822,9 @@ export type WriteRequest = {
|
||||
} | {
|
||||
type: "CheckStackForUpdate";
|
||||
params: CheckStackForUpdate;
|
||||
} | {
|
||||
type: "BatchCheckStackForUpdate";
|
||||
params: BatchCheckStackForUpdate;
|
||||
} | {
|
||||
type: "CreateDeployment";
|
||||
params: CreateDeployment;
|
||||
@@ -10764,6 +10846,9 @@ export type WriteRequest = {
|
||||
} | {
|
||||
type: "CheckDeploymentForUpdate";
|
||||
params: CheckDeploymentForUpdate;
|
||||
} | {
|
||||
type: "BatchCheckDeploymentForUpdate";
|
||||
params: BatchCheckDeploymentForUpdate;
|
||||
} | {
|
||||
type: "CreateBuild";
|
||||
params: CreateBuild;
|
||||
|
||||
@@ -120,13 +120,13 @@ const GroupActionDialog = ({
|
||||
action! as Types.ExecuteRequest["type"],
|
||||
{
|
||||
onSuccess: onClose,
|
||||
}
|
||||
},
|
||||
);
|
||||
const { mutate: write, isPending: writePending } = useWrite(
|
||||
action! as Types.WriteRequest["type"],
|
||||
{
|
||||
onSuccess: onClose,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (!action) return;
|
||||
@@ -146,7 +146,7 @@ const GroupActionDialog = ({
|
||||
<li key={resource}>{resource}</li>
|
||||
))}
|
||||
</ul>
|
||||
{!action.startsWith("Refresh") && (
|
||||
{!action.startsWith("Refresh") && !action.startsWith("Check") && (
|
||||
<>
|
||||
<p
|
||||
onClick={() => {
|
||||
@@ -173,7 +173,10 @@ const GroupActionDialog = ({
|
||||
for (const resource of selected) {
|
||||
if (action.startsWith("Delete")) {
|
||||
write({ id: resource } as any);
|
||||
} else if (action.startsWith("Refresh")) {
|
||||
} else if (
|
||||
action.startsWith("Refresh") ||
|
||||
action.startsWith("Check")
|
||||
) {
|
||||
write({ [usableResourceExecuteKey(type)]: resource } as any);
|
||||
} else {
|
||||
execute({
|
||||
@@ -185,7 +188,11 @@ const GroupActionDialog = ({
|
||||
setSelected([]);
|
||||
}
|
||||
}}
|
||||
disabled={action.startsWith("Refresh") ? false : text !== formatted}
|
||||
disabled={
|
||||
action.startsWith("Refresh") || action.startsWith("Check")
|
||||
? false
|
||||
: text !== formatted
|
||||
}
|
||||
loading={isPending}
|
||||
/>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -140,6 +140,7 @@ export const DeploymentComponents: RequiredResourceComponents = {
|
||||
<GroupActions
|
||||
type="Deployment"
|
||||
actions={[
|
||||
"CheckDeploymentForUpdate",
|
||||
"PullDeployment",
|
||||
"Deploy",
|
||||
"RestartDeployment",
|
||||
|
||||
@@ -114,6 +114,7 @@ export const StackComponents: RequiredResourceComponents = {
|
||||
<GroupActions
|
||||
type="Stack"
|
||||
actions={[
|
||||
"CheckStackForUpdate",
|
||||
"PullStack",
|
||||
"DeployStack",
|
||||
"RestartStack",
|
||||
|
||||
@@ -131,7 +131,7 @@ export const Terminal = ({
|
||||
debounce = setTimeout(() => {
|
||||
if (!viewport) return;
|
||||
viewport.scrollTop = viewport.scrollHeight - viewport.clientHeight;
|
||||
}, 500);
|
||||
}, 500) as any;
|
||||
});
|
||||
},
|
||||
on_close: () => {
|
||||
|
||||
@@ -68,7 +68,7 @@ export const useLogin = <
|
||||
return useMutation({
|
||||
mutationKey: [type],
|
||||
mutationFn: (params: P) => komodo_client().auth.login<T, R>(type, params),
|
||||
onError: (e: { result: { error?: string; trace?: string[] } }, v, c) => {
|
||||
onError: (e: { result: { error?: string; trace?: string[] } }, ...args) => {
|
||||
console.log("Login error:", e);
|
||||
const msg = e.result.error ?? "Unknown error. See console.";
|
||||
const detail = e.result?.trace
|
||||
@@ -83,7 +83,7 @@ export const useLogin = <
|
||||
description: `${msg_log}See console for details`,
|
||||
variant: "destructive",
|
||||
});
|
||||
config?.onError && config.onError(e, v, c);
|
||||
config?.onError && config.onError(e, ...args);
|
||||
},
|
||||
...config,
|
||||
});
|
||||
@@ -105,7 +105,7 @@ export const useManageAuth = <
|
||||
return useMutation({
|
||||
mutationKey: [type],
|
||||
mutationFn: (params: P) => komodo_client().auth.manage<T, R>(type, params),
|
||||
onError: (e: { result: { error?: string; trace?: string[] } }, v, c) => {
|
||||
onError: (e: { result: { error?: string; trace?: string[] } }, ...args) => {
|
||||
console.log("Manage auth error:", e);
|
||||
const msg = e.result.error ?? "Unknown error. See console.";
|
||||
const detail = e.result?.trace
|
||||
@@ -120,7 +120,7 @@ export const useManageAuth = <
|
||||
description: `${msg_log}See console for details`,
|
||||
variant: "destructive",
|
||||
});
|
||||
config?.onError && config.onError(e, v, c);
|
||||
config?.onError && config.onError(e, ...args);
|
||||
},
|
||||
...config,
|
||||
});
|
||||
@@ -267,7 +267,7 @@ export const useWrite = <
|
||||
return useMutation({
|
||||
mutationKey: [type],
|
||||
mutationFn: (params: P) => komodo_client().write<T, R>(type, params),
|
||||
onError: (e: { result: { error?: string; trace?: string[] } }, v, c) => {
|
||||
onError: (e: { result: { error?: string; trace?: string[] } }, ...args) => {
|
||||
console.log("Write error:", e);
|
||||
const msg = e.result.error ?? "Unknown error. See console.";
|
||||
const detail = e.result?.trace
|
||||
@@ -282,7 +282,7 @@ export const useWrite = <
|
||||
description: `${msg_log}See console for details`,
|
||||
variant: "destructive",
|
||||
});
|
||||
config?.onError && config.onError(e, v, c);
|
||||
config?.onError && config.onError(e, ...args);
|
||||
},
|
||||
...config,
|
||||
});
|
||||
@@ -304,7 +304,7 @@ export const useExecute = <
|
||||
return useMutation({
|
||||
mutationKey: [type],
|
||||
mutationFn: (params: P) => komodo_client().execute<T, R>(type, params),
|
||||
onError: (e: { result: { error?: string; trace?: string[] } }, v, c) => {
|
||||
onError: (e: { result: { error?: string; trace?: string[] } }, ...args) => {
|
||||
console.log("Execute error:", e);
|
||||
const msg = e.result.error ?? "Unknown error. See console.";
|
||||
const detail = e.result?.trace
|
||||
@@ -319,7 +319,7 @@ export const useExecute = <
|
||||
description: `${msg_log}See console for details`,
|
||||
variant: "destructive",
|
||||
});
|
||||
config?.onError && config.onError(e, v, c);
|
||||
config?.onError && config.onError(e, ...args);
|
||||
},
|
||||
...config,
|
||||
});
|
||||
|
||||
+16
-16
@@ -40,7 +40,7 @@ const onMessageHandlers: {
|
||||
|
||||
export const useWebsocketMessages = (
|
||||
key: string,
|
||||
handler: (update: Types.UpdateListItem) => void
|
||||
handler: (update: Types.UpdateListItem) => void,
|
||||
) => {
|
||||
onMessageHandlers[key] = handler;
|
||||
useEffect(() => {
|
||||
@@ -66,7 +66,7 @@ export const WebsocketProvider = ({ children }: { children: ReactNode }) => {
|
||||
|
||||
const on_update_fn = useCallback(
|
||||
(update: Types.UpdateListItem) => on_update(update, invalidate),
|
||||
[invalidate]
|
||||
[invalidate],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -89,7 +89,7 @@ export const WebsocketProvider = ({ children }: { children: ReactNode }) => {
|
||||
console.info(count, "| Automatically triggering reconnect");
|
||||
reconnect();
|
||||
}
|
||||
}, 5_000);
|
||||
}, 5_000) as any;
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -103,7 +103,7 @@ export const WebsocketProvider = ({ children }: { children: ReactNode }) => {
|
||||
|
||||
const on_update = (
|
||||
update: Types.UpdateListItem,
|
||||
invalidate: ReturnType<typeof useInvalidate>
|
||||
invalidate: ReturnType<typeof useInvalidate>,
|
||||
) => {
|
||||
const Components = ResourceComponents[update.target.type as UsableResource];
|
||||
const title = Components ? (
|
||||
@@ -178,7 +178,7 @@ const on_update = (
|
||||
["InspectDockerImage"],
|
||||
["ListDockerVolumes"],
|
||||
["InspectDockerVolume"],
|
||||
["GetResourceMatchingContainer"]
|
||||
["GetResourceMatchingContainer"],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -199,7 +199,7 @@ const on_update = (
|
||||
["ListSwarmConfigs"],
|
||||
["InspectSwarmConfig"],
|
||||
["ListSwarmSecrets"],
|
||||
["InspectSwarmSecret"]
|
||||
["InspectSwarmSecret"],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ const on_update = (
|
||||
["GetServersSummary"],
|
||||
["GetServer"],
|
||||
["GetServerState"],
|
||||
["GetHistoricalServerStats"]
|
||||
["GetHistoricalServerStats"],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -228,7 +228,7 @@ const on_update = (
|
||||
["SearchStackLog", { stack: update.target.id }],
|
||||
["GetStack"],
|
||||
["ListStackServices"],
|
||||
["GetResourceMatchingContainer"]
|
||||
["GetResourceMatchingContainer"],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@ const on_update = (
|
||||
["GetDeploymentLog", { deployment: update.target.id }],
|
||||
["SearchDeploymentLog", { deployment: update.target.id }],
|
||||
["GetDeploymentContainer"],
|
||||
["GetResourceMatchingContainer"]
|
||||
["GetResourceMatchingContainer"],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -254,7 +254,7 @@ const on_update = (
|
||||
["GetBuildsSummary"],
|
||||
["GetBuildMonthlyStats"],
|
||||
["GetBuild"],
|
||||
["ListBuildVersions"]
|
||||
["ListBuildVersions"],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -263,7 +263,7 @@ const on_update = (
|
||||
["ListRepos"],
|
||||
["ListFullRepos"],
|
||||
["GetReposSummary"],
|
||||
["GetRepo"]
|
||||
["GetRepo"],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ const on_update = (
|
||||
["ListProcedures"],
|
||||
["ListFullProcedures"],
|
||||
["GetProceduresSummary"],
|
||||
["GetProcedure"]
|
||||
["GetProcedure"],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -283,7 +283,7 @@ const on_update = (
|
||||
["ListActions"],
|
||||
["ListFullActions"],
|
||||
["GetActionsSummary"],
|
||||
["GetAction"]
|
||||
["GetAction"],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -292,7 +292,7 @@ const on_update = (
|
||||
["ListResourceSyncs"],
|
||||
["ListFullResourceSyncs"],
|
||||
["GetResourceSyncsSummary"],
|
||||
["GetResourceSync"]
|
||||
["GetResourceSync"],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -301,7 +301,7 @@ const on_update = (
|
||||
["ListBuilders"],
|
||||
["ListFullBuilders"],
|
||||
["GetBuildersSummary"],
|
||||
["GetBuilder"]
|
||||
["GetBuilder"],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -310,7 +310,7 @@ const on_update = (
|
||||
["ListAlerters"],
|
||||
["ListFullAlerters"],
|
||||
["GetAlertersSummary"],
|
||||
["GetAlerter"]
|
||||
["GetAlerter"],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -323,41 +323,4 @@ export const terminalLink = ({
|
||||
case "Deployment":
|
||||
return `/deployments/${target.params.deployment}/terminal/${name}`;
|
||||
}
|
||||
};
|
||||
|
||||
export const base64urlToArrayBuffer = (base64url: any) => {
|
||||
// Convert from URL-safe base64 to normal base64
|
||||
const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
|
||||
const pad =
|
||||
base64.length % 4 === 0 ? "" : "=".repeat(4 - (base64.length % 4));
|
||||
const bstr = atob(base64 + pad);
|
||||
const bytes = new Uint8Array(bstr.length);
|
||||
for (let i = 0; i < bstr.length; i++) {
|
||||
bytes[i] = bstr.charCodeAt(i);
|
||||
}
|
||||
return bytes.buffer;
|
||||
};
|
||||
|
||||
export const arrayBufferToBase64url = (buffer: any) => {
|
||||
const bytes = new Uint8Array(buffer);
|
||||
let binary = "";
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
const base64 = btoa(binary);
|
||||
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
||||
};
|
||||
|
||||
export const preparePasskeyCredential = (data: any) => {
|
||||
return {
|
||||
...data,
|
||||
publicKey: {
|
||||
...data.publicKey,
|
||||
challenge: base64urlToArrayBuffer(data.publicKey.challenge),
|
||||
allowCredentials: data.publicKey.allowCredentials?.map((cred: any) => ({
|
||||
...cred,
|
||||
id: base64urlToArrayBuffer(cred.id),
|
||||
})),
|
||||
},
|
||||
};
|
||||
};
|
||||
};
|
||||
+18
-18
@@ -7,13 +7,13 @@ export async function init_monaco() {
|
||||
fetch(`/client/${file}${extension}`)
|
||||
.then((res) => res.text())
|
||||
.then((dts) =>
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(
|
||||
monaco.typescript.typescriptDefaults.addExtraLib(
|
||||
dts,
|
||||
`file:///client/${file}${extension}`
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
`file:///client/${file}${extension}`,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
promises.push(
|
||||
Promise.all(
|
||||
@@ -21,13 +21,13 @@ export async function init_monaco() {
|
||||
fetch(`/${file}`)
|
||||
.then((res) => res.text())
|
||||
.then((dts) =>
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(
|
||||
monaco.typescript.typescriptDefaults.addExtraLib(
|
||||
dts,
|
||||
`file:///${file}`
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
`file:///${file}`,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await Promise.all(promises);
|
||||
@@ -37,17 +37,17 @@ export async function init_monaco() {
|
||||
moduleDetection?: "force" | "auto" | "legacy" | 3 | 2 | 1; // string or numeric enum
|
||||
};
|
||||
|
||||
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
|
||||
module: monaco.languages.typescript.ModuleKind.ESNext,
|
||||
target: monaco.languages.typescript.ScriptTarget.ESNext,
|
||||
monaco.typescript.typescriptDefaults.setCompilerOptions({
|
||||
module: monaco.typescript.ModuleKind.ESNext,
|
||||
target: monaco.typescript.ScriptTarget.ESNext,
|
||||
allowNonTsExtensions: true,
|
||||
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
|
||||
moduleResolution: monaco.typescript.ModuleResolutionKind.NodeJs,
|
||||
typeRoots: ["index.d.ts"],
|
||||
allowTopLevelAwait: true,
|
||||
moduleDetection: "force",
|
||||
} as monaco.languages.typescript.CompilerOptions & Partial<ExtraOptions>);
|
||||
} as monaco.typescript.CompilerOptions & Partial<ExtraOptions>);
|
||||
|
||||
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
|
||||
monaco.typescript.typescriptDefaults.setDiagnosticsOptions({
|
||||
diagnosticCodesToIgnore: [
|
||||
// Allows top level await
|
||||
1375,
|
||||
|
||||
@@ -25,7 +25,7 @@ import { cn, usableResourcePath } from "@lib/utils";
|
||||
import { Types } from "komodo_client";
|
||||
import { RequiredResourceComponents, UsableResource } from "@types";
|
||||
import { DataTable, SortableHeader } from "@ui/data-table";
|
||||
import { AlertTriangle, Box, Circle, History } from "lucide-react";
|
||||
import { AlertTriangle, LayoutDashboard, Circle, History } from "lucide-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { UpdateAvailable as StackUpdateAvailable } from "@components/resources/stack";
|
||||
import { UpdateAvailable as DeploymentUpdateAvailable } from "@components/resources/deployment";
|
||||
@@ -40,7 +40,7 @@ export default function Dashboard() {
|
||||
<ActiveResources />
|
||||
<Page
|
||||
title="Dashboard"
|
||||
icon={<Box className="w-8 h-8" />}
|
||||
icon={<LayoutDashboard className="w-8 h-8" />}
|
||||
actions={
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<ShowTables />
|
||||
@@ -108,7 +108,7 @@ const TableSection = ({
|
||||
|
||||
const filtered = useFilterResources(
|
||||
resources as Types.ResourceListItem<unknown>[],
|
||||
search
|
||||
search,
|
||||
);
|
||||
|
||||
let count = filtered.length;
|
||||
@@ -164,7 +164,7 @@ const ResourceRow = ({ type }: { type: UsableResource }) => {
|
||||
const _recents = useUser().data?.recents?.[type]?.slice(0, 6);
|
||||
const _resources = useRead(`List${type}s`, {}).data;
|
||||
const recents = _recents?.filter(
|
||||
(recent) => !_resources?.every((resource) => resource.id !== recent)
|
||||
(recent) => !_resources?.every((resource) => resource.id !== recent),
|
||||
);
|
||||
const resources = _resources
|
||||
?.filter((r) => !recents?.includes(r.id))
|
||||
@@ -238,7 +238,7 @@ const RecentCard = ({
|
||||
className={cn(
|
||||
"w-full px-3 py-2 border rounded-md hover:bg-accent/25 hover:-translate-y-1 transition-all duration-1000 linear flex flex-col justify-between",
|
||||
showServerStats ? "min-h-32" : "h-20",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -256,7 +256,7 @@ const RecentCard = ({
|
||||
"overflow-hidden w-full transition-all duration-1000 linear",
|
||||
showServerStats
|
||||
? "max-h-40 opacity-100 py-2"
|
||||
: "max-h-0 opacity-0 py-0"
|
||||
: "max-h-0 opacity-0 py-0",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
@@ -274,7 +274,7 @@ const RecentCard = ({
|
||||
const ActiveResources = () => {
|
||||
const builds =
|
||||
useRead("ListBuilds", {}).data?.filter(
|
||||
(build) => build.info.state === Types.BuildState.Building
|
||||
(build) => build.info.state === Types.BuildState.Building,
|
||||
) ?? [];
|
||||
const repos =
|
||||
useRead("ListRepos", {}).data?.filter((repo) =>
|
||||
@@ -282,15 +282,15 @@ const ActiveResources = () => {
|
||||
Types.RepoState.Building,
|
||||
Types.RepoState.Cloning,
|
||||
Types.RepoState.Pulling,
|
||||
].includes(repo.info.state)
|
||||
].includes(repo.info.state),
|
||||
) ?? [];
|
||||
const procedures =
|
||||
useRead("ListProcedures", {}).data?.filter(
|
||||
(procedure) => procedure.info.state === Types.ProcedureState.Running
|
||||
(procedure) => procedure.info.state === Types.ProcedureState.Running,
|
||||
) ?? [];
|
||||
const actions =
|
||||
useRead("ListActions", {}).data?.filter(
|
||||
(action) => action.info.state === Types.ActionState.Running
|
||||
(action) => action.info.state === Types.ActionState.Running,
|
||||
) ?? [];
|
||||
const global_auto_updates =
|
||||
useRead("ListUpdates", {
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useRef, useState } from "react";
|
||||
import { ThemeToggle } from "@ui/theme";
|
||||
import { KOMODO_BASE_URL, sanitize_query } from "@main";
|
||||
import { KeyRound, Loader2, X } from "lucide-react";
|
||||
import { cn, preparePasskeyCredential } from "@lib/utils";
|
||||
import { cn } from "@lib/utils";
|
||||
import { MoghAuth } from "komodo_client";
|
||||
import { useToast } from "@ui/use-toast";
|
||||
|
||||
@@ -96,7 +96,7 @@ export default function Login({
|
||||
case "Passkey":
|
||||
setPasskeyPending(true);
|
||||
return navigator.credentials
|
||||
.get(preparePasskeyCredential(data))
|
||||
.get(MoghAuth.Passkey.prepareRequestChallengeResponse(data))
|
||||
.then((credential) => completePasskeyLogin({ credential }))
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
|
||||
@@ -2,8 +2,8 @@ import { ConfirmButton } from "@components/util";
|
||||
import { useManageAuth, useUserInvalidate } from "@lib/hooks";
|
||||
import { Button } from "@ui/button";
|
||||
import { Fingerprint, Trash } from "lucide-react";
|
||||
import { Types } from "komodo_client";
|
||||
import { base64urlToArrayBuffer, cn } from "@lib/utils";
|
||||
import { MoghAuth, Types } from "komodo_client";
|
||||
import { cn } from "@lib/utils";
|
||||
import { useToast } from "@ui/use-toast";
|
||||
|
||||
export const EnrollPasskey = ({ user }: { user: Types.User }) => {
|
||||
@@ -32,22 +32,8 @@ export const EnrollPasskey = ({ user }: { user: Types.User }) => {
|
||||
|
||||
const { mutate: begin_enrollment } = useManageAuth("BeginPasskeyEnrollment", {
|
||||
onSuccess: (challenge) => {
|
||||
const formatted_challenge = {
|
||||
...challenge,
|
||||
publicKey: {
|
||||
...challenge.publicKey,
|
||||
challenge: base64urlToArrayBuffer(challenge.publicKey.challenge),
|
||||
user: {
|
||||
...challenge.publicKey.user,
|
||||
id: base64urlToArrayBuffer(challenge.publicKey.user.id),
|
||||
},
|
||||
excludeCredentials: challenge.publicKey.excludeCredentials?.map(
|
||||
(cred: any) => ({ ...cred, id: base64urlToArrayBuffer(cred.id) }),
|
||||
),
|
||||
},
|
||||
};
|
||||
navigator.credentials
|
||||
.create(formatted_challenge)
|
||||
.create(MoghAuth.Passkey.prepareCreationChallengeResponse(challenge))
|
||||
.then((credential) => confirm_enrollment({ credential }))
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
export default {
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
"./pages/**/*.{ts,tsx}",
|
||||
@@ -250,5 +250,5 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
plugins: [import("tailwindcss-animate")],
|
||||
};
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { defineConfig } from "vite";
|
||||
import tspaths from "vite-tsconfig-paths";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config({ path: ".env.development" });
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tspaths()],
|
||||
server: { allowedHosts: [process.env.ALLOWED_HOST] },
|
||||
});
|
||||
|
||||
+1072
-747
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user