Compare commits

...
33 changed files with 703 additions and 178 deletions
Generated
+17 -15
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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
+1
View File
@@ -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,
+4 -1
View File
@@ -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))
}
+122 -34
View File
@@ -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(&current_digest)
|| !latest_digest.update_available(&current_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)
}
}
+2
View File
@@ -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),
+88 -16
View File
@@ -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(&current_digest.0);
latest_digest.update_available(&current_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
View File
@@ -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,
+1 -1
View File
@@ -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,
}
+8 -9
View File
@@ -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,
}
+8 -5
View File
@@ -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 {
+3 -3
View File
@@ -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(&current_digest.0)
.update_available(current_digests)
.into()
} else {
None
+2 -1
View File
@@ -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 -1
View File
@@ -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
+9 -10
View File
@@ -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,
+59 -2
View File
@@ -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>;
+64 -2
View File
@@ -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>;
//
+29 -17
View File
@@ -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,
}
}
+14 -6
View File
@@ -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.
+50 -8
View File
@@ -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 {
+2 -2
View File
@@ -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]
+2
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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
View File
@@ -1,4 +1,4 @@
FROM rust:1.91.1-bullseye as builder
FROM rust:1.93.0-bullseye as builder
WORKDIR /builder
COPY . .
+2
View File
@@ -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;
+88 -9
View File
@@ -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;
+12 -5
View File
@@ -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",