mirror of
https://github.com/moghtech/komodo.git
synced 2026-11-08 01:54:21 -06:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6aaef24cd | ||
|
|
3dce84c1f5 | ||
|
|
8b16f97caf | ||
|
|
c98a809db4 | ||
|
|
c066b4fe16 | ||
|
|
b54fa235bc | ||
|
|
05fd1aba2c |
Generated
+17
-15
@@ -1131,7 +1131,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "command"
|
||||
version = "2.0.0-dev-112"
|
||||
version = "2.0.0-dev-114"
|
||||
dependencies = [
|
||||
"komodo_client",
|
||||
"shlex",
|
||||
@@ -1462,7 +1462,7 @@ checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
|
||||
|
||||
[[package]]
|
||||
name = "database"
|
||||
version = "2.0.0-dev-112"
|
||||
version = "2.0.0-dev-114"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-compression",
|
||||
@@ -1732,7 +1732,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "encoding"
|
||||
version = "2.0.0-dev-112"
|
||||
version = "2.0.0-dev-114"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -1774,7 +1774,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "environment"
|
||||
version = "2.0.0-dev-112"
|
||||
version = "2.0.0-dev-114"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"formatting",
|
||||
@@ -1903,7 +1903,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "formatting"
|
||||
version = "2.0.0-dev-112"
|
||||
version = "2.0.0-dev-114"
|
||||
dependencies = [
|
||||
"mogh_error",
|
||||
]
|
||||
@@ -2069,7 +2069,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "git"
|
||||
version = "2.0.0-dev-112"
|
||||
version = "2.0.0-dev-114"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"command",
|
||||
@@ -2663,7 +2663,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "interpolate"
|
||||
version = "2.0.0-dev-112"
|
||||
version = "2.0.0-dev-114"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"komodo_client",
|
||||
@@ -2789,7 +2789,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_cli"
|
||||
version = "2.0.0-dev-112"
|
||||
version = "2.0.0-dev-114"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -2817,7 +2817,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_client"
|
||||
version = "2.0.0-dev-112"
|
||||
version = "2.0.0-dev-114"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -2856,7 +2856,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_core"
|
||||
version = "2.0.0-dev-112"
|
||||
version = "2.0.0-dev-114"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -2929,7 +2929,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_periphery"
|
||||
version = "2.0.0-dev-112"
|
||||
version = "2.0.0-dev-114"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -3341,13 +3341,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mogh_server"
|
||||
version = "1.1.2"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c6c3aa20123c38b4f2cb63f42099352f9f4003ff72e744f26f4cd72023c4274"
|
||||
checksum = "ffd2b7646dbaa1a09b3df6782f1692e459775a6fb46fb2d613d43ae5ca1a9dcb"
|
||||
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-112"
|
||||
version = "2.0.0-dev-114"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"encoding",
|
||||
@@ -5881,7 +5883,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "transport"
|
||||
version = "2.0.0-dev-112"
|
||||
version = "2.0.0-dev-114"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "2.0.0-dev-112"
|
||||
version = "2.0.0-dev-114"
|
||||
edition = "2024"
|
||||
authors = ["mbecker20 <becker.maxh@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
@@ -46,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.3.0"
|
||||
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
|
||||
|
||||
@@ -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,
|
||||
@@ -29,12 +30,15 @@ use crate::{
|
||||
api::execute::{self, ExecuteRequest},
|
||||
helpers::{
|
||||
periphery_client,
|
||||
query::get_deployment_state,
|
||||
query::{get_deployment_state, get_swarm_or_server},
|
||||
registry_token,
|
||||
update::{add_update, make_update},
|
||||
},
|
||||
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,14 +424,17 @@ 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(false)
|
||||
return Ok(CheckDeploymentForUpdateResponse {
|
||||
deployment: deployment.id,
|
||||
update_available: false,
|
||||
});
|
||||
}
|
||||
let domain = extract_registry_domain(&image)?;
|
||||
let account =
|
||||
@@ -440,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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -456,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,
|
||||
}),
|
||||
@@ -504,15 +528,15 @@ pub async fn check_deployment_for_update_inner(
|
||||
resolved: true,
|
||||
resolved_ts: ts.into(),
|
||||
level: SeverityLevel::Ok,
|
||||
target: ResourceTarget::Deployment(deployment.id.clone()),
|
||||
target: ResourceTarget::Deployment(id.clone()),
|
||||
data: AlertData::DeploymentAutoUpdated {
|
||||
id: deployment.id.clone(),
|
||||
name: deployment.name.clone(),
|
||||
id,
|
||||
name,
|
||||
swarm_id,
|
||||
swarm_name,
|
||||
server_id,
|
||||
server_name,
|
||||
image: image.clone(),
|
||||
image,
|
||||
},
|
||||
};
|
||||
let res = db_client().alerts.insert_one(&alert).await;
|
||||
@@ -524,10 +548,7 @@ pub async fn check_deployment_for_update_inner(
|
||||
send_alerts(&[alert]).await;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Failed to auto update Deployment {} | {e:#}",
|
||||
deployment.name
|
||||
)
|
||||
warn!("Failed to auto update Deployment {name} | {e:#}",)
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -540,7 +561,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();
|
||||
@@ -572,5 +596,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),
|
||||
|
||||
@@ -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::{
|
||||
@@ -40,7 +41,7 @@ use crate::{
|
||||
update::{add_update, make_update},
|
||||
},
|
||||
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(),
|
||||
@@ -790,8 +790,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,
|
||||
@@ -799,15 +799,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()
|
||||
@@ -817,7 +819,7 @@ pub async fn check_stack_for_update_inner(
|
||||
update_available: false,
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
let mut services = Vec::new();
|
||||
@@ -829,7 +831,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;
|
||||
};
|
||||
@@ -848,7 +850,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)
|
||||
@@ -905,7 +907,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'
|
||||
@@ -928,6 +933,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 {
|
||||
@@ -979,5 +986,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)
|
||||
}
|
||||
}
|
||||
|
||||
+15
-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,6 @@ 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),
|
||||
resource_poll_interval: env
|
||||
.komodo_resource_poll_interval
|
||||
.unwrap_or(config.resource_poll_interval),
|
||||
@@ -412,6 +400,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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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`
|
||||
@@ -279,6 +272,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 +413,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,
|
||||
@@ -701,6 +701,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 +808,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(),
|
||||
@@ -843,6 +852,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 +879,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,
|
||||
@@ -973,6 +980,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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
+91
-12
@@ -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. */
|
||||
@@ -5373,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;
|
||||
@@ -5846,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 {
|
||||
/**
|
||||
@@ -10864,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 }
|
||||
@@ -10871,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 }
|
||||
|
||||
@@ -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 . .
|
||||
|
||||
+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
+88
-9
@@ -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. */
|
||||
@@ -5251,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;
|
||||
@@ -5649,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 {
|
||||
/**
|
||||
@@ -10749,6 +10822,9 @@ export type WriteRequest = {
|
||||
} | {
|
||||
type: "CheckStackForUpdate";
|
||||
params: CheckStackForUpdate;
|
||||
} | {
|
||||
type: "BatchCheckStackForUpdate";
|
||||
params: BatchCheckStackForUpdate;
|
||||
} | {
|
||||
type: "CreateDeployment";
|
||||
params: CreateDeployment;
|
||||
@@ -10770,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",
|
||||
|
||||
Reference in New Issue
Block a user