Compare commits

...

5 Commits

Author SHA1 Message Date
Maxwell Becker
aea5441466 1.16.9 (#172)
* BatchDestroyDeployment

* periphery image pull api

* Add Pull apis

* Add PullStack / PullDeployment

* improve init deploy from container

* stacks + deployments update_available source

* Fix deploy / destroy stack service

* updates available indicator

* add poll for updates and auto update options

* use interval to handle waiting between resource refresh

* stack auto update deploy whole stack

* format

* clean up the docs

* update available alerts

* update alerting format

* fix most clippy
2024-11-04 20:28:31 -08:00
mbecker20
97ced3b2cb frontend allow Alerter configure StackStateChanged, include Stacks and Repos in whitelist 2024-11-02 21:00:38 -04:00
Maxwell Becker
1f79987c58 1.16.8 (#170)
* update configs

* bump to 1.16.8
2024-11-01 14:35:12 -07:00
Maxwell Becker
e859a919c5 1.16.8 (#169)
* use this to extract from path

* Fix references to __ALL__
2024-11-01 14:33:41 -07:00
mbecker20
2a1270dd74 webhook check will return better status codex 2024-11-01 15:57:36 -04:00
94 changed files with 2478 additions and 720 deletions

35
Cargo.lock generated
View File

@@ -41,7 +41,7 @@ dependencies = [
[[package]]
name = "alerter"
version = "1.16.7"
version = "1.16.9"
dependencies = [
"anyhow",
"axum",
@@ -845,6 +845,14 @@ dependencies = [
"either",
]
[[package]]
name = "cache"
version = "1.16.9"
dependencies = [
"anyhow",
"tokio",
]
[[package]]
name = "cc"
version = "1.1.30"
@@ -943,7 +951,7 @@ dependencies = [
[[package]]
name = "command"
version = "1.16.7"
version = "1.16.9"
dependencies = [
"komodo_client",
"run_command",
@@ -1355,7 +1363,7 @@ dependencies = [
[[package]]
name = "environment_file"
version = "1.16.7"
version = "1.16.9"
dependencies = [
"thiserror",
]
@@ -1439,7 +1447,7 @@ dependencies = [
[[package]]
name = "formatting"
version = "1.16.7"
version = "1.16.9"
dependencies = [
"serror",
]
@@ -1571,9 +1579,10 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "git"
version = "1.16.7"
version = "1.16.9"
dependencies = [
"anyhow",
"cache",
"command",
"formatting",
"komodo_client",
@@ -2191,7 +2200,7 @@ dependencies = [
[[package]]
name = "komodo_cli"
version = "1.16.7"
version = "1.16.9"
dependencies = [
"anyhow",
"clap",
@@ -2207,7 +2216,7 @@ dependencies = [
[[package]]
name = "komodo_client"
version = "1.16.7"
version = "1.16.9"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2238,7 +2247,7 @@ dependencies = [
[[package]]
name = "komodo_core"
version = "1.16.7"
version = "1.16.9"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2249,6 +2258,7 @@ dependencies = [
"axum-server",
"base64 0.22.1",
"bcrypt",
"cache",
"command",
"dashmap",
"derive_variants",
@@ -2297,7 +2307,7 @@ dependencies = [
[[package]]
name = "komodo_periphery"
version = "1.16.7"
version = "1.16.9"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2305,6 +2315,7 @@ dependencies = [
"axum-extra",
"axum-server",
"bollard",
"cache",
"clap",
"command",
"derive_variants",
@@ -2384,7 +2395,7 @@ dependencies = [
[[package]]
name = "logger"
version = "1.16.7"
version = "1.16.9"
dependencies = [
"anyhow",
"komodo_client",
@@ -3090,7 +3101,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "periphery_client"
version = "1.16.7"
version = "1.16.9"
dependencies = [
"anyhow",
"komodo_client",
@@ -4865,7 +4876,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "update_logger"
version = "1.16.7"
version = "1.16.9"
dependencies = [
"anyhow",
"komodo_client",

View File

@@ -9,7 +9,7 @@ members = [
]
[workspace.package]
version = "1.16.7"
version = "1.16.9"
edition = "2021"
authors = ["mbecker20 <becker.maxh@gmail.com>"]
license = "GPL-3.0-or-later"
@@ -28,6 +28,7 @@ environment_file = { path = "lib/environment_file" }
formatting = { path = "lib/formatting" }
command = { path = "lib/command" }
logger = { path = "lib/logger" }
cache = { path = "lib/cache" }
git = { path = "lib/git" }
# MOGH

View File

@@ -56,6 +56,9 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
Execution::BatchDeploy(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PullDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StartDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
@@ -179,6 +182,9 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
Execution::BatchDeployStackIfChanged(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PullStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StartStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
@@ -215,231 +221,239 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
Execution::RunAction(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::BatchRunAction(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
.map(ExecutionResult::Batch),
Execution::RunProcedure(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::BatchRunProcedure(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
.map(ExecutionResult::Batch),
Execution::RunBuild(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::BatchRunBuild(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
.map(ExecutionResult::Batch),
Execution::CancelBuild(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::Deploy(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::BatchDeploy(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
.map(ExecutionResult::Batch),
Execution::PullDeployment(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::StartDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::RestartDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::PauseDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::UnpauseDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::StopDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::DestroyDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::BatchDestroyDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
.map(ExecutionResult::Batch),
Execution::CloneRepo(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::BatchCloneRepo(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
.map(ExecutionResult::Batch),
Execution::PullRepo(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::BatchPullRepo(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
.map(ExecutionResult::Batch),
Execution::BuildRepo(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::BatchBuildRepo(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
.map(ExecutionResult::Batch),
Execution::CancelRepoBuild(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::StartContainer(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::RestartContainer(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::PauseContainer(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::UnpauseContainer(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::StopContainer(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::DestroyContainer(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::StartAllContainers(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::RestartAllContainers(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::PauseAllContainers(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::UnpauseAllContainers(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::StopAllContainers(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::PruneContainers(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::DeleteNetwork(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::PruneNetworks(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::DeleteImage(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::PruneImages(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::DeleteVolume(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::PruneVolumes(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::PruneDockerBuilders(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::PruneBuildx(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::PruneSystem(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::RunSync(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::CommitSync(request) => komodo_client()
.write(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::DeployStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::BatchDeployStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
.map(ExecutionResult::Batch),
Execution::DeployStackIfChanged(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::BatchDeployStackIfChanged(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
.map(ExecutionResult::Batch),
Execution::PullStack(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::StartStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::RestartStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::PauseStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::UnpauseStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::StopStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::DestroyStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(ExecutionResult::Single),
Execution::BatchDestroyStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
.map(ExecutionResult::Batch),
Execution::Sleep(request) => {
let duration =
Duration::from_millis(request.duration_ms as u64);

View File

@@ -21,6 +21,7 @@ environment_file.workspace = true
formatting.workspace = true
command.workspace = true
logger.workspace = true
cache.workspace = true
git.workspace = true
# mogh
serror = { workspace = true, features = ["axum"] }

View File

@@ -22,7 +22,7 @@ pub async fn send_alert(
match alert.level {
SeverityLevel::Ok => {
format!(
"{level} | *{name}*{region} is now *reachable*\n{link}"
"{level} | **{name}**{region} is now **reachable**\n{link}"
)
}
SeverityLevel::Critical => {
@@ -31,7 +31,7 @@ pub async fn send_alert(
.map(|e| format!("\n**error**: {e:#?}"))
.unwrap_or_default();
format!(
"{level} | *{name}*{region} is *unreachable* ❌\n{link}{err}"
"{level} | **{name}**{region} is **unreachable**\n{link}{err}"
)
}
_ => unreachable!(),
@@ -46,7 +46,7 @@ pub async fn send_alert(
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
format!(
"{level} | *{name}*{region} cpu usage at *{percentage:.1}%*\n{link}"
"{level} | **{name}**{region} cpu usage at **{percentage:.1}%**\n{link}"
)
}
AlertData::ServerMem {
@@ -60,7 +60,7 @@ pub async fn send_alert(
let link = resource_link(ResourceTargetVariant::Server, id);
let percentage = 100.0 * used_gb / total_gb;
format!(
"{level} | *{name}*{region} memory usage at *{percentage:.1}%* 💾\n\nUsing *{used_gb:.1} GiB* / *{total_gb:.1} GiB*\n{link}"
"{level} | **{name}**{region} memory usage at **{percentage:.1}%** 💾\n\nUsing **{used_gb:.1} GiB** / **{total_gb:.1} GiB**\n{link}"
)
}
AlertData::ServerDisk {
@@ -75,7 +75,7 @@ pub async fn send_alert(
let link = resource_link(ResourceTargetVariant::Server, id);
let percentage = 100.0 * used_gb / total_gb;
format!(
"{level} | *{name}*{region} disk usage at *{percentage:.1}%* 💿\nmount point: `{path:?}`\nusing *{used_gb:.1} GiB* / *{total_gb:.1} GiB*\n{link}"
"{level} | **{name}**{region} disk usage at **{percentage:.1}%** 💿\nmount point: `{path:?}`\nusing **{used_gb:.1} GiB** / **{total_gb:.1} GiB**\n{link}"
)
}
AlertData::ContainerStateChange {
@@ -88,7 +88,17 @@ pub async fn send_alert(
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
let to = fmt_docker_container_state(to);
format!("📦 Deployment *{name}* is now {to}\nserver: {server_name}\nprevious: {from}\n{link}")
format!("📦 Deployment **{name}** is now **{to}**\nserver: **{server_name}**\nprevious: **{from}**\n{link}")
}
AlertData::DeploymentImageUpdateAvailable {
id,
name,
server_id: _server_id,
server_name,
image,
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
format!("⬆ Deployment **{name}** has an update available\nserver: **{server_name}**\nimage: **{image}**\n{link}")
}
AlertData::StackStateChange {
id,
@@ -100,28 +110,39 @@ pub async fn send_alert(
} => {
let link = resource_link(ResourceTargetVariant::Stack, id);
let to = fmt_stack_state(to);
format!("🥞 Stack *{name}* is now {to}\nserver: {server_name}\nprevious: {from}\n{link}")
format!("🥞 Stack **{name}** is now {to}\nserver: **{server_name}**\nprevious: **{from}**\n{link}")
}
AlertData::StackImageUpdateAvailable {
id,
name,
server_id: _server_id,
server_name,
service,
image,
} => {
let link = resource_link(ResourceTargetVariant::Stack, id);
format!("⬆ Stack **{name}** has an update available\nserver: **{server_name}**\nservice: **{service}**\nimage: **{image}**\n{link}")
}
AlertData::AwsBuilderTerminationFailed {
instance_id,
message,
} => {
format!("{level} | Failed to terminated AWS builder instance\ninstance id: *{instance_id}*\n{message}")
format!("{level} | Failed to terminated AWS builder instance\ninstance id: **{instance_id}**\n{message}")
}
AlertData::ResourceSyncPendingUpdates { id, name } => {
let link =
resource_link(ResourceTargetVariant::ResourceSync, id);
format!(
"{level} | Pending resource sync updates on *{name}*\n{link}"
"{level} | Pending resource sync updates on **{name}**\n{link}"
)
}
AlertData::BuildFailed { id, name, version } => {
let link = resource_link(ResourceTargetVariant::Build, id);
format!("{level} | Build *{name}* failed\nversion: v{version}\n{link}")
format!("{level} | Build **{name}** failed\nversion: **v{version}**\n{link}")
}
AlertData::RepoBuildFailed { id, name } => {
let link = resource_link(ResourceTargetVariant::Repo, id);
format!("{level} | Repo build for *{name}* failed\n{link}")
format!("{level} | Repo build for **{name}** failed\n{link}")
}
AlertData::None {} => Default::default(),
};

View File

@@ -182,7 +182,7 @@ pub async fn send_alert(
..
} => {
let to = fmt_docker_container_state(to);
let text = format!("📦 Container *{name}* is now {to}");
let text = format!("📦 Container *{name}* is now *{to}*");
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
@@ -195,6 +195,27 @@ pub async fn send_alert(
];
(text, blocks.into())
}
AlertData::DeploymentImageUpdateAvailable {
id,
name,
server_name,
server_id: _server_id,
image,
} => {
let text =
format!("⬆ Deployment *{name}* has an update available");
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
"server: *{server_name}*\nimage: *{image}*",
)),
Block::section(resource_link(
ResourceTargetVariant::Deployment,
id,
)),
];
(text, blocks.into())
}
AlertData::StackStateChange {
name,
server_name,
@@ -204,11 +225,32 @@ pub async fn send_alert(
..
} => {
let to = fmt_stack_state(to);
let text = format!("🥞 Stack *{name}* is now {to}");
let text = format!("🥞 Stack *{name}* is now *{to}*");
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
"server: {server_name}\nprevious: {from}",
"server: *{server_name}*\nprevious: *{from}*",
)),
Block::section(resource_link(
ResourceTargetVariant::Stack,
id,
)),
];
(text, blocks.into())
}
AlertData::StackImageUpdateAvailable {
id,
name,
server_name,
server_id: _server_id,
service,
image,
} => {
let text = format!("⬆ Stack *{name}* has an update available");
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
"server: *{server_name}*\nservice: *{service}*\nimage: *{image}*",
)),
Block::section(resource_link(
ResourceTargetVariant::Stack,
@@ -233,8 +275,9 @@ pub async fn send_alert(
(text, blocks.into())
}
AlertData::ResourceSyncPendingUpdates { id, name } => {
let text =
format!("{level} | Pending resource sync updates on {name}");
let text = format!(
"{level} | Pending resource sync updates on *{name}*"
);
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
@@ -252,20 +295,21 @@ pub async fn send_alert(
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
"build id: *{id}*\nbuild name: *{name}*\nversion: v{version}",
"build name: *{name}*\nversion: *v{version}*",
)),
Block::section(resource_link(
ResourceTargetVariant::Build,
id,
)),
Block::section(resource_link(ResourceTargetVariant::Build, id))
];
(text, blocks.into())
}
AlertData::RepoBuildFailed { id, name } => {
let text =
format!("{level} | Repo build for {name} has failed");
format!("{level} | Repo build for *{name}* has *failed*");
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
"repo id: *{id}*\nrepo name: *{name}*",
)),
Block::section(format!("repo name: *{name}*",)),
Block::section(resource_link(
ResourceTargetVariant::Repo,
id,

View File

@@ -1,6 +1,7 @@
use std::collections::HashSet;
use std::{collections::HashSet, sync::OnceLock};
use anyhow::{anyhow, Context};
use cache::TimeoutCache;
use formatting::format_serror;
use komodo_client::{
api::execute::*,
@@ -9,7 +10,7 @@ use komodo_client::{
deployment::{
extract_registry_domain, Deployment, DeploymentImage,
},
get_image_name,
get_image_name, komodo_timestamp, optional_string,
permission::PermissionLevel,
server::Server,
update::{Log, Update},
@@ -73,12 +74,16 @@ async fn setup_deployment_execution(
.await?;
if deployment.config.server_id.is_empty() {
return Err(anyhow!("deployment has no server configured"));
return Err(anyhow!("Deployment has no Server configured"));
}
let server =
resource::get::<Server>(&deployment.config.server_id).await?;
if !server.config.enabled {
return Err(anyhow!("Attached Server is not enabled"));
}
Ok((deployment, server))
}
@@ -110,13 +115,6 @@ impl Resolve<Deploy, (User, Update)> for State {
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
periphery
.health_check()
.await
.context("Failed server health check, stopping run.")?;
// This block resolves the attached Build to an actual versioned image
let (version, registry_token) = match &deployment.config.image {
DeploymentImage::Build { build_id, version } => {
@@ -128,12 +126,7 @@ impl Resolve<Deploy, (User, Update)> for State {
} else {
*version
};
// Remove ending patch if it is 0, this means use latest patch.
let version_str = if version.patch == 0 {
format!("{}.{}", version.major, version.minor)
} else {
version.to_string()
};
let version_str = version.to_string();
// Potentially add the build image_tag postfix
let version_str = if build.config.image_tag.is_empty() {
version_str
@@ -241,7 +234,7 @@ impl Resolve<Deploy, (User, Update)> for State {
update.version = version;
update_update(update.clone()).await?;
match periphery
match periphery_client(&server)?
.request(api::container::Deploy {
deployment,
stop_signal,
@@ -254,10 +247,8 @@ impl Resolve<Deploy, (User, Update)> for State {
Ok(log) => update.logs.push(log),
Err(e) => {
update.push_error_log(
"deploy container",
format_serror(
&e.context("failed to deploy container").into(),
),
"Deploy Container",
format_serror(&e.into()),
);
}
};
@@ -271,6 +262,155 @@ impl Resolve<Deploy, (User, Update)> for State {
}
}
/// Wait this long after a pull to allow another pull through
const PULL_TIMEOUT: i64 = 5_000;
type ServerId = String;
type Image = String;
type PullCache = TimeoutCache<(ServerId, Image), Log>;
fn pull_cache() -> &'static PullCache {
static PULL_CACHE: OnceLock<PullCache> = OnceLock::new();
PULL_CACHE.get_or_init(Default::default)
}
pub async fn pull_deployment_inner(
deployment: Deployment,
server: &Server,
) -> anyhow::Result<Log> {
let (image, account, token) = match deployment.config.image {
DeploymentImage::Build { build_id, version } => {
let build = resource::get::<Build>(&build_id).await?;
let image_name = get_image_name(&build)
.context("failed to create image name")?;
let version = if version.is_none() {
build.config.version.to_string()
} else {
version.to_string()
};
// Potentially add the build image_tag postfix
let version = if build.config.image_tag.is_empty() {
version
} else {
format!("{version}-{}", build.config.image_tag)
};
// replace image with corresponding build image.
let image = format!("{image_name}:{version}");
if build.config.image_registry.domain.is_empty() {
(image, None, None)
} else {
let ImageRegistryConfig {
domain, account, ..
} = build.config.image_registry;
let account =
if deployment.config.image_registry_account.is_empty() {
account
} else {
deployment.config.image_registry_account
};
let token = if !account.is_empty() {
registry_token(&domain, &account).await.with_context(
|| format!("Failed to get git token in call to db. Stopping run. | {domain} | {account}"),
)?
} else {
None
};
(image, optional_string(&account), token)
}
}
DeploymentImage::Image { image } => {
let domain = extract_registry_domain(&image)?;
let token = if !deployment
.config
.image_registry_account
.is_empty()
{
registry_token(&domain, &deployment.config.image_registry_account).await.with_context(
|| format!("Failed to get git token in call to db. Stopping run. | {domain} | {}", deployment.config.image_registry_account),
)?
} else {
None
};
(
image,
optional_string(&deployment.config.image_registry_account),
token,
)
}
};
// Acquire the pull lock for this image on the server
let lock = pull_cache()
.get_lock((server.id.clone(), image.clone()))
.await;
// Lock the path lock, prevents simultaneous pulls by
// ensuring simultaneous pulls will wait for first to finish
// and checking cached results.
let mut locked = lock.lock().await;
// Early return from cache if lasted pulled with PULL_TIMEOUT
if locked.last_ts + PULL_TIMEOUT > komodo_timestamp() {
return locked.clone_res();
}
let res = async {
let log = match periphery_client(server)?
.request(api::image::PullImage {
name: image,
account,
token,
})
.await
{
Ok(log) => log,
Err(e) => Log::error("Pull image", format_serror(&e.into())),
};
update_cache_for_server(server).await;
anyhow::Ok(log)
}
.await;
// Set the cache with results. Any other calls waiting on the lock will
// then immediately also use this same result.
locked.set(&res, komodo_timestamp());
res
}
impl Resolve<PullDeployment, (User, Update)> for State {
async fn resolve(
&self,
PullDeployment { deployment }: PullDeployment,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let (deployment, server) =
setup_deployment_execution(&deployment, &user).await?;
// get the action state for the deployment (or insert default).
let action_state = action_states()
.deployment
.get_or_insert_default(&deployment.id)
.await;
// Will check to ensure deployment not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let _action_guard =
action_state.update(|state| state.pulling = true)?;
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let log = pull_deployment_inner(deployment, &server).await?;
update.logs.push(log);
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}
impl Resolve<StartDeployment, (User, Update)> for State {
#[instrument(name = "StartDeployment", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
@@ -295,9 +435,7 @@ impl Resolve<StartDeployment, (User, Update)> for State {
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let log = match periphery
let log = match periphery_client(&server)?
.request(api::container::StartContainer {
name: deployment.name,
})
@@ -343,9 +481,7 @@ impl Resolve<RestartDeployment, (User, Update)> for State {
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let log = match periphery
let log = match periphery_client(&server)?
.request(api::container::RestartContainer {
name: deployment.name,
})
@@ -393,9 +529,7 @@ impl Resolve<PauseDeployment, (User, Update)> for State {
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let log = match periphery
let log = match periphery_client(&server)?
.request(api::container::PauseContainer {
name: deployment.name,
})
@@ -441,9 +575,7 @@ impl Resolve<UnpauseDeployment, (User, Update)> for State {
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let log = match periphery
let log = match periphery_client(&server)?
.request(api::container::UnpauseContainer {
name: deployment.name,
})
@@ -495,9 +627,7 @@ impl Resolve<StopDeployment, (User, Update)> for State {
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let log = match periphery
let log = match periphery_client(&server)?
.request(api::container::StopContainer {
name: deployment.name,
signal: signal
@@ -525,6 +655,29 @@ impl Resolve<StopDeployment, (User, Update)> for State {
}
}
impl super::BatchExecute for BatchDestroyDeployment {
type Resource = Deployment;
fn single_request(deployment: String) -> ExecuteRequest {
ExecuteRequest::DestroyDeployment(DestroyDeployment {
deployment,
signal: None,
time: None,
})
}
}
impl Resolve<BatchDestroyDeployment, (User, Update)> for State {
#[instrument(name = "BatchDestroyDeployment", skip(self, user), fields(user_id = user.id))]
async fn resolve(
&self,
BatchDestroyDeployment { pattern }: BatchDestroyDeployment,
(user, _): (User, Update),
) -> anyhow::Result<BatchExecutionResponse> {
super::batch_execute::<BatchDestroyDeployment>(&pattern, &user)
.await
}
}
impl Resolve<DestroyDeployment, (User, Update)> for State {
#[instrument(name = "DestroyDeployment", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
@@ -553,9 +706,7 @@ impl Resolve<DestroyDeployment, (User, Update)> for State {
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let log = match periphery
let log = match periphery_client(&server)?
.request(api::container::RemoveContainer {
name: deployment.name,
signal: signal

View File

@@ -38,6 +38,10 @@ mod server_template;
mod stack;
mod sync;
pub use {
deployment::pull_deployment_inner, stack::pull_stack_inner,
};
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolver, EnumVariants,
@@ -73,18 +77,21 @@ pub enum ExecuteRequest {
// ==== DEPLOYMENT ====
Deploy(Deploy),
BatchDeploy(BatchDeploy),
PullDeployment(PullDeployment),
StartDeployment(StartDeployment),
RestartDeployment(RestartDeployment),
PauseDeployment(PauseDeployment),
UnpauseDeployment(UnpauseDeployment),
StopDeployment(StopDeployment),
DestroyDeployment(DestroyDeployment),
BatchDestroyDeployment(BatchDestroyDeployment),
// ==== STACK ====
DeployStack(DeployStack),
BatchDeployStack(BatchDeployStack),
DeployStackIfChanged(DeployStackIfChanged),
BatchDeployStackIfChanged(BatchDeployStackIfChanged),
PullStack(PullStack),
StartStack(StartStack),
RestartStack(RestartStack),
StopStack(StopStack),
@@ -140,13 +147,13 @@ async fn handler(
Ok((TypedHeader(ContentType::json()), res))
}
enum ExecutionResult {
pub enum ExecutionResult {
Single(Update),
/// The batch contents will be pre serialized here
Batch(String),
}
async fn inner_handler(
pub async fn inner_handler(
request: ExecuteRequest,
user: User,
) -> anyhow::Result<ExecutionResult> {
@@ -254,9 +261,9 @@ async fn batch_execute<E: BatchExecute>(
user: &User,
) -> anyhow::Result<BatchExecutionResponse> {
let resources = list_full_for_user_using_pattern::<E::Resource>(
&pattern,
pattern,
Default::default(),
&user,
user,
&[],
)
.await?;

View File

@@ -6,8 +6,9 @@ use komodo_client::{
api::{execute::*, write::RefreshStackCache},
entities::{
permission::PermissionLevel,
server::Server,
stack::{Stack, StackInfo},
update::Update,
update::{Log, Update},
user::User,
},
};
@@ -43,6 +44,7 @@ impl super::BatchExecute for BatchDeployStack {
fn single_request(stack: String) -> ExecuteRequest {
ExecuteRequest::DeployStack(DeployStack {
stack,
service: None,
stop_time: None,
})
}
@@ -63,7 +65,11 @@ impl Resolve<DeployStack, (User, Update)> for State {
#[instrument(name = "DeployStack", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
DeployStack { stack, stop_time }: DeployStack,
DeployStack {
stack,
service,
stop_time,
}: DeployStack,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let (mut stack, server) = get_stack_and_server(
@@ -85,6 +91,13 @@ impl Resolve<DeployStack, (User, Update)> for State {
update_update(update.clone()).await?;
if let Some(service) = &service {
update.logs.push(Log::simple(
&format!("Service: {service}"),
format!("Execution requested for Stack service {service}"),
))
}
let git_token = crate::helpers::git_token(
&stack.config.git_provider,
&stack.config.git_account,
@@ -158,7 +171,7 @@ impl Resolve<DeployStack, (User, Update)> for State {
} = periphery_client(&server)?
.request(ComposeUp {
stack: stack.clone(),
service: None,
service,
git_token,
registry_token,
replacers: secret_replacers.into_iter().collect(),
@@ -292,6 +305,7 @@ impl Resolve<BatchDeployStackIfChanged, (User, Update)> for State {
}
impl Resolve<DeployStackIfChanged, (User, Update)> for State {
#[instrument(name = "DeployStackIfChanged", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
DeployStackIfChanged { stack, stop_time }: DeployStackIfChanged,
@@ -354,6 +368,7 @@ impl Resolve<DeployStackIfChanged, (User, Update)> for State {
.resolve(
DeployStack {
stack: stack.name,
service: None,
stop_time,
},
(user, update),
@@ -362,6 +377,87 @@ impl Resolve<DeployStackIfChanged, (User, Update)> for State {
}
}
pub async fn pull_stack_inner(
mut stack: Stack,
service: Option<String>,
server: &Server,
update: Option<&mut Update>,
) -> anyhow::Result<ComposePullResponse> {
if let (Some(service), Some(update)) = (&service, update) {
update.logs.push(Log::simple(
&format!("Service: {service}"),
format!("Execution requested for Stack service {service}"),
))
}
let git_token = crate::helpers::git_token(
&stack.config.git_provider,
&stack.config.git_account,
|https| stack.config.git_https = https,
).await.with_context(
|| format!("Failed to get git token in call to db. Stopping run. | {} | {}", stack.config.git_provider, stack.config.git_account),
)?;
let registry_token = crate::helpers::registry_token(
&stack.config.registry_provider,
&stack.config.registry_account,
).await.with_context(
|| format!("Failed to get registry token in call to db. Stopping run. | {} | {}", stack.config.registry_provider, stack.config.registry_account),
)?;
let res = periphery_client(server)?
.request(ComposePull {
stack,
service,
git_token,
registry_token,
})
.await?;
// Ensure cached stack state up to date by updating server cache
update_cache_for_server(server).await;
Ok(res)
}
impl Resolve<PullStack, (User, Update)> for State {
#[instrument(name = "PullStack", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
PullStack { stack, service }: PullStack,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let (stack, server) = get_stack_and_server(
&stack,
&user,
PermissionLevel::Execute,
true,
)
.await?;
// get the action state for the stack (or insert default).
let action_state =
action_states().stack.get_or_insert_default(&stack.id).await;
// Will check to ensure stack not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let _action_guard =
action_state.update(|state| state.pulling = true)?;
update_update(update.clone()).await?;
let res =
pull_stack_inner(stack, service, &server, Some(&mut update))
.await?;
update.logs.extend(res.logs);
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}
impl Resolve<StartStack, (User, Update)> for State {
#[instrument(name = "StartStack", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
@@ -468,6 +564,7 @@ impl super::BatchExecute for BatchDestroyStack {
fn single_request(stack: String) -> ExecuteRequest {
ExecuteRequest::DestroyStack(DestroyStack {
stack,
service: None,
remove_orphans: false,
stop_time: None,
})
@@ -491,6 +588,7 @@ impl Resolve<DestroyStack, (User, Update)> for State {
&self,
DestroyStack {
stack,
service,
remove_orphans,
stop_time,
}: DestroyStack,
@@ -498,7 +596,7 @@ impl Resolve<DestroyStack, (User, Update)> for State {
) -> anyhow::Result<Update> {
execute_compose::<DestroyStack>(
&stack,
None,
service,
&user,
|state| state.destroying = true,
update,

View File

@@ -539,20 +539,21 @@ impl Resolve<GetResourceMatchingContainer, User> for State {
for StackServiceNames {
service_name,
container_name,
..
} in stack
.info
.deployed_services
.unwrap_or(stack.info.latest_services)
{
let is_match = match compose_container_match_regex(&container_name)
.with_context(|| format!("failed to construct container name matching regex for service {service_name}"))
{
Ok(regex) => regex,
Err(e) => {
warn!("{e:#}");
continue;
}
}.is_match(&container);
.with_context(|| format!("failed to construct container name matching regex for service {service_name}"))
{
Ok(regex) => regex,
Err(e) => {
warn!("{e:#}");
continue;
}
}.is_match(&container);
if is_match {
return Ok(GetResourceMatchingContainerResponse {

View File

@@ -2,10 +2,14 @@ use anyhow::{anyhow, Context};
use komodo_client::{
api::write::*,
entities::{
deployment::{Deployment, DeploymentState},
deployment::{
Deployment, DeploymentImage, DeploymentState,
PartialDeploymentConfig, RestartMode,
},
docker::container::RestartPolicyNameEnum,
komodo_timestamp,
permission::PermissionLevel,
server::Server,
server::{Server, ServerState},
to_komodo_name,
update::Update,
user::User,
@@ -13,7 +17,7 @@ use komodo_client::{
},
};
use mungos::{by_id::update_one_by_id, mongodb::bson::doc};
use periphery_client::api;
use periphery_client::api::{self, container::InspectContainer};
use resolver_api::Resolve;
use crate::{
@@ -23,7 +27,7 @@ use crate::{
update::{add_update, make_update},
},
resource,
state::{action_states, db_client, State},
state::{action_states, db_client, server_status_cache, State},
};
impl Resolve<CreateDeployment, User> for State {
@@ -55,6 +59,97 @@ impl Resolve<CopyDeployment, User> for State {
}
}
impl Resolve<CreateDeploymentFromContainer, User> for State {
#[instrument(
name = "CreateDeploymentFromContainer",
skip(self, user)
)]
async fn resolve(
&self,
CreateDeploymentFromContainer { name, server }: CreateDeploymentFromContainer,
user: User,
) -> anyhow::Result<Deployment> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
PermissionLevel::Write,
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(anyhow!(
"Cannot inspect container: server is {:?}",
cache.state
));
}
let container = periphery_client(&server)?
.request(InspectContainer { name: name.clone() })
.await
.context("Failed to inspect container")?;
let mut config = PartialDeploymentConfig {
server_id: server.id.into(),
..Default::default()
};
if let Some(container_config) = container.config {
config.image = container_config
.image
.map(|image| DeploymentImage::Image { image });
config.command = container_config.cmd.join(" ").into();
config.environment = container_config
.env
.into_iter()
.map(|env| format!(" {env}"))
.collect::<Vec<_>>()
.join("\n")
.into();
config.labels = container_config
.labels
.into_iter()
.map(|(key, val)| format!(" {key}: {val}"))
.collect::<Vec<_>>()
.join("\n")
.into();
}
if let Some(host_config) = container.host_config {
config.volumes = host_config
.binds
.into_iter()
.map(|bind| format!(" {bind}"))
.collect::<Vec<_>>()
.join("\n")
.into();
config.network = host_config.network_mode;
config.ports = host_config
.port_bindings
.into_iter()
.filter_map(|(container, mut host)| {
let host = host.pop()?.host_port?;
Some(format!(" {host}:{}", container.replace("/tcp", "")))
})
.collect::<Vec<_>>()
.join("\n")
.into();
config.restart = host_config.restart_policy.map(|restart| {
match restart.name {
RestartPolicyNameEnum::Always => RestartMode::Always,
RestartPolicyNameEnum::No
| RestartPolicyNameEnum::Empty => RestartMode::NoRestart,
RestartPolicyNameEnum::UnlessStopped => {
RestartMode::UnlessStopped
}
RestartPolicyNameEnum::OnFailure => RestartMode::OnFailure,
}
});
}
resource::create::<Deployment>(&name, config, &user).await
}
}
impl Resolve<DeleteDeployment, User> for State {
#[instrument(name = "DeleteDeployment", skip(self, user))]
async fn resolve(

View File

@@ -80,6 +80,7 @@ pub enum WriteRequest {
// ==== DEPLOYMENT ====
CreateDeployment(CreateDeployment),
CopyDeployment(CopyDeployment),
CreateDeploymentFromContainer(CreateDeploymentFromContainer),
DeleteDeployment(DeleteDeployment),
UpdateDeployment(UpdateDeployment),
RenameDeployment(RenameDeployment),

View File

@@ -23,6 +23,7 @@ use periphery_client::api::compose::{
use resolver_api::Resolve;
use crate::{
api::execute::pull_stack_inner,
config::core_config,
helpers::{
git_token, periphery_client,
@@ -32,7 +33,7 @@ use crate::{
resource,
stack::{
get_stack_and_server,
remote::{get_remote_compose_contents, RemoteComposeContents},
remote::{get_repo_compose_contents, RemoteComposeContents},
services::extract_services_into_res,
},
state::{db_client, github_client, State},
@@ -258,54 +259,56 @@ impl Resolve<RefreshStackCache, User> for State {
// =============
// FILES ON HOST
// =============
if stack.config.server_id.is_empty() {
(vec![], None, None, None, None)
let (server, state) = if stack.config.server_id.is_empty() {
(None, ServerState::Disabled)
} else {
let (server, status) =
let (server, state) =
get_server_with_state(&stack.config.server_id).await?;
if status != ServerState::Ok {
(vec![], None, None, None, None)
} else {
let GetComposeContentsOnHostResponse { contents, errors } =
match periphery_client(&server)?
.request(GetComposeContentsOnHost {
file_paths: stack.file_paths().to_vec(),
name: stack.name.clone(),
run_directory: stack.config.run_directory.clone(),
})
.await
.context(
"failed to get compose file contents from host",
) {
Ok(res) => res,
Err(e) => GetComposeContentsOnHostResponse {
contents: Default::default(),
errors: vec![FileContents {
path: stack.config.run_directory.clone(),
contents: format_serror(&e.into()),
}],
},
};
(Some(server), state)
};
if state != ServerState::Ok {
(vec![], None, None, None, None)
} else if let Some(server) = server {
let GetComposeContentsOnHostResponse { contents, errors } =
match periphery_client(&server)?
.request(GetComposeContentsOnHost {
file_paths: stack.file_paths().to_vec(),
name: stack.name.clone(),
run_directory: stack.config.run_directory.clone(),
})
.await
.context("failed to get compose file contents from host")
{
Ok(res) => res,
Err(e) => GetComposeContentsOnHostResponse {
contents: Default::default(),
errors: vec![FileContents {
path: stack.config.run_directory.clone(),
contents: format_serror(&e.into()),
}],
},
};
let project_name = stack.project_name(true);
let project_name = stack.project_name(true);
let mut services = Vec::new();
let mut services = Vec::new();
for contents in &contents {
if let Err(e) = extract_services_into_res(
&project_name,
&contents.contents,
&mut services,
) {
warn!(
for contents in &contents {
if let Err(e) = extract_services_into_res(
&project_name,
&contents.contents,
&mut services,
) {
warn!(
"failed to extract stack services, things won't works correctly. stack: {} | {e:#}",
stack.name
);
}
}
(services, Some(contents), Some(errors), None, None)
}
(services, Some(contents), Some(errors), None, None)
} else {
(vec![], None, None, None, None)
}
} else if !repo_empty {
// ================
@@ -317,9 +320,8 @@ impl Resolve<RefreshStackCache, User> for State {
hash: latest_hash,
message: latest_message,
..
} =
get_remote_compose_contents(&stack, Some(&mut missing_files))
.await?;
} = get_repo_compose_contents(&stack, Some(&mut missing_files))
.await?;
let project_name = stack.project_name(true);
@@ -357,21 +359,21 @@ impl Resolve<RefreshStackCache, User> for State {
&mut services,
) {
warn!(
"failed to extract stack services, things won't works correctly. stack: {} | {e:#}",
"Failed to extract Stack services for {}, things may not work correctly. | {e:#}",
stack.name
);
services.extend(stack.info.latest_services);
services.extend(stack.info.latest_services.clone());
};
(services, None, None, None, None)
};
let info = StackInfo {
missing_files,
deployed_services: stack.info.deployed_services,
deployed_project_name: stack.info.deployed_project_name,
deployed_contents: stack.info.deployed_contents,
deployed_hash: stack.info.deployed_hash,
deployed_message: stack.info.deployed_message,
deployed_services: stack.info.deployed_services.clone(),
deployed_project_name: stack.info.deployed_project_name.clone(),
deployed_contents: stack.info.deployed_contents.clone(),
deployed_hash: stack.info.deployed_hash.clone(),
deployed_message: stack.info.deployed_message.clone(),
latest_services,
remote_contents,
remote_errors,
@@ -391,6 +393,23 @@ impl Resolve<RefreshStackCache, User> for State {
.await
.context("failed to update stack info on db")?;
if (stack.config.poll_for_updates || stack.config.auto_update)
&& !stack.config.server_id.is_empty()
{
let (server, state) =
get_server_with_state(&stack.config.server_id).await?;
if state == ServerState::Ok {
let name = stack.name.clone();
if let Err(e) =
pull_stack_inner(stack, None, &server, None).await
{
warn!(
"Failed to pull latest images for Stack {name} | {e:#}",
);
}
}
}
Ok(NoData {})
}
}

View File

@@ -44,7 +44,9 @@ pub async fn get_builder_periphery(
match builder.config {
BuilderConfig::Url(config) => {
if config.address.is_empty() {
return Err(anyhow!("Builder has not yet configured an address"));
return Err(anyhow!(
"Builder has not yet configured an address"
));
}
let periphery = PeripheryClient::new(
config.address,

View File

@@ -323,6 +323,22 @@ async fn execute_execution(
"Batch method BatchDeploy not implemented correctly"
));
}
Execution::PullDeployment(req) => {
let req = ExecuteRequest::PullDeployment(req);
let update = init_execution_update(&req, &user).await?;
let ExecuteRequest::PullDeployment(req) = req else {
unreachable!()
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
.await
.context("Failed at PullDeployment"),
&update_id,
)
.await?
}
Execution::StartDeployment(req) => {
let req = ExecuteRequest::StartDeployment(req);
let update = init_execution_update(&req, &user).await?;
@@ -908,6 +924,22 @@ async fn execute_execution(
"Batch method BatchDeployStackIfChanged not implemented correctly"
));
}
Execution::PullStack(req) => {
let req = ExecuteRequest::PullStack(req);
let update = init_execution_update(&req, &user).await?;
let ExecuteRequest::PullStack(req) = req else {
unreachable!()
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
.await
.context("Failed at PullStack"),
&update_id,
)
.await?
}
Execution::StartStack(req) => {
let req = ExecuteRequest::StartStack(req);
let update = init_execution_update(&req, &user).await?;
@@ -1159,6 +1191,7 @@ impl ExtendBatch for BatchDeployStack {
fn single_execution(stack: String) -> Execution {
Execution::DeployStack(DeployStack {
stack,
service: None,
stop_time: None,
})
}
@@ -1179,6 +1212,7 @@ impl ExtendBatch for BatchDestroyStack {
fn single_execution(stack: String) -> Execution {
Execution::DestroyStack(DestroyStack {
stack,
service: None,
remove_orphans: false,
stop_time: None,
})

View File

@@ -103,7 +103,7 @@ pub fn get_stack_state_from_containers(
})
.collect::<Vec<_>>();
let containers = containers.iter().filter(|container| {
services.iter().any(|StackServiceNames { service_name, container_name }| {
services.iter().any(|StackServiceNames { service_name, container_name, .. }| {
match compose_container_match_regex(container_name)
.with_context(|| format!("failed to construct container name matching regex for service {service_name}"))
{

View File

@@ -264,6 +264,12 @@ pub async fn init_execution_update(
ExecuteRequest::BatchDeploy(_data) => {
return Ok(Default::default())
}
ExecuteRequest::PullDeployment(data) => (
Operation::PullDeployment,
ResourceTarget::Deployment(
resource::get::<Deployment>(&data.deployment).await?.id,
),
),
ExecuteRequest::StartDeployment(data) => (
Operation::StartDeployment,
ResourceTarget::Deployment(
@@ -300,6 +306,9 @@ pub async fn init_execution_update(
resource::get::<Deployment>(&data.deployment).await?.id,
),
),
ExecuteRequest::BatchDestroyDeployment(_data) => {
return Ok(Default::default())
}
// Build
ExecuteRequest::RunBuild(data) => (
@@ -395,7 +404,11 @@ pub async fn init_execution_update(
// Stack
ExecuteRequest::DeployStack(data) => (
Operation::DeployStack,
if data.service.is_some() {
Operation::DeployStackService
} else {
Operation::DeployStack
},
ResourceTarget::Stack(
resource::get::<Stack>(&data.stack).await?.id,
),
@@ -422,6 +435,16 @@ pub async fn init_execution_update(
resource::get::<Stack>(&data.stack).await?.id,
),
),
ExecuteRequest::PullStack(data) => (
if data.service.is_some() {
Operation::PullStackService
} else {
Operation::PullStack
},
ResourceTarget::Stack(
resource::get::<Stack>(&data.stack).await?.id,
),
),
ExecuteRequest::RestartStack(data) => (
if data.service.is_some() {
Operation::RestartStackService
@@ -463,7 +486,11 @@ pub async fn init_execution_update(
),
),
ExecuteRequest::DestroyStack(data) => (
Operation::DestroyStack,
if data.service.is_some() {
Operation::DestroyStackService
} else {
Operation::DestroyStack
},
ResourceTarget::Stack(
resource::get::<Stack>(&data.stack).await?.id,
),

View File

@@ -132,11 +132,6 @@ impl RepoExecution for BuildRepo {
}
}
#[derive(Deserialize)]
pub struct RepoWebhookPath {
pub option: RepoWebhookOption,
}
#[derive(Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RepoWebhookOption {
@@ -220,6 +215,7 @@ impl StackExecution for DeployStack {
if stack.config.webhook_force_deploy {
let req = ExecuteRequest::DeployStack(DeployStack {
stack: stack.id,
service: None,
stop_time: None,
});
let update = init_execution_update(&req, &user).await?;
@@ -244,11 +240,6 @@ impl StackExecution for DeployStack {
}
}
#[derive(Deserialize)]
pub struct StackWebhookPath {
pub option: StackWebhookOption,
}
#[derive(Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum StackWebhookOption {
@@ -340,11 +331,6 @@ impl SyncExecution for RunSync {
}
}
#[derive(Deserialize)]
pub struct SyncWebhookPath {
pub option: SyncWebhookOption,
}
#[derive(Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SyncWebhookOption {
@@ -410,7 +396,7 @@ fn procedure_locks() -> &'static ListenerLockCache {
pub async fn handle_procedure_webhook<B: super::VerifyBranch>(
procedure: Procedure,
target_branch: String,
target_branch: &str,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
@@ -425,7 +411,7 @@ pub async fn handle_procedure_webhook<B: super::VerifyBranch>(
}
if target_branch != ANY_BRANCH {
B::verify_branch(&body, &target_branch)?;
B::verify_branch(&body, target_branch)?;
}
let user = git_webhook_user().to_owned();
@@ -457,7 +443,7 @@ fn action_locks() -> &'static ListenerLockCache {
pub async fn handle_action_webhook<B: super::VerifyBranch>(
action: Action,
target_branch: String,
target_branch: &str,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
@@ -471,7 +457,7 @@ pub async fn handle_action_webhook<B: super::VerifyBranch>(
}
if target_branch != ANY_BRANCH {
B::verify_branch(&body, &target_branch)?;
B::verify_branch(&body, target_branch)?;
}
let user = git_webhook_user().to_owned();

View File

@@ -3,7 +3,9 @@ use komodo_client::entities::{
action::Action, build::Build, procedure::Procedure, repo::Repo,
resource::Resource, stack::Stack, sync::ResourceSync,
};
use reqwest::StatusCode;
use serde::Deserialize;
use serror::AddStatusCode;
use tracing::Instrument;
use crate::resource::KomodoResource;
@@ -12,8 +14,8 @@ use super::{
resources::{
handle_action_webhook, handle_build_webhook,
handle_procedure_webhook, handle_repo_webhook,
handle_stack_webhook, handle_sync_webhook, RepoWebhookPath,
StackWebhookPath, SyncWebhookPath,
handle_stack_webhook, handle_sync_webhook, RepoWebhookOption,
StackWebhookOption, SyncWebhookOption,
},
CustomSecret, VerifyBranch, VerifySecret,
};
@@ -24,7 +26,14 @@ struct Id {
}
#[derive(Deserialize)]
struct Branch {
struct IdAndOption<T> {
id: String,
option: T,
}
#[derive(Deserialize)]
struct IdAndBranch {
id: String,
#[serde(default = "default_branch")]
branch: String,
}
@@ -64,7 +73,7 @@ pub fn router<P: VerifySecret + VerifyBranch>() -> Router {
.route(
"/repo/:id/:option",
post(
|Path(Id { id }), Path(RepoWebhookPath { option }), headers: HeaderMap, body: String| async move {
|Path(IdAndOption::<RepoWebhookOption> { id, option }), headers: HeaderMap, body: String| async move {
let repo =
auth_webhook::<P, Repo>(&id, headers, &body).await?;
tokio::spawn(async move {
@@ -90,7 +99,7 @@ pub fn router<P: VerifySecret + VerifyBranch>() -> Router {
.route(
"/stack/:id/:option",
post(
|Path(Id { id }), Path(StackWebhookPath { option }), headers: HeaderMap, body: String| async move {
|Path(IdAndOption::<StackWebhookOption> { id, option }), headers: HeaderMap, body: String| async move {
let stack =
auth_webhook::<P, Stack>(&id, headers, &body).await?;
tokio::spawn(async move {
@@ -116,7 +125,7 @@ pub fn router<P: VerifySecret + VerifyBranch>() -> Router {
.route(
"/sync/:id/:option",
post(
|Path(Id { id }), Path(SyncWebhookPath { option }), headers: HeaderMap, body: String| async move {
|Path(IdAndOption::<SyncWebhookOption> { id, option }), headers: HeaderMap, body: String| async move {
let sync =
auth_webhook::<P, ResourceSync>(&id, headers, &body).await?;
tokio::spawn(async move {
@@ -142,19 +151,19 @@ pub fn router<P: VerifySecret + VerifyBranch>() -> Router {
.route(
"/procedure/:id/:branch",
post(
|Path(Id { id }), Path(Branch { branch }), headers: HeaderMap, body: String| async move {
|Path(IdAndBranch { id, branch }), headers: HeaderMap, body: String| async move {
let procedure =
auth_webhook::<P, Procedure>(&id, headers, &body).await?;
tokio::spawn(async move {
let span = info_span!("ProcedureWebhook", id);
async {
let res = handle_procedure_webhook::<P>(
procedure, branch, body,
procedure, &branch, body,
)
.await;
if let Err(e) = res {
warn!(
"Failed at running webhook for procedure {id} | {e:#}"
"Failed at running webhook for procedure {id} | target branch: {branch} | {e:#}"
);
}
}
@@ -168,19 +177,19 @@ pub fn router<P: VerifySecret + VerifyBranch>() -> Router {
.route(
"/action/:id/:branch",
post(
|Path(Id { id }), Path(Branch { branch }), headers: HeaderMap, body: String| async move {
|Path(IdAndBranch { id, branch }), headers: HeaderMap, body: String| async move {
let action =
auth_webhook::<P, Action>(&id, headers, &body).await?;
tokio::spawn(async move {
let span = info_span!("ActionWebhook", id);
async {
let res = handle_action_webhook::<P>(
action, branch, body,
action, &branch, body,
)
.await;
if let Err(e) = res {
warn!(
"Failed at running webhook for action {id} | {e:#}"
"Failed at running webhook for action {id} | target branch: {branch} | {e:#}"
);
}
}
@@ -202,7 +211,10 @@ where
P: VerifySecret,
R: KomodoResource + CustomSecret,
{
let resource = crate::resource::get::<R>(id).await?;
P::verify_secret(headers, body, R::custom_secret(&resource))?;
let resource = crate::resource::get::<R>(id)
.await
.status_code(StatusCode::BAD_REQUEST)?;
P::verify_secret(headers, body, R::custom_secret(&resource))
.status_code(StatusCode::UNAUTHORIZED)?;
Ok(resource)
}

View File

@@ -41,6 +41,7 @@ pub async fn insert_deployments_status_unknown(
id: deployment.id,
state: DeploymentState::Unknown,
container: None,
update_available: false,
},
prev,
}

View File

@@ -62,6 +62,7 @@ pub struct CachedDeploymentStatus {
pub id: String,
pub state: DeploymentState,
pub container: Option<ContainerListItem>,
pub update_available: bool,
}
#[derive(Default, Clone, Debug)]
@@ -117,12 +118,13 @@ async fn refresh_server_cache(ts: i64) {
#[instrument(level = "debug")]
pub async fn update_cache_for_server(server: &Server) {
let (deployments, repos, stacks) = tokio::join!(
let (deployments, builds, repos, stacks) = tokio::join!(
find_collect(
&db_client().deployments,
doc! { "config.server_id": &server.id },
None,
),
find_collect(&db_client().builds, doc! {}, None,),
find_collect(
&db_client().repos,
doc! { "config.server_id": &server.id },
@@ -136,6 +138,7 @@ pub async fn update_cache_for_server(server: &Server) {
);
let deployments = deployments.inspect_err(|e| error!("failed to get deployments list from db (update status cache) | server : {} | {e:#}", server.name)).unwrap_or_default();
let builds = builds.inspect_err(|e| error!("failed to get builds list from db (update status cache) | server : {} | {e:#}", server.name)).unwrap_or_default();
let repos = repos.inspect_err(|e| error!("failed to get repos list from db (update status cache) | server: {} | {e:#}", server.name)).unwrap_or_default();
let stacks = stacks.inspect_err(|e| error!("failed to get stacks list from db (update status cache) | server: {} | {e:#}", server.name)).unwrap_or_default();
@@ -211,8 +214,19 @@ pub async fn update_cache_for_server(server: &Server) {
container.server_id = Some(server.id.clone())
});
tokio::join!(
resources::update_deployment_cache(deployments, &containers),
resources::update_stack_cache(stacks, &containers),
resources::update_deployment_cache(
server.name.clone(),
deployments,
&containers,
&images,
&builds,
),
resources::update_stack_cache(
server.name.clone(),
stacks,
&containers,
&images
),
);
insert_server_status(
server,

View File

@@ -1,24 +1,53 @@
use std::{
collections::HashSet,
sync::{Mutex, OnceLock},
};
use anyhow::Context;
use komodo_client::entities::{
deployment::{Deployment, DeploymentState},
docker::container::ContainerListItem,
stack::{Stack, StackService, StackServiceNames},
use komodo_client::{
api::execute::{Deploy, DeployStack},
entities::{
alert::{Alert, AlertData, SeverityLevel},
build::Build,
deployment::{Deployment, DeploymentImage, DeploymentState},
docker::{
container::{ContainerListItem, ContainerStateStatusEnum},
image::ImageListItem,
},
komodo_timestamp,
stack::{Stack, StackService, StackServiceNames, StackState},
user::auto_redeploy_user,
ResourceTarget,
},
};
use crate::{
alert::send_alerts,
api::execute::{self, ExecuteRequest},
helpers::query::get_stack_state_from_containers,
stack::{
compose_container_match_regex,
services::extract_services_from_stack,
},
state::{deployment_status_cache, stack_status_cache},
state::{
action_states, db_client, deployment_status_cache,
stack_status_cache,
},
};
use super::{CachedDeploymentStatus, CachedStackStatus, History};
fn deployment_alert_sent_cache() -> &'static Mutex<HashSet<String>> {
static CACHE: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
CACHE.get_or_init(Default::default)
}
pub async fn update_deployment_cache(
server_name: String,
deployments: Vec<Deployment>,
containers: &[ContainerListItem],
images: &[ImageListItem],
builds: &[Build],
) {
let deployment_status_cache = deployment_status_cache();
for deployment in deployments {
@@ -34,6 +63,109 @@ pub async fn update_deployment_cache(
.as_ref()
.map(|c| c.state.into())
.unwrap_or(DeploymentState::NotDeployed);
let image = match deployment.config.image {
DeploymentImage::Build { build_id, version } => {
let (build_name, build_version) = builds
.iter()
.find(|build| build.id == build_id)
.map(|b| (b.name.as_ref(), b.config.version))
.unwrap_or(("Unknown", Default::default()));
let version = if version.is_none() {
build_version.to_string()
} else {
version.to_string()
};
format!("{build_name}:{version}")
}
DeploymentImage::Image { image } => image,
};
let update_available = if let Some(ContainerListItem {
image_id: Some(curr_image_id),
..
}) = &container
{
images
.iter()
.find(|i| i.name == image)
.map(|i| &i.id != curr_image_id)
.unwrap_or_default()
} else {
false
};
if update_available {
if deployment.config.auto_update {
if state == DeploymentState::Running
&& !action_states()
.deployment
.get_or_insert_default(&deployment.id)
.await
.busy()
.unwrap_or(true)
{
let deployment = deployment.name.clone();
tokio::spawn(async move {
if let Err(e) = execute::inner_handler(
ExecuteRequest::Deploy(Deploy {
deployment: deployment.clone(),
stop_time: None,
stop_signal: None,
}),
auto_redeploy_user().to_owned(),
)
.await
{
warn!(
"Failed to auto update Deployment {deployment} | {e:#}"
)
}
});
}
} else if state == DeploymentState::Running
&& deployment.config.send_alerts
&& !deployment_alert_sent_cache()
.lock()
.unwrap()
.contains(&deployment.id)
{
// Add that it is already sent to the cache, so another alert won't be sent.
deployment_alert_sent_cache()
.lock()
.unwrap()
.insert(deployment.id.clone());
let ts = komodo_timestamp();
let alert = Alert {
id: Default::default(),
ts,
resolved: true,
resolved_ts: ts.into(),
level: SeverityLevel::Ok,
target: ResourceTarget::Deployment(deployment.id.clone()),
data: AlertData::DeploymentImageUpdateAvailable {
id: deployment.id.clone(),
name: deployment.name,
server_name: server_name.clone(),
server_id: deployment.config.server_id,
image,
},
};
let res = db_client().alerts.insert_one(&alert).await;
if let Err(e) = res {
error!(
"Failed to record Deployment update avaialable to db | {e:#}"
);
}
send_alerts(&[alert]).await;
}
} else {
// If it sees there is no longer update available, remove
// from the sent cache, so on next `update_available = true`
// the cache is empty and a fresh alert will be sent.
deployment_alert_sent_cache()
.lock()
.unwrap()
.remove(&deployment.id);
}
deployment_status_cache
.insert(
deployment.id.clone(),
@@ -42,6 +174,7 @@ pub async fn update_deployment_cache(
id: deployment.id,
state,
container,
update_available,
},
prev,
}
@@ -51,38 +184,139 @@ pub async fn update_deployment_cache(
}
}
/// (StackId, Service)
fn stack_alert_sent_cache(
) -> &'static Mutex<HashSet<(String, String)>> {
static CACHE: OnceLock<Mutex<HashSet<(String, String)>>> =
OnceLock::new();
CACHE.get_or_init(Default::default)
}
pub async fn update_stack_cache(
server_name: String,
stacks: Vec<Stack>,
containers: &[ContainerListItem],
images: &[ImageListItem],
) {
let stack_status_cache = stack_status_cache();
for stack in stacks {
let services = match extract_services_from_stack(&stack, false)
.await
{
Ok(services) => services,
Err(e) => {
warn!("failed to extract services for stack {}. cannot match services to containers. (update status cache) | {e:?}", stack.name);
continue;
let services = extract_services_from_stack(&stack);
let mut services_with_containers = services.iter().map(|StackServiceNames { service_name, container_name, image }| {
let container = containers.iter().find(|container| {
match compose_container_match_regex(container_name)
.with_context(|| format!("failed to construct container name matching regex for service {service_name}"))
{
Ok(regex) => regex,
Err(e) => {
warn!("{e:#}");
return false
}
}.is_match(&container.name)
}).cloned();
let update_available = if let Some(ContainerListItem { image_id: Some(curr_image_id), .. }) = &container {
images
.iter()
.find(|i| &i.name == image)
.map(|i| &i.id != curr_image_id)
.unwrap_or_default()
} else {
false
};
if update_available {
if !stack.config.auto_update
&& stack.config.send_alerts
&& container.is_some()
&& container.as_ref().unwrap().state == ContainerStateStatusEnum::Running
&& !stack_alert_sent_cache()
.lock()
.unwrap()
.contains(&(stack.id.clone(), service_name.clone()))
{
stack_alert_sent_cache()
.lock()
.unwrap()
.insert((stack.id.clone(), service_name.clone()));
let ts = komodo_timestamp();
let alert = Alert {
id: Default::default(),
ts,
resolved: true,
resolved_ts: ts.into(),
level: SeverityLevel::Ok,
target: ResourceTarget::Stack(stack.id.clone()),
data: AlertData::StackImageUpdateAvailable {
id: stack.id.clone(),
name: stack.name.clone(),
server_name: server_name.clone(),
server_id: stack.config.server_id.clone(),
service: service_name.clone(),
image: image.clone(),
},
};
tokio::spawn(async move {
let res = db_client().alerts.insert_one(&alert).await;
if let Err(e) = res {
error!(
"Failed to record Stack update avaialable to db | {e:#}"
);
}
send_alerts(&[alert]).await;
});
}
} else {
stack_alert_sent_cache()
.lock()
.unwrap()
.remove(&(stack.id.clone(), service_name.clone()));
}
};
let mut services_with_containers = services.iter().map(|StackServiceNames { service_name, container_name }| {
let container = containers.iter().find(|container| {
match compose_container_match_regex(container_name)
.with_context(|| format!("failed to construct container name matching regex for service {service_name}"))
{
Ok(regex) => regex,
Err(e) => {
warn!("{e:#}");
return false
}
}.is_match(&container.name)
}).cloned();
StackService {
service: service_name.clone(),
container,
}
}).collect::<Vec<_>>();
StackService {
service: service_name.clone(),
image: image.to_string(),
container,
update_available,
}
}).collect::<Vec<_>>();
let update_available =
services_with_containers.iter().any(|service| {
service.update_available
// Only consider running services with available updates
&& service
.container
.as_ref()
.map(|c| c.state == ContainerStateStatusEnum::Running)
.unwrap_or_default()
});
let state = get_stack_state_from_containers(
&stack.config.ignore_services,
&services,
containers,
);
if update_available
&& stack.config.auto_update
&& state == StackState::Running
&& !action_states()
.stack
.get_or_insert_default(&stack.id)
.await
.busy()
.unwrap_or(true)
{
let stack = stack.name.clone();
tokio::spawn(async move {
if let Err(e) = execute::inner_handler(
ExecuteRequest::DeployStack(DeployStack {
stack: stack.clone(),
service: None,
stop_time: None,
}),
auto_redeploy_user().to_owned(),
)
.await
{
warn!("Failed auto update Stack {stack} | {e:#}")
}
});
}
services_with_containers
.sort_by(|a, b| a.service.cmp(&b.service));
let prev = stack_status_cache
@@ -91,11 +325,7 @@ pub async fn update_stack_cache(
.map(|s| s.curr.state);
let status = CachedStackStatus {
id: stack.id.clone(),
state: get_stack_state_from_containers(
&stack.config.ignore_services,
&services,
containers,
),
state,
services: services_with_containers,
};
stack_status_cache

View File

@@ -72,6 +72,19 @@ impl super::KomodoResource for Deployment {
}
DeploymentImage::Image { image } => (image, None),
};
let (image, update_available) = status
.as_ref()
.and_then(|s| {
s.curr.container.as_ref().map(|c| {
(
c.image
.clone()
.unwrap_or_else(|| String::from("Unknown")),
s.curr.update_available,
)
})
})
.unwrap_or((build_image, false));
DeploymentListItem {
name: deployment.name,
id: deployment.id,
@@ -85,16 +98,8 @@ impl super::KomodoResource for Deployment {
status: status.as_ref().and_then(|s| {
s.curr.container.as_ref().and_then(|c| c.status.to_owned())
}),
image: status
.as_ref()
.and_then(|s| {
s.curr.container.as_ref().map(|c| {
c.image
.clone()
.unwrap_or_else(|| String::from("Unknown"))
})
})
.unwrap_or(build_image),
image,
update_available,
server_id: deployment.config.server_id,
build_id,
},

View File

@@ -243,6 +243,16 @@ async fn validate_config(
));
}
}
Execution::PullDeployment(params) => {
let deployment =
super::get_check_permissions::<Deployment>(
&params.deployment,
user,
PermissionLevel::Execute,
)
.await?;
params.deployment = deployment.id;
}
Execution::StartDeployment(params) => {
let deployment =
super::get_check_permissions::<Deployment>(
@@ -607,6 +617,15 @@ async fn validate_config(
));
}
}
Execution::PullStack(params) => {
let stack = super::get_check_permissions::<Stack>(
&params.stack,
user,
PermissionLevel::Execute,
)
.await?;
params.stack = stack.id;
}
Execution::StartStack(params) => {
let stack = super::get_check_permissions::<Stack>(
&params.stack,

View File

@@ -1,4 +1,6 @@
use async_timing_util::{wait_until_timelength, Timelength};
use std::time::Duration;
use async_timing_util::{get_timelength_in_ms, Timelength};
use komodo_client::{
api::write::{
RefreshBuildCache, RefreshRepoCache, RefreshResourceSyncPending,
@@ -10,6 +12,7 @@ use mungos::find::find_collect;
use resolver_api::Resolve;
use crate::{
api::execute::pull_deployment_inner,
config::core_config,
state::{db_client, State},
};
@@ -20,9 +23,11 @@ pub fn spawn_resource_refresh_loop() {
.try_into()
.expect("Invalid resource poll interval");
tokio::spawn(async move {
refresh_all().await;
let mut interval = tokio::time::interval(Duration::from_millis(
get_timelength_in_ms(interval) as u64,
));
loop {
wait_until_timelength(interval, 3000).await;
interval.tick().await;
refresh_all().await;
}
});
@@ -30,6 +35,7 @@ pub fn spawn_resource_refresh_loop() {
async fn refresh_all() {
refresh_stacks().await;
refresh_deployments().await;
refresh_builds().await;
refresh_repos().await;
refresh_syncs().await;
@@ -60,6 +66,43 @@ async fn refresh_stacks() {
}
}
async fn refresh_deployments() {
let servers = find_collect(&db_client().servers, None, None)
.await
.inspect_err(|e| {
warn!(
"Failed to get Servers from database in refresh task | {e:#}"
)
})
.unwrap_or_default();
let Ok(deployments) = find_collect(&db_client().deployments, None, None)
.await
.inspect_err(|e| {
warn!(
"Failed to get Deployments from database in refresh task | {e:#}"
)
})
else {
return;
};
for deployment in deployments {
if deployment.config.poll_for_updates
|| deployment.config.auto_update
{
if let Some(server) =
servers.iter().find(|s| s.id == deployment.config.server_id)
{
let name = deployment.name.clone();
if let Err(e) =
pull_deployment_inner(deployment, server).await
{
warn!("Failed to pull latest image for Deployment {name} | {e:#}");
}
}
}
}
}
async fn refresh_builds() {
let Ok(builds) = find_collect(&db_client().builds, None, None)
.await

View File

@@ -9,7 +9,7 @@ use komodo_client::{
stack::{
PartialStackConfig, Stack, StackConfig, StackConfigDiff,
StackInfo, StackListItem, StackListItemInfo,
StackQuerySpecifics, StackState,
StackQuerySpecifics, StackServiceWithUpdate, StackState,
},
update::Update,
user::{stack_user, User},
@@ -56,21 +56,21 @@ impl super::KomodoResource for Stack {
let state =
status.as_ref().map(|s| s.curr.state).unwrap_or_default();
let project_name = stack.project_name(false);
let services = match (
state,
stack.info.deployed_services,
stack.info.latest_services,
) {
// Always use latest if its down.
(StackState::Down, _, latest_services) => latest_services,
// Also use latest if deployed services is empty.
(_, Some(deployed_services), _) => deployed_services,
// Otherwise use deployed services
(_, _, latest_services) => latest_services,
}
.into_iter()
.map(|service| service.service_name)
.collect();
let services = status
.as_ref()
.map(|s| {
s.curr
.services
.iter()
.map(|service| StackServiceWithUpdate {
service: service.service.clone(),
image: service.image.clone(),
update_available: service.update_available,
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
// This is only true if it is KNOWN to be true. so other cases are false.
let (project_missing, status) =
if stack.config.server_id.is_empty()
@@ -98,6 +98,7 @@ impl super::KomodoResource for Stack {
} else {
(false, None)
};
StackListItem {
id: stack.id,
name: stack.name,

View File

@@ -56,7 +56,7 @@ pub async fn execute_compose<T: ExecuteCompose>(
if let Some(service) = &service {
update.logs.push(Log::simple(
&format!("Service: {service}"),
format!("Execution requested for service stack {service}"),
format!("Execution requested for Stack service {service}"),
))
}

View File

@@ -17,7 +17,7 @@ pub struct RemoteComposeContents {
}
/// Returns Result<(read paths, error paths, logs, short hash, commit message)>
pub async fn get_remote_compose_contents(
pub async fn get_repo_compose_contents(
stack: &Stack,
// Collect any files which are missing in the repo.
mut missing_files: Option<&mut Vec<String>>,

View File

@@ -1,67 +1,30 @@
use anyhow::Context;
use komodo_client::entities::{
stack::{
ComposeFile, ComposeService, ComposeServiceDeploy, Stack,
StackServiceNames,
},
FileContents,
use komodo_client::entities::stack::{
ComposeFile, ComposeService, ComposeServiceDeploy, Stack,
StackServiceNames,
};
use super::remote::{
get_remote_compose_contents, RemoteComposeContents,
};
/// Passing fresh will re-extract services from compose file, whether local or remote (repo)
pub async fn extract_services_from_stack(
pub fn extract_services_from_stack(
stack: &Stack,
fresh: bool,
) -> anyhow::Result<Vec<StackServiceNames>> {
if !fresh {
if let Some(services) = &stack.info.deployed_services {
return Ok(services.clone());
} else {
return Ok(stack.info.latest_services.clone());
}
}
let compose_contents = if stack.config.file_contents.is_empty() {
let RemoteComposeContents {
successful,
errored,
..
} = get_remote_compose_contents(stack, None).await.context(
"failed to get remote compose files to extract services",
)?;
if !errored.is_empty() {
let mut e = anyhow::Error::msg("Trace root");
for err in errored {
e = e.context(format!("{}: {}", err.path, err.contents));
) -> Vec<StackServiceNames> {
if let Some(mut services) = stack.info.deployed_services.clone() {
if services.iter().any(|service| service.image.is_empty()) {
for service in
services.iter_mut().filter(|s| s.image.is_empty())
{
service.image = stack
.info
.latest_services
.iter()
.find(|s| s.service_name == service.service_name)
.map(|s| s.image.clone())
.unwrap_or_default();
}
return Err(
e.context("Failed to read one or more remote compose files"),
);
}
successful
services
} else {
vec![FileContents {
path: String::from("compose.yaml"),
contents: stack.config.file_contents.clone(),
}]
};
let mut res = Vec::new();
for FileContents { path, contents } in &compose_contents {
extract_services_into_res(
&stack.project_name(true),
contents,
&mut res,
)
.with_context(|| {
format!("failed to extract services from file at path: {path}")
})?;
stack.info.latest_services.clone()
}
Ok(res)
}
pub fn extract_services_into_res(
@@ -79,10 +42,11 @@ pub fn extract_services_into_res(
ComposeService {
container_name,
deploy,
..
image,
},
) in compose.services
{
let image = image.unwrap_or_default();
match deploy {
Some(ComposeServiceDeploy {
replicas: Some(replicas),
@@ -93,6 +57,7 @@ pub fn extract_services_into_res(
"{project_name}-{service_name}-{i}"
),
service_name: format!("{service_name}-{i}"),
image: image.clone(),
});
}
}
@@ -102,6 +67,7 @@ pub fn extract_services_into_res(
format!("{project_name}-{service_name}")
}),
service_name,
image,
});
}
}

View File

@@ -97,6 +97,7 @@ pub async fn deploy_from_cache(
ResourceTarget::Stack(name) => {
let req = ExecuteRequest::DeployStack(DeployStack {
stack: name.to_string(),
service: None,
stop_time: None,
});

View File

@@ -392,6 +392,13 @@ impl ResourceSyncTrait for Procedure {
.unwrap_or_default();
}
Execution::BatchDeploy(_config) => {}
Execution::PullDeployment(config) => {
config.deployment = resources
.deployments
.get(&config.deployment)
.map(|d| d.name.clone())
.unwrap_or_default();
}
Execution::StartDeployment(config) => {
config.deployment = resources
.deployments
@@ -643,6 +650,13 @@ impl ResourceSyncTrait for Procedure {
.unwrap_or_default();
}
Execution::BatchDeployStackIfChanged(_config) => {}
Execution::PullStack(config) => {
config.stack = resources
.stacks
.get(&config.stack)
.map(|s| s.name.clone())
.unwrap_or_default();
}
Execution::StartStack(config) => {
config.stack = resources
.stacks

View File

@@ -447,6 +447,15 @@ impl ToToml for Procedure {
.unwrap_or(&String::new()),
),
Execution::BatchDeploy(_exec) => {}
Execution::PullDeployment(exec) => {
exec.deployment.clone_from(
all
.deployments
.get(&exec.deployment)
.map(|r| &r.name)
.unwrap_or(&String::new()),
)
}
Execution::StartDeployment(exec) => {
exec.deployment.clone_from(
all
@@ -730,6 +739,13 @@ impl ToToml for Procedure {
)
}
Execution::BatchDeployStackIfChanged(_exec) => {}
Execution::PullStack(exec) => exec.stack.clone_from(
all
.stacks
.get(&exec.stack)
.map(|r| &r.name)
.unwrap_or(&String::new()),
),
Execution::StartStack(exec) => exec.stack.clone_from(
all
.stacks

View File

@@ -21,6 +21,7 @@ environment_file.workspace = true
formatting.workspace = true
command.workspace = true
logger.workspace = true
cache.workspace = true
git.workspace = true
# mogh
serror = { workspace = true, features = ["axum"] }

View File

@@ -1,25 +1,22 @@
use std::path::PathBuf;
use std::{fmt::Write, path::PathBuf};
use anyhow::{anyhow, Context};
use command::run_komodo_command;
use formatting::format_serror;
use git::{write_commit_file, GitRes};
use komodo_client::entities::{
stack::ComposeProject, to_komodo_name, update::Log, CloneArgs,
FileContents,
};
use periphery_client::api::{
compose::*,
git::{PullOrCloneRepo, RepoActionResponse},
stack::ComposeProject, to_komodo_name, update::Log, FileContents,
};
use periphery_client::api::{compose::*, git::RepoActionResponse};
use resolver_api::Resolve;
use serde::{Deserialize, Serialize};
use tokio::fs;
use crate::{
compose::{compose_up, docker_compose},
compose::{compose_up, docker_compose, write_stack, WriteStackRes},
config::periphery_config,
helpers::log_grep,
docker::docker_login,
helpers::{log_grep, pull_or_clone_stack},
State,
};
@@ -249,59 +246,7 @@ impl Resolve<WriteCommitComposeContents> for State {
}: WriteCommitComposeContents,
_: (),
) -> anyhow::Result<RepoActionResponse> {
if stack.config.files_on_host {
return Err(anyhow!(
"Wrong method called for files on host stack"
));
}
if stack.config.repo.is_empty() {
return Err(anyhow!("Repo is not configured"));
}
let root = periphery_config()
.stack_dir
.join(to_komodo_name(&stack.name));
let mut args: CloneArgs = (&stack).into();
// Set the clone destination to the one created for this run
args.destination = Some(root.display().to_string());
let git_token = match git_token {
Some(token) => Some(token),
None => {
if !stack.config.git_account.is_empty() {
match crate::helpers::git_token(
&stack.config.git_provider,
&stack.config.git_account,
) {
Ok(token) => Some(token.to_string()),
Err(e) => {
return Err(
e.context("Failed to find required git token"),
);
}
}
} else {
None
}
}
};
State
.resolve(
PullOrCloneRepo {
args,
git_token,
environment: vec![],
env_file_path: stack.config.env_file_path.clone(),
skip_secret_interp: stack.config.skip_secret_interp,
// repo replacer only needed for on_clone / on_pull,
// which aren't available for stacks
replacers: Default::default(),
},
(),
)
.await?;
let root = pull_or_clone_stack(&stack, git_token).await?;
let file_path = stack
.config
@@ -334,6 +279,119 @@ impl Resolve<WriteCommitComposeContents> for State {
//
impl<'a> WriteStackRes for &'a mut ComposePullResponse {
fn logs(&mut self) -> &mut Vec<Log> {
&mut self.logs
}
}
impl Resolve<ComposePull> for State {
#[instrument(
name = "ComposePull",
skip(self, git_token, registry_token)
)]
async fn resolve(
&self,
ComposePull {
stack,
service,
git_token,
registry_token,
}: ComposePull,
_: (),
) -> anyhow::Result<ComposePullResponse> {
let mut res = ComposePullResponse::default();
let (run_directory, env_file_path) =
write_stack(&stack, git_token, &mut res).await?;
// Canonicalize the path to ensure it exists, and is the cleanest path to the run directory.
let run_directory = run_directory.canonicalize().context(
"Failed to validate run directory on host after stack write (canonicalize error)",
)?;
let file_paths = stack
.file_paths()
.iter()
.map(|path| {
(
path,
// This will remove any intermediate uneeded '/./' in the path
run_directory.join(path).components().collect::<PathBuf>(),
)
})
.collect::<Vec<_>>();
for (path, full_path) in &file_paths {
if !full_path.exists() {
return Err(anyhow!("Missing compose file at {path}"));
}
}
let docker_compose = docker_compose();
let service_arg = service
.as_ref()
.map(|service| format!(" {service}"))
.unwrap_or_default();
let file_args = if stack.config.file_paths.is_empty() {
String::from("compose.yaml")
} else {
stack.config.file_paths.join(" -f ")
};
// Login to the registry to pull private images, if provider / account are set
if !stack.config.registry_provider.is_empty()
&& !stack.config.registry_account.is_empty()
{
docker_login(
&stack.config.registry_provider,
&stack.config.registry_account,
registry_token.as_deref(),
)
.await
.with_context(|| {
format!(
"domain: {} | account: {}",
stack.config.registry_provider,
stack.config.registry_account
)
})
.context("failed to login to image registry")?;
}
let env_file = env_file_path
.map(|path| format!(" --env-file {path}"))
.unwrap_or_default();
let additional_env_files = stack
.config
.additional_env_files
.iter()
.fold(String::new(), |mut output, file| {
let _ = write!(output, " --env-file {file}");
output
});
let project_name = stack.project_name(false);
let log = run_komodo_command(
"compose pull",
run_directory.as_ref(),
format!(
"{docker_compose} -p {project_name} -f {file_args}{env_file}{additional_env_files} pull{service_arg}",
),
false,
)
.await;
res.logs.push(log);
Ok(res)
}
}
//
impl Resolve<ComposeUp> for State {
#[instrument(
name = "ComposeUp",

View File

@@ -1,12 +1,20 @@
use std::sync::OnceLock;
use cache::TimeoutCache;
use command::run_komodo_command;
use komodo_client::entities::{
deployment::extract_registry_domain,
docker::image::{Image, ImageHistoryResponseItem},
komodo_timestamp,
update::Log,
};
use periphery_client::api::image::*;
use resolver_api::Resolve;
use crate::{docker::docker_client, State};
use crate::{
docker::{docker_client, docker_login},
State,
};
//
@@ -36,6 +44,68 @@ impl Resolve<ImageHistory> for State {
//
/// Wait this long after a pull to allow another pull through
const PULL_TIMEOUT: i64 = 5_000;
fn pull_cache() -> &'static TimeoutCache<String, Log> {
static PULL_CACHE: OnceLock<TimeoutCache<String, Log>> =
OnceLock::new();
PULL_CACHE.get_or_init(Default::default)
}
impl Resolve<PullImage> for State {
#[instrument(name = "PullImage", skip(self))]
async fn resolve(
&self,
PullImage {
name,
account,
token,
}: PullImage,
_: (),
) -> anyhow::Result<Log> {
// Acquire the image lock
let lock = pull_cache().get_lock(name.clone()).await;
// Lock the image lock, prevents simultaneous pulls by
// ensuring simultaneous pulls will wait for first to finish
// and checking cached results.
let mut locked = lock.lock().await;
// Early return from cache if lasted pulled with PULL_TIMEOUT
if locked.last_ts + PULL_TIMEOUT > komodo_timestamp() {
return locked.clone_res();
}
let res = async {
docker_login(
&extract_registry_domain(&name)?,
account.as_deref().unwrap_or_default(),
token.as_deref(),
)
.await?;
anyhow::Ok(
run_komodo_command(
"docker pull",
None,
format!("docker pull {name}"),
false,
)
.await,
)
}
.await;
// Set the cache with results. Any other calls waiting on the lock will
// then immediately also use this same result.
locked.set(&res, komodo_timestamp());
res
}
}
//
impl Resolve<DeleteImage> for State {
#[instrument(name = "DeleteImage", skip(self))]
async fn resolve(

View File

@@ -82,6 +82,7 @@ pub enum PeripheryRequest {
// Compose (Write)
WriteComposeContentsToHost(WriteComposeContentsToHost),
WriteCommitComposeContents(WriteCommitComposeContents),
ComposePull(ComposePull),
ComposeUp(ComposeUp),
ComposeExecution(ComposeExecution),
@@ -121,6 +122,7 @@ pub enum PeripheryRequest {
ImageHistory(ImageHistory),
// Image (Write)
PullImage(PullImage),
DeleteImage(DeleteImage),
PruneImages(PruneImages),

View File

@@ -43,7 +43,7 @@ pub async fn compose_up(
// Will also set additional fields on the reponse.
// Use the env_file_path in the compose command.
let (run_directory, env_file_path) =
write_stack(&stack, git_token, res)
write_stack(&stack, git_token, &mut *res)
.await
.context("Failed to write / clone compose file")?;
@@ -206,7 +206,7 @@ pub async fn compose_up(
"compose pull",
run_directory.as_ref(),
format!(
"{docker_compose} -p {project_name} -f {file_args}{env_file} pull{service_arg}",
"{docker_compose} -p {project_name} -f {file_args}{env_file}{additional_env_files} pull{service_arg}",
),
false,
)
@@ -289,7 +289,7 @@ pub async fn compose_up(
// Run compose up
let extra_args = parse_extra_args(&stack.config.extra_args);
let command = format!(
"{docker_compose} -p {project_name} -f {file_args}{env_file} up -d{extra_args}{service_arg}",
"{docker_compose} -p {project_name} -f {file_args}{env_file}{additional_env_files} up -d{extra_args}{service_arg}",
);
let log = if stack.config.skip_secret_interp {
@@ -330,13 +330,35 @@ pub async fn compose_up(
Ok(())
}
pub trait WriteStackRes {
fn logs(&mut self) -> &mut Vec<Log>;
fn add_remote_error(&mut self, _contents: FileContents) {}
fn set_commit_hash(&mut self, _hash: Option<String>) {}
fn set_commit_message(&mut self, _message: Option<String>) {}
}
impl<'a> WriteStackRes for &'a mut ComposeUpResponse {
fn logs(&mut self) -> &mut Vec<Log> {
&mut self.logs
}
fn add_remote_error(&mut self, contents: FileContents) {
self.remote_errors.push(contents);
}
fn set_commit_hash(&mut self, hash: Option<String>) {
self.commit_hash = hash;
}
fn set_commit_message(&mut self, message: Option<String>) {
self.commit_message = message;
}
}
/// Either writes the stack file_contents to a file, or clones the repo.
/// Returns (run_directory, env_file_path)
async fn write_stack<'a>(
stack: &'a Stack,
pub async fn write_stack(
stack: &Stack,
git_token: Option<String>,
res: &mut ComposeUpResponse,
) -> anyhow::Result<(PathBuf, Option<&'a str>)> {
mut res: impl WriteStackRes,
) -> anyhow::Result<(PathBuf, Option<&str>)> {
let root = periphery_config()
.stack_dir
.join(to_komodo_name(&stack.name));
@@ -361,7 +383,7 @@ async fn write_stack<'a>(
.skip_secret_interp
.then_some(&periphery_config().secrets),
run_directory.as_ref(),
&mut res.logs,
res.logs(),
)
.await
{
@@ -399,7 +421,7 @@ async fn write_stack<'a>(
.skip_secret_interp
.then_some(&periphery_config().secrets),
run_directory.as_ref(),
&mut res.logs,
res.logs(),
)
.await
{
@@ -452,9 +474,9 @@ async fn write_stack<'a>(
Err(e) => {
let error = format_serror(&e.into());
res
.logs
.logs()
.push(Log::error("no git token", error.clone()));
res.remote_errors.push(FileContents {
res.add_remote_error(FileContents {
path: Default::default(),
contents: error,
});
@@ -523,8 +545,10 @@ async fn write_stack<'a>(
let error = format_serror(
&e.context("failed to pull stack repo").into(),
);
res.logs.push(Log::error("pull stack repo", error.clone()));
res.remote_errors.push(FileContents {
res
.logs()
.push(Log::error("pull stack repo", error.clone()));
res.add_remote_error(FileContents {
path: Default::default(),
contents: error,
});
@@ -534,11 +558,11 @@ async fn write_stack<'a>(
}
};
res.logs.extend(logs);
res.commit_hash = commit_hash;
res.commit_message = commit_message;
res.logs().extend(logs);
res.set_commit_hash(commit_hash);
res.set_commit_message(commit_message);
if !all_logs_success(&res.logs) {
if !all_logs_success(res.logs()) {
return Err(anyhow!("Stopped after repo pull failure"));
}

View File

@@ -1,10 +1,17 @@
use anyhow::Context;
use std::path::PathBuf;
use anyhow::{anyhow, Context};
use komodo_client::{
entities::{EnvironmentVar, SearchCombinator},
entities::{
stack::Stack, to_komodo_name, CloneArgs, EnvironmentVar,
SearchCombinator,
},
parsers::QUOTE_PATTERN,
};
use periphery_client::api::git::PullOrCloneRepo;
use resolver_api::Resolve;
use crate::config::periphery_config;
use crate::{config::periphery_config, State};
pub fn git_token(
domain: &str,
@@ -89,3 +96,65 @@ pub fn interpolate_variables(
true,
)
}
/// Returns path to root directory of the stack repo.
pub async fn pull_or_clone_stack(
stack: &Stack,
git_token: Option<String>,
) -> anyhow::Result<PathBuf> {
if stack.config.files_on_host {
return Err(anyhow!(
"Wrong method called for files on host stack"
));
}
if stack.config.repo.is_empty() {
return Err(anyhow!("Repo is not configured"));
}
let root = periphery_config()
.stack_dir
.join(to_komodo_name(&stack.name));
let mut args: CloneArgs = stack.into();
// Set the clone destination to the one created for this run
args.destination = Some(root.display().to_string());
let git_token = match git_token {
Some(token) => Some(token),
None => {
if !stack.config.git_account.is_empty() {
match crate::helpers::git_token(
&stack.config.git_provider,
&stack.config.git_account,
) {
Ok(token) => Some(token.to_string()),
Err(e) => {
return Err(
e.context("Failed to find required git token"),
);
}
}
} else {
None
}
}
};
State
.resolve(
PullOrCloneRepo {
args,
git_token,
environment: vec![],
env_file_path: stack.config.env_file_path.clone(),
skip_secret_interp: stack.config.skip_secret_interp,
// repo replacer only needed for on_clone / on_pull,
// which aren't available for stacks
replacers: Default::default(),
},
(),
)
.await?;
Ok(root)
}

View File

@@ -27,7 +27,7 @@ pub struct RunAction {
pub action: String,
}
/// Runs multiple Actions in parallel that match pattern. Response: [BatchExecutionResult]
/// Runs multiple Actions in parallel that match pattern. Response: [BatchExecutionResponse]
#[typeshare]
#[derive(
Debug,

View File

@@ -36,7 +36,7 @@ pub struct RunBuild {
//
/// Runs multiple builds in parallel that match pattern. Response: [BatchExecutionResult].
/// Runs multiple builds in parallel that match pattern. Response: [BatchExecutionResponse].
#[typeshare]
#[derive(
Debug,

View File

@@ -41,7 +41,7 @@ pub struct Deploy {
//
/// Deploys multiple Deployments in parallel that match pattern. Response: [BatchExecutionResult].
/// Deploys multiple Deployments in parallel that match pattern. Response: [BatchExecutionResponse].
#[typeshare]
#[derive(
Serialize,
@@ -71,6 +71,27 @@ pub struct BatchDeploy {
//
/// Pulls the image for the target deployment. Response: [Update]
#[typeshare]
#[derive(
Serialize,
Deserialize,
Debug,
Clone,
PartialEq,
Request,
EmptyTraits,
Parser,
)]
#[empty_traits(KomodoExecuteRequest)]
#[response(Update)]
pub struct PullDeployment {
/// Name or id
pub deployment: String,
}
//
/// Starts the container for the target deployment. Response: [Update]
///
/// 1. Runs `docker start ${container_name}`.
@@ -220,7 +241,7 @@ pub struct DestroyDeployment {
//
/// Destroys multiple Deployments in parallel that match pattern. Response: [BatchExecutionResult].
/// Destroys multiple Deployments in parallel that match pattern. Response: [BatchExecutionResponse].
#[typeshare]
#[derive(
Serialize,

View File

@@ -73,6 +73,7 @@ pub enum Execution {
// DEPLOYMENT
Deploy(Deploy),
BatchDeploy(BatchDeploy),
PullDeployment(PullDeployment),
StartDeployment(StartDeployment),
RestartDeployment(RestartDeployment),
PauseDeployment(PauseDeployment),
@@ -124,6 +125,7 @@ pub enum Execution {
BatchDeployStack(BatchDeployStack),
DeployStackIfChanged(DeployStackIfChanged),
BatchDeployStackIfChanged(BatchDeployStackIfChanged),
PullStack(PullStack),
StartStack(StartStack),
RestartStack(RestartStack),
PauseStack(PauseStack),

View File

@@ -27,7 +27,7 @@ pub struct RunProcedure {
pub procedure: String,
}
/// Runs multiple Procedures in parallel that match pattern. Response: [BatchExecutionResult].
/// Runs multiple Procedures in parallel that match pattern. Response: [BatchExecutionResponse].
#[typeshare]
#[derive(
Debug,

View File

@@ -39,7 +39,7 @@ pub struct CloneRepo {
//
/// Clones multiple Repos in parallel that match pattern. Response: [BatchExecutionResult].
/// Clones multiple Repos in parallel that match pattern. Response: [BatchExecutionResponse].
#[typeshare]
#[derive(
Debug,
@@ -95,7 +95,7 @@ pub struct PullRepo {
//
/// Pulls multiple Repos in parallel that match pattern. Response: [BatchExecutionResult].
/// Pulls multiple Repos in parallel that match pattern. Response: [BatchExecutionResponse].
#[typeshare]
#[derive(
Debug,
@@ -155,7 +155,7 @@ pub struct BuildRepo {
//
/// Builds multiple Repos in parallel that match pattern. Response: [BatchExecutionResult].
/// Builds multiple Repos in parallel that match pattern. Response: [BatchExecutionResponse].
#[typeshare]
#[derive(
Debug,

View File

@@ -25,6 +25,8 @@ use super::{BatchExecutionResponse, KomodoExecuteRequest};
pub struct DeployStack {
/// Id or name
pub stack: String,
/// Optionally specify a specific service to "compose up"
pub service: Option<String>,
/// Override the default termination max time.
/// Only used if the stack needs to be taken down first.
pub stop_time: Option<i32>,
@@ -32,7 +34,7 @@ pub struct DeployStack {
//
/// Deploys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResult].
/// Deploys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResponse].
#[typeshare]
#[derive(
Serialize,
@@ -88,7 +90,7 @@ pub struct DeployStackIfChanged {
//
/// Deploys multiple Stacks if changed in parallel that match pattern. Response: [BatchExecutionResult].
/// Deploys multiple Stacks if changed in parallel that match pattern. Response: [BatchExecutionResponse].
#[typeshare]
#[derive(
Serialize,
@@ -118,6 +120,29 @@ pub struct BatchDeployStackIfChanged {
//
/// Pulls images for the target stack. `docker compose pull`. Response: [Update]
#[typeshare]
#[derive(
Debug,
Clone,
PartialEq,
Serialize,
Deserialize,
Request,
EmptyTraits,
Parser,
)]
#[empty_traits(KomodoExecuteRequest)]
#[response(Update)]
pub struct PullStack {
/// Id or name
pub stack: String,
/// Optionally specify a specific service to start
pub service: Option<String>,
}
//
/// Starts the target stack. `docker compose start`. Response: [Update]
#[typeshare]
#[derive(
@@ -254,6 +279,8 @@ pub struct StopStack {
pub struct DestroyStack {
/// Id or name
pub stack: String,
/// Optionally specify a specific service to destroy
pub service: Option<String>,
/// Pass `--remove-orphans`
#[serde(default)]
pub remove_orphans: bool,
@@ -263,7 +290,7 @@ pub struct DestroyStack {
//
/// Destroys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResult].
/// Destroys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResponse].
#[typeshare]
#[derive(
Serialize,

View File

@@ -179,7 +179,7 @@ impl GetBuildMonthlyStatsResponse {
/// Retrieve versions of the build that were built in the past and available for deployment,
/// sorted by most recent first.
/// Response: [GetBuildVersionsResponse].
/// Response: [ListBuildVersionsResponse].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Default, Request, EmptyTraits,

View File

@@ -170,7 +170,7 @@ pub type SearchDeploymentLogResponse = Log;
//
/// Get the deployment container's stats using `docker stats`.
/// Response: [DockerContainerStats].
/// Response: [GetDeploymentStatsResponse].
///
/// Note. This call will hit the underlying server directly for most up to date stats.
#[typeshare]

View File

@@ -27,7 +27,7 @@ pub type GetGitProviderAccountResponse = GitProviderAccount;
//
/// List git provider accounts matching optional query.
/// Response: [ListGitProvidersResponse].
/// Response: [ListGitProviderAccountsResponse].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Default, Request, EmptyTraits,
@@ -64,7 +64,7 @@ pub type GetDockerRegistryAccountResponse = DockerRegistryAccount;
//
/// List docker registry accounts matching optional query.
/// Response: [ListDockerRegistrysResponse].
/// Response: [ListDockerRegistryAccountsResponse].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Default, Request, EmptyTraits,

View File

@@ -373,7 +373,7 @@ pub type SearchContainerLogResponse = Log;
//
/// Inspect a docker container on the server. Response: [Container].
/// Find the attached resource for a container. Either Deployment or Stack. Response: [GetResourceMatchingContainerResponse].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
@@ -388,6 +388,7 @@ pub struct GetResourceMatchingContainer {
pub container: String,
}
/// Response for [GetResourceMatchingContainer]. Resource is either Deployment, Stack, or None.
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GetResourceMatchingContainerResponse {

View File

@@ -51,7 +51,7 @@ pub type ListStackServicesResponse = Vec<StackService>;
//
/// Get a stack service's log. Response: [GetStackContainersResponse].
/// Get a stack service's log. Response: [GetStackServiceLogResponse].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,

View File

@@ -46,6 +46,22 @@ pub struct CopyDeployment {
//
/// Create a Deployment from an existing container. Response: [Deployment].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
)]
#[empty_traits(KomodoWriteRequest)]
#[response(Deployment)]
pub struct CreateDeploymentFromContainer {
/// The name or id of the existing container.
pub name: String,
/// The server id or name on which container exists.
pub server: String,
}
//
/// Deletes the deployment at the given id, and returns the deleted deployment.
/// Response: [Deployment].
///

View File

@@ -47,7 +47,7 @@ pub type UpdateGitProviderAccountResponse = GitProviderAccount;
//
/// **Admin only.** Delete a git provider account.
/// Response: [User].
/// Response: [DeleteGitProviderAccountResponse].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,

View File

@@ -4,14 +4,14 @@ mod conversion;
mod environment;
mod file_contents;
mod labels;
mod maybe_string_i64;
mod string_list;
mod term_signal_labels;
mod maybe_string_i64;
pub use conversion::*;
pub use environment::*;
pub use file_contents::*;
pub use labels::*;
pub use maybe_string_i64::*;
pub use string_list::*;
pub use term_signal_labels::*;
pub use maybe_string_i64::*;

View File

@@ -144,6 +144,20 @@ pub enum AlertData {
to: DeploymentState,
},
/// A Deployment has an image update available
DeploymentImageUpdateAvailable {
/// The id of the deployment
id: String,
/// The name of the deployment
name: String,
/// The server id of server that the deployment is on
server_id: String,
/// The server name
server_name: String,
/// The image with update
image: String,
},
/// A stack's state has changed unexpectedly.
StackStateChange {
/// The id of the stack
@@ -160,6 +174,22 @@ pub enum AlertData {
to: StackState,
},
/// A Stack has an image update available
StackImageUpdateAvailable {
/// The id of the stack
id: String,
/// The name of the stack
name: String,
/// The server id of server that the stack is on
server_id: String,
/// The server name
server_name: String,
/// The service name to update
service: String,
/// The image with update
image: String,
},
/// An AWS builder failed to terminate.
AwsBuilderTerminationFailed {
/// The id of the aws instance which failed to terminate

View File

@@ -252,10 +252,10 @@ pub struct BuildConfig {
/// Secret arguments.
///
/// These values remain hidden in the final image by using
/// docker secret mounts. See [https://docs.docker.com/build/building/secrets].
/// docker secret mounts. See <https://docs.docker.com/build/building/secrets>.
///
/// The values can be used in RUN commands:
/// ```
/// ```sh
/// RUN --mount=type=secret,id=SECRET_KEY \
/// SECRET_KEY=$(cat /run/secrets/SECRET_KEY) ...
/// ```

View File

@@ -41,6 +41,8 @@ pub struct DeploymentListItemInfo {
pub status: Option<String>,
/// The image attached to the deployment.
pub image: String,
/// Whether there is a newer image available at the same tag.
pub update_available: bool,
/// The server that deployment sits on.
pub server_id: String,
/// An attached Komodo Build, if it exists.
@@ -87,6 +89,19 @@ pub struct DeploymentConfig {
#[builder(default)]
pub redeploy_on_build: bool,
/// Whether to poll for any updates to the image.
#[serde(default)]
#[builder(default)]
pub poll_for_updates: bool,
/// Whether to automatically redeploy when
/// newer a image is found. Will implicitly
/// enable `poll_for_updates`, you don't need to
/// enable both.
#[serde(default)]
#[builder(default)]
pub auto_update: bool,
/// Whether to send ContainerStateChange alerts for this deployment.
#[serde(default = "default_send_alerts")]
#[builder(default = "default_send_alerts()")]
@@ -217,6 +232,8 @@ impl Default for DeploymentConfig {
image_registry_account: Default::default(),
skip_secret_interp: Default::default(),
redeploy_on_build: Default::default(),
poll_for_updates: Default::default(),
auto_update: Default::default(),
term_signal_labels: Default::default(),
termination_signal: Default::default(),
termination_timeout: default_termination_timeout(),
@@ -417,6 +434,7 @@ pub fn term_signal_labels_from_str(
#[typeshare]
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub struct DeploymentActionState {
pub pulling: bool,
pub deploying: bool,
pub starting: bool,
pub restarting: bool,

View File

@@ -46,7 +46,7 @@ pub mod logger;
pub mod permission;
/// Subtypes of [Procedure][procedure::Procedure].
pub mod procedure;
/// Subtypes of [ProviderAccount][provider::ProviderAccount]
/// Subtypes of [GitProviderAccount][provider::GitProviderAccount] and [DockerRegistryAccount][provider::DockerRegistryAccount]
pub mod provider;
/// Subtypes of [Repo][repo::Repo].
pub mod repo;
@@ -392,7 +392,7 @@ pub struct CloneArgs {
pub provider: String,
/// Use https (vs http).
pub https: bool,
/// Full repo identifier. <namespace>/<repo_name>
/// Full repo identifier. {namespace}/{repo_name}
pub repo: Option<String>,
/// Git Branch. Default: `main`
pub branch: String,
@@ -677,6 +677,7 @@ pub enum Operation {
DeleteStack,
WriteStackContents,
RefreshStackCache,
PullStack,
DeployStack,
StartStack,
RestartStack,
@@ -686,11 +687,14 @@ pub enum Operation {
DestroyStack,
// stack (service)
DeployStackService,
PullStackService,
StartStackService,
RestartStackService,
PauseStackService,
UnpauseStackService,
StopStackService,
DestroyStackService,
// deployment
CreateDeployment,
@@ -698,6 +702,7 @@ pub enum Operation {
RenameDeployment,
DeleteDeployment,
Deploy,
PullDeployment,
StartDeployment,
RestartDeployment,
PauseDeployment,

View File

@@ -87,7 +87,7 @@ pub struct DockerRegistryAccount {
///
/// For docker registry, this can include 'http://...',
/// however this is not recommended and won't work unless "insecure registries" are enabled
/// on your hosts. See [https://docs.docker.com/reference/cli/dockerd/#insecure-registries].
/// on your hosts. See <https://docs.docker.com/reference/cli/dockerd/#insecure-registries>.
#[cfg_attr(feature = "mongo", index)]
#[serde(default = "default_registry_domain")]
#[partial_default(default_registry_domain())]

View File

@@ -78,10 +78,10 @@ pub struct StackListItemInfo {
pub state: StackState,
/// A string given by docker conveying the status of the stack.
pub status: Option<String>,
/// The service names that are part of the stack.
/// The services that are part of the stack.
/// If deployed, will be `deployed_services`.
/// Otherwise, its `latest_services`
pub services: Vec<String>,
pub services: Vec<StackServiceWithUpdate>,
/// Whether the compose project is missing on the host.
/// Ie, it does not show up in `docker compose ls`.
/// If true, and the stack is not Down, this is an unhealthy state.
@@ -95,6 +95,16 @@ pub struct StackListItemInfo {
pub latest_hash: Option<String>,
}
#[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StackServiceWithUpdate {
pub service: String,
/// The service's image
pub image: String,
/// Whether there is a newer image available for this service
pub update_available: bool,
}
#[typeshare]
#[derive(
Debug,
@@ -224,6 +234,19 @@ pub struct StackConfig {
#[builder(default)]
pub run_build: bool,
/// Whether to poll for any updates to the images.
#[serde(default)]
#[builder(default)]
pub poll_for_updates: bool,
/// Whether to automatically redeploy when
/// newer images are found. Will implicitly
/// enable `poll_for_updates`, you don't need to
/// enable both.
#[serde(default)]
#[builder(default)]
pub auto_update: bool,
/// Whether to run `docker compose down` before `compose up`.
#[serde(default)]
#[builder(default)]
@@ -462,6 +485,8 @@ impl Default for StackConfig {
registry_account: Default::default(),
file_contents: Default::default(),
auto_pull: default_auto_pull(),
poll_for_updates: Default::default(),
auto_update: Default::default(),
ignore_services: Default::default(),
pre_deploy: Default::default(),
extra_args: Default::default(),
@@ -521,6 +546,9 @@ pub struct StackServiceNames {
/// This stores only 1. and 2., ie stacko-mongo.
/// Containers will be matched via regex like `^container_name-?[0-9]*$``
pub container_name: String,
/// The services image.
#[serde(default)]
pub image: String,
}
#[typeshare]
@@ -528,13 +556,18 @@ pub struct StackServiceNames {
pub struct StackService {
/// The service name
pub service: String,
/// The service image
pub image: String,
/// The container
pub container: Option<ContainerListItem>,
/// Whether there is an update available for this services image.
pub update_available: bool,
}
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)]
pub struct StackActionState {
pub pulling: bool,
pub deploying: bool,
pub starting: bool,
pub restarting: bool,

View File

@@ -55,13 +55,13 @@ pub fn komodo_client() -> &'static KomodoClient {
/// Default environment variables for the [KomodoClient].
#[derive(Deserialize)]
struct KomodoEnv {
pub struct KomodoEnv {
/// KOMODO_ADDRESS
komodo_address: String,
pub komodo_address: String,
/// KOMODO_API_KEY
komodo_api_key: String,
pub komodo_api_key: String,
/// KOMODO_API_SECRET
komodo_api_secret: String,
pub komodo_api_secret: String,
}
/// Client to interface with [Komodo](https://komo.do/docs/api#rust-client)

View File

@@ -1,6 +1,6 @@
{
"name": "komodo_client",
"version": "1.16.7",
"version": "1.16.9",
"description": "Komodo client package",
"homepage": "https://komo.do",
"main": "dist/lib.js",

View File

@@ -219,6 +219,7 @@ export type WriteResponses = {
// ==== DEPLOYMENT ====
CreateDeployment: Types.Deployment;
CopyDeployment: Types.Deployment;
CreateDeploymentFromContainer: Types.Deployment;
DeleteDeployment: Types.Deployment;
UpdateDeployment: Types.Deployment;
RenameDeployment: Types.Update;
@@ -350,6 +351,7 @@ export type ExecuteResponses = {
// ==== DEPLOYMENT ====
Deploy: Types.Update;
BatchDeploy: Types.BatchExecutionResponse;
PullDeployment: Types.Update;
StartDeployment: Types.Update;
RestartDeployment: Types.Update;
PauseDeployment: Types.Update;
@@ -391,6 +393,7 @@ export type ExecuteResponses = {
BatchDeployStack: Types.BatchExecutionResponse;
DeployStackIfChanged: Types.Update;
BatchDeployStackIfChanged: Types.BatchExecutionResponse;
PullStack: Types.Update;
StartStack: Types.Update;
RestartStack: Types.Update;
StopStack: Types.Update;

View File

@@ -1,5 +1,5 @@
/*
Generated by typeshare 1.11.0
Generated by typeshare 1.12.0
*/
export interface MongoIdObj {
@@ -319,10 +319,10 @@ export interface BuildConfig {
* Secret arguments.
*
* These values remain hidden in the final image by using
* docker secret mounts. See [https://docs.docker.com/build/building/secrets].
* docker secret mounts. See <https://docs.docker.com/build/building/secrets>.
*
* The values can be used in RUN commands:
* ```
* ```sh
* RUN --mount=type=secret,id=SECRET_KEY \
* SECRET_KEY=$(cat /run/secrets/SECRET_KEY) ...
* ```
@@ -434,6 +434,7 @@ export type Execution =
| { type: "CancelBuild", params: CancelBuild }
| { type: "Deploy", params: Deploy }
| { type: "BatchDeploy", params: BatchDeploy }
| { type: "PullDeployment", params: PullDeployment }
| { type: "StartDeployment", params: StartDeployment }
| { type: "RestartDeployment", params: RestartDeployment }
| { type: "PauseDeployment", params: PauseDeployment }
@@ -475,6 +476,7 @@ export type Execution =
| { type: "BatchDeployStack", params: BatchDeployStack }
| { type: "DeployStackIfChanged", params: DeployStackIfChanged }
| { type: "BatchDeployStackIfChanged", params: BatchDeployStackIfChanged }
| { type: "PullStack", params: PullStack }
| { type: "StartStack", params: StartStack }
| { type: "RestartStack", params: RestartStack }
| { type: "PauseStack", params: PauseStack }
@@ -559,7 +561,7 @@ export interface DockerRegistryAccount {
*
* For docker registry, this can include 'http://...',
* however this is not recommended and won't work unless "insecure registries" are enabled
* on your hosts. See [https://docs.docker.com/reference/cli/dockerd/#insecure-registries].
* on your hosts. See <https://docs.docker.com/reference/cli/dockerd/#insecure-registries>.
*/
domain: string;
/** The account username */
@@ -781,6 +783,13 @@ export interface DeploymentConfig {
skip_secret_interp?: boolean;
/** Whether to redeploy the deployment whenever the attached build finishes. */
redeploy_on_build?: boolean;
/** Whether to poll for any updates to the image. */
poll_for_updates?: boolean;
/**
* Whether to automatically redeploy when a
* newer image is found.
*/
auto_update?: boolean;
/** Whether to send ContainerStateChange alerts for this deployment. */
send_alerts: boolean;
/** Configure quick links that are displayed in the resource header */
@@ -859,6 +868,8 @@ export interface DeploymentListItemInfo {
status?: string;
/** The image attached to the deployment. */
image: string;
/** Whether there is a newer image available at the same tag. */
update_available: boolean;
/** The server that deployment sits on. */
server_id: string;
/** An attached Komodo Build, if it exists. */
@@ -976,6 +987,19 @@ export type AlertData =
from: DeploymentState;
/** The current container state */
to: DeploymentState;
}}
/** A Deployment has an image update available */
| { type: "DeploymentImageUpdateAvailable", data: {
/** The id of the deployment */
id: string;
/** The name of the deployment */
name: string;
/** The server id of server that the deployment is on */
server_id: string;
/** The server name */
server_name: string;
/** The image with update */
image: string;
}}
/** A stack's state has changed unexpectedly. */
| { type: "StackStateChange", data: {
@@ -991,6 +1015,21 @@ export type AlertData =
from: StackState;
/** The current stack state */
to: StackState;
}}
/** A Stack has an image update available */
| { type: "StackImageUpdateAvailable", data: {
/** The id of the stack */
id: string;
/** The name of the stack */
name: string;
/** The server id of server that the stack is on */
server_id: string;
/** The server name */
server_name: string;
/** The service name to update */
service: string;
/** The image with update */
image: string;
}}
/** An AWS builder failed to terminate. */
| { type: "AwsBuilderTerminationFailed", data: {
@@ -1080,6 +1119,7 @@ export interface Log {
export type GetContainerLogResponse = Log;
export interface DeploymentActionState {
pulling: boolean;
deploying: boolean;
starting: boolean;
restarting: boolean;
@@ -1469,6 +1509,7 @@ export type ServerTemplate = Resource<ServerTemplateConfig, undefined>;
export type GetServerTemplateResponse = ServerTemplate;
export interface StackActionState {
pulling: boolean;
deploying: boolean;
starting: boolean;
restarting: boolean;
@@ -1505,6 +1546,13 @@ export interface StackConfig {
* Combine with build_extra_args for custom behaviors.
*/
run_build?: boolean;
/** Whether to poll for any updates to the images. */
poll_for_updates?: boolean;
/**
* Whether to automatically redeploy when a
* newer images are found.
*/
auto_update?: boolean;
/** Whether to run `docker compose down` before `compose up`. */
destroy_before_deploy?: boolean;
/** Whether to skip secret interpolation into the stack environment variables. */
@@ -1643,6 +1691,8 @@ export interface StackServiceNames {
* Containers will be matched via regex like `^container_name-?[0-9]*$``
*/
container_name: string;
/** The services image. */
image?: string;
}
export interface StackInfo {
@@ -1822,6 +1872,7 @@ export enum Operation {
DeleteStack = "DeleteStack",
WriteStackContents = "WriteStackContents",
RefreshStackCache = "RefreshStackCache",
PullStack = "PullStack",
DeployStack = "DeployStack",
StartStack = "StartStack",
RestartStack = "RestartStack",
@@ -1829,16 +1880,20 @@ export enum Operation {
UnpauseStack = "UnpauseStack",
StopStack = "StopStack",
DestroyStack = "DestroyStack",
DeployStackService = "DeployStackService",
PullStackService = "PullStackService",
StartStackService = "StartStackService",
RestartStackService = "RestartStackService",
PauseStackService = "PauseStackService",
UnpauseStackService = "UnpauseStackService",
StopStackService = "StopStackService",
DestroyStackService = "DestroyStackService",
CreateDeployment = "CreateDeployment",
UpdateDeployment = "UpdateDeployment",
RenameDeployment = "RenameDeployment",
DeleteDeployment = "DeleteDeployment",
Deploy = "Deploy",
PullDeployment = "PullDeployment",
StartDeployment = "StartDeployment",
RestartDeployment = "RestartDeployment",
PauseDeployment = "PauseDeployment",
@@ -3180,8 +3235,12 @@ export type ListServersResponse = ServerListItem[];
export interface StackService {
/** The service name */
service: string;
/** The service image */
image: string;
/** The container */
container?: ContainerListItem;
/** Whether there is an update available for this services image. */
update_available: boolean;
}
export type ListStackServicesResponse = StackService[];
@@ -3209,6 +3268,14 @@ 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 server that stack is deployed on. */
server_id: string;
@@ -3227,11 +3294,11 @@ export interface StackListItemInfo {
/** A string given by docker conveying the status of the stack. */
status?: string;
/**
* The service names that are part of the stack.
* The services that are part of the stack.
* If deployed, will be `deployed_services`.
* Otherwise, its `latest_services`
*/
services: string[];
services: StackServiceWithUpdate[];
/**
* Whether the compose project is missing on the host.
* Ie, it does not show up in `docker compose ls`.
@@ -3535,7 +3602,7 @@ export interface AwsServerTemplateConfig {
user_data: string;
}
/** Builds multiple Repos in parallel that match pattern. Response: [BatchExecutionResult]. */
/** Builds multiple Repos in parallel that match pattern. Response: [BatchExecutionResponse]. */
export interface BatchBuildRepo {
/**
* Id or name or wildcard pattern or regex.
@@ -3552,7 +3619,7 @@ export interface BatchBuildRepo {
pattern: string;
}
/** Clones multiple Repos in parallel that match pattern. Response: [BatchExecutionResult]. */
/** Clones multiple Repos in parallel that match pattern. Response: [BatchExecutionResponse]. */
export interface BatchCloneRepo {
/**
* Id or name or wildcard pattern or regex.
@@ -3569,7 +3636,7 @@ export interface BatchCloneRepo {
pattern: string;
}
/** Deploys multiple Deployments in parallel that match pattern. Response: [BatchExecutionResult]. */
/** Deploys multiple Deployments in parallel that match pattern. Response: [BatchExecutionResponse]. */
export interface BatchDeploy {
/**
* Id or name or wildcard pattern or regex.
@@ -3586,7 +3653,7 @@ export interface BatchDeploy {
pattern: string;
}
/** Deploys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResult]. */
/** Deploys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResponse]. */
export interface BatchDeployStack {
/**
* Id or name or wildcard pattern or regex.
@@ -3603,7 +3670,7 @@ export interface BatchDeployStack {
pattern: string;
}
/** Deploys multiple Stacks if changed in parallel that match pattern. Response: [BatchExecutionResult]. */
/** Deploys multiple Stacks if changed in parallel that match pattern. Response: [BatchExecutionResponse]. */
export interface BatchDeployStackIfChanged {
/**
* Id or name or wildcard pattern or regex.
@@ -3620,7 +3687,7 @@ export interface BatchDeployStackIfChanged {
pattern: string;
}
/** Destroys multiple Deployments in parallel that match pattern. Response: [BatchExecutionResult]. */
/** Destroys multiple Deployments in parallel that match pattern. Response: [BatchExecutionResponse]. */
export interface BatchDestroyDeployment {
/**
* Id or name or wildcard pattern or regex.
@@ -3637,7 +3704,7 @@ export interface BatchDestroyDeployment {
pattern: string;
}
/** Destroys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResult]. */
/** Destroys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResponse]. */
export interface BatchDestroyStack {
/**
* Id or name or wildcard pattern or regex.
@@ -3659,7 +3726,7 @@ export interface BatchExecutionResponseItemErr {
error: _Serror;
}
/** Pulls multiple Repos in parallel that match pattern. Response: [BatchExecutionResult]. */
/** Pulls multiple Repos in parallel that match pattern. Response: [BatchExecutionResponse]. */
export interface BatchPullRepo {
/**
* Id or name or wildcard pattern or regex.
@@ -3676,7 +3743,7 @@ export interface BatchPullRepo {
pattern: string;
}
/** Runs multiple Actions in parallel that match pattern. Response: [BatchExecutionResult] */
/** Runs multiple Actions in parallel that match pattern. Response: [BatchExecutionResponse] */
export interface BatchRunAction {
/**
* Id or name or wildcard pattern or regex.
@@ -3693,7 +3760,7 @@ export interface BatchRunAction {
pattern: string;
}
/** Runs multiple builds in parallel that match pattern. Response: [BatchExecutionResult]. */
/** Runs multiple builds in parallel that match pattern. Response: [BatchExecutionResponse]. */
export interface BatchRunBuild {
/**
* Id or name or wildcard pattern or regex.
@@ -3710,7 +3777,7 @@ export interface BatchRunBuild {
pattern: string;
}
/** Runs multiple Procedures in parallel that match pattern. Response: [BatchExecutionResult]. */
/** Runs multiple Procedures in parallel that match pattern. Response: [BatchExecutionResponse]. */
export interface BatchRunProcedure {
/**
* Id or name or wildcard pattern or regex.
@@ -3778,7 +3845,7 @@ export interface CloneArgs {
provider: string;
/** Use https (vs http). */
https: boolean;
/** Full repo identifier. <namespace>/<repo_name> */
/** Full repo identifier. {namespace}/{repo_name} */
repo?: string;
/** Git Branch. Default: `main` */
branch: string;
@@ -4034,6 +4101,14 @@ export interface CreateDeployment {
config?: _PartialDeploymentConfig;
}
/** Create a Deployment from an existing container. Response: [Deployment]. */
export interface CreateDeploymentFromContainer {
/** The name or id of the existing container. */
name: string;
/** The server id or name on which container exists. */
server: string;
}
/**
* **Admin only.** Create a docker registry account.
* Response: [DockerRegistryAccount].
@@ -4320,7 +4395,7 @@ export interface DeleteDockerRegistryAccount {
/**
* **Admin only.** Delete a git provider account.
* Response: [User].
* Response: [DeleteGitProviderAccountResponse].
*/
export interface DeleteGitProviderAccount {
/** The id of the git provider to delete */
@@ -4509,6 +4584,8 @@ export interface Deploy {
export interface DeployStack {
/** Id or name */
stack: string;
/** Optionally specify a specific service to "compose up" */
service?: string;
/**
* Override the default termination max time.
* Only used if the stack needs to be taken down first.
@@ -4567,6 +4644,8 @@ export interface DestroyDeployment {
export interface DestroyStack {
/** Id or name */
stack: string;
/** Optionally specify a specific service to destroy */
service?: string;
/** Pass `--remove-orphans` */
remove_orphans?: boolean;
/** Override the default termination max time. */
@@ -4899,7 +4978,7 @@ export interface GetDeploymentLog {
/**
* Get the deployment container's stats using `docker stats`.
* Response: [DockerContainerStats].
* Response: [GetDeploymentStatsResponse].
*
* Note. This call will hit the underlying server directly for most up to date stats.
*/
@@ -5129,7 +5208,7 @@ export interface GetReposSummaryResponse {
unknown: number;
}
/** Inspect a docker container on the server. Response: [Container]. */
/** Find the attached resource for a container. Either Deployment or Stack. Response: [GetResourceMatchingContainerResponse]. */
export interface GetResourceMatchingContainer {
/** Id or name */
server: string;
@@ -5137,6 +5216,7 @@ export interface GetResourceMatchingContainer {
container: string;
}
/** Response for [GetResourceMatchingContainer]. Resource is either Deployment, Stack, or None. */
export interface GetResourceMatchingContainerResponse {
resource?: ResourceTarget;
}
@@ -5250,7 +5330,7 @@ export interface GetStackActionState {
stack: string;
}
/** Get a stack service's log. Response: [GetStackContainersResponse]. */
/** Get a stack service's log. Response: [GetStackServiceLogResponse]. */
export interface GetStackServiceLog {
/** Id or name */
stack: string;
@@ -5666,7 +5746,7 @@ export interface ListApiKeysForServiceUser {
/**
* Retrieve versions of the build that were built in the past and available for deployment,
* sorted by most recent first.
* Response: [GetBuildVersionsResponse].
* Response: [ListBuildVersionsResponse].
*/
export interface ListBuildVersions {
/** Id or name */
@@ -5797,7 +5877,7 @@ export interface ListDockerRegistriesFromConfig {
/**
* List docker registry accounts matching optional query.
* Response: [ListDockerRegistrysResponse].
* Response: [ListDockerRegistryAccountsResponse].
*/
export interface ListDockerRegistryAccounts {
/** Optionally filter by accounts with a specific domain. */
@@ -5884,7 +5964,7 @@ export interface ListFullStacks {
/**
* List git provider accounts matching optional query.
* Response: [ListGitProvidersResponse].
* Response: [ListGitProviderAccountsResponse].
*/
export interface ListGitProviderAccounts {
/** Optionally filter by accounts with a specific domain. */
@@ -6245,6 +6325,12 @@ export interface PruneVolumes {
server: string;
}
/** Pulls the image for the target deployment. Response: [Update] */
export interface PullDeployment {
/** Name or id */
deployment: string;
}
/**
* Pulls the target repo. Response: [Update].
*
@@ -6258,6 +6344,14 @@ export interface PullRepo {
repo: string;
}
/** Pulls images for the target stack. `docker compose pull`. Response: [Update] */
export interface PullStack {
/** Id or name */
stack: string;
/** Optionally specify a specific service to start */
service?: string;
}
/**
* Push a resource to the front of the users 10 most recently viewed resources.
* Response: [NoData].
@@ -7248,16 +7342,19 @@ export type ExecuteRequest =
| { type: "PruneSystem", params: PruneSystem }
| { type: "Deploy", params: Deploy }
| { type: "BatchDeploy", params: BatchDeploy }
| { type: "PullDeployment", params: PullDeployment }
| { type: "StartDeployment", params: StartDeployment }
| { type: "RestartDeployment", params: RestartDeployment }
| { type: "PauseDeployment", params: PauseDeployment }
| { type: "UnpauseDeployment", params: UnpauseDeployment }
| { type: "StopDeployment", params: StopDeployment }
| { type: "DestroyDeployment", params: DestroyDeployment }
| { type: "BatchDestroyDeployment", params: BatchDestroyDeployment }
| { type: "DeployStack", params: DeployStack }
| { type: "BatchDeployStack", params: BatchDeployStack }
| { type: "DeployStackIfChanged", params: DeployStackIfChanged }
| { type: "BatchDeployStackIfChanged", params: BatchDeployStackIfChanged }
| { type: "PullStack", params: PullStack }
| { type: "StartStack", params: StartStack }
| { type: "RestartStack", params: RestartStack }
| { type: "StopStack", params: StopStack }
@@ -7442,6 +7539,7 @@ export type WriteRequest =
| { type: "CreateNetwork", params: CreateNetwork }
| { type: "CreateDeployment", params: CreateDeployment }
| { type: "CopyDeployment", params: CopyDeployment }
| { type: "CreateDeploymentFromContainer", params: CreateDeploymentFromContainer }
| { type: "DeleteDeployment", params: DeleteDeployment }
| { type: "UpdateDeployment", params: UpdateDeployment }
| { type: "RenameDeployment", params: RenameDeployment }

View File

@@ -122,7 +122,29 @@ pub struct WriteCommitComposeContents {
//
/// Rewrites the compose directory, pulls any images, takes down existing containers,
/// and runs docker compose up.
/// and runs docker compose up. Response: [ComposePullResponse]
#[derive(Debug, Clone, Serialize, Deserialize, Request)]
#[response(ComposePullResponse)]
pub struct ComposePull {
/// The stack to deploy
pub stack: Stack,
/// Only deploy one service
pub service: Option<String>,
/// If provided, use it to login in. Otherwise check periphery local registries.
pub git_token: Option<String>,
/// If provided, use it to login in. Otherwise check periphery local git providers.
pub registry_token: Option<String>,
}
/// Response for [ComposePull]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ComposePullResponse {
pub logs: Vec<Log>,
}
//
/// docker compose up.
#[derive(Debug, Clone, Serialize, Deserialize, Request)]
#[response(ComposeUpResponse)]
pub struct ComposeUp {

View File

@@ -23,6 +23,19 @@ pub struct ImageHistory {
//
#[derive(Debug, Clone, Serialize, Deserialize, Request)]
#[response(Log)]
pub struct PullImage {
/// The name of the image.
pub name: String,
/// Optional account to use to pull the image
pub account: Option<String>,
/// Override registry token for account with one sent from core.
pub token: Option<String>,
}
//
#[derive(Serialize, Deserialize, Debug, Clone, Request)]
#[response(Log)]
pub struct DeleteImage {

View File

@@ -30,7 +30,7 @@ https://${HOST}/listener/${AUTH_TYPE}/${RESOURCE_TYPE}/${ID_OR_NAME}/${EXECUTION
- **`EXECUTION`**:
- Which executions are available depends on the `RESOURCE_TYPE`. Builds only have the `/build` action.
Repos can select between `/pull`, `/clone`, or `/build`. Stacks have `/deploy` and `/refresh`, and Resource Syncs have `/sync` and `/refresh`.
- For **Procedures and Actions**, this will be the **branch to listen to for pushes**, or `__ALL__` to trigger
- For **Procedures and Actions**, this will be the **branch to listen to for pushes**, or `__ANY__` to trigger
on pushes to any branch.
## Create the webhook on the Git Provider

View File

@@ -160,6 +160,7 @@ export type WriteResponses = {
CreateNetwork: Types.Update;
CreateDeployment: Types.Deployment;
CopyDeployment: Types.Deployment;
CreateDeploymentFromContainer: Types.Deployment;
DeleteDeployment: Types.Deployment;
UpdateDeployment: Types.Deployment;
RenameDeployment: Types.Update;
@@ -263,6 +264,7 @@ export type ExecuteResponses = {
PruneSystem: Types.Update;
Deploy: Types.Update;
BatchDeploy: Types.BatchExecutionResponse;
PullDeployment: Types.Update;
StartDeployment: Types.Update;
RestartDeployment: Types.Update;
PauseDeployment: Types.Update;
@@ -290,6 +292,7 @@ export type ExecuteResponses = {
BatchDeployStack: Types.BatchExecutionResponse;
DeployStackIfChanged: Types.Update;
BatchDeployStackIfChanged: Types.BatchExecutionResponse;
PullStack: Types.Update;
StartStack: Types.Update;
RestartStack: Types.Update;
StopStack: Types.Update;

View File

@@ -323,10 +323,10 @@ export interface BuildConfig {
* Secret arguments.
*
* These values remain hidden in the final image by using
* docker secret mounts. See [https://docs.docker.com/build/building/secrets].
* docker secret mounts. See <https://docs.docker.com/build/building/secrets>.
*
* The values can be used in RUN commands:
* ```
* ```sh
* RUN --mount=type=secret,id=SECRET_KEY \
* SECRET_KEY=$(cat /run/secrets/SECRET_KEY) ...
* ```
@@ -453,6 +453,9 @@ export type Execution =
} | {
type: "BatchDeploy";
params: BatchDeploy;
} | {
type: "PullDeployment";
params: PullDeployment;
} | {
type: "StartDeployment";
params: StartDeployment;
@@ -576,6 +579,9 @@ export type Execution =
} | {
type: "BatchDeployStackIfChanged";
params: BatchDeployStackIfChanged;
} | {
type: "PullStack";
params: PullStack;
} | {
type: "StartStack";
params: StartStack;
@@ -666,7 +672,7 @@ export interface DockerRegistryAccount {
*
* For docker registry, this can include 'http://...',
* however this is not recommended and won't work unless "insecure registries" are enabled
* on your hosts. See [https://docs.docker.com/reference/cli/dockerd/#insecure-registries].
* on your hosts. See <https://docs.docker.com/reference/cli/dockerd/#insecure-registries>.
*/
domain: string;
/** The account username */
@@ -879,6 +885,13 @@ export interface DeploymentConfig {
skip_secret_interp?: boolean;
/** Whether to redeploy the deployment whenever the attached build finishes. */
redeploy_on_build?: boolean;
/** Whether to poll for any updates to the image. */
poll_for_updates?: boolean;
/**
* Whether to automatically redeploy when a
* newer image is found.
*/
auto_update?: boolean;
/** Whether to send ContainerStateChange alerts for this deployment. */
send_alerts: boolean;
/** Configure quick links that are displayed in the resource header */
@@ -954,6 +967,8 @@ export interface DeploymentListItemInfo {
status?: string;
/** The image attached to the deployment. */
image: string;
/** Whether there is a newer image available at the same tag. */
update_available: boolean;
/** The server that deployment sits on. */
server_id: string;
/** An attached Komodo Build, if it exists. */
@@ -1076,6 +1091,22 @@ export type AlertData =
to: DeploymentState;
};
}
/** A Deployment has an image update available */
| {
type: "DeploymentImageUpdateAvailable";
data: {
/** The id of the deployment */
id: string;
/** The name of the deployment */
name: string;
/** The server id of server that the deployment is on */
server_id: string;
/** The server name */
server_name: string;
/** The image with update */
image: string;
};
}
/** A stack's state has changed unexpectedly. */
| {
type: "StackStateChange";
@@ -1094,6 +1125,24 @@ export type AlertData =
to: StackState;
};
}
/** A Stack has an image update available */
| {
type: "StackImageUpdateAvailable";
data: {
/** The id of the stack */
id: string;
/** The name of the stack */
name: string;
/** The server id of server that the stack is on */
server_id: string;
/** The server name */
server_name: string;
/** The service name to update */
service: string;
/** The image with update */
image: string;
};
}
/** An AWS builder failed to terminate. */
| {
type: "AwsBuilderTerminationFailed";
@@ -1184,6 +1233,7 @@ export interface Log {
}
export type GetContainerLogResponse = Log;
export interface DeploymentActionState {
pulling: boolean;
deploying: boolean;
starting: boolean;
restarting: boolean;
@@ -1550,6 +1600,7 @@ export type ServerTemplateConfig =
export type ServerTemplate = Resource<ServerTemplateConfig, undefined>;
export type GetServerTemplateResponse = ServerTemplate;
export interface StackActionState {
pulling: boolean;
deploying: boolean;
starting: boolean;
restarting: boolean;
@@ -1584,6 +1635,13 @@ export interface StackConfig {
* Combine with build_extra_args for custom behaviors.
*/
run_build?: boolean;
/** Whether to poll for any updates to the images. */
poll_for_updates?: boolean;
/**
* Whether to automatically redeploy when a
* newer images are found.
*/
auto_update?: boolean;
/** Whether to run `docker compose down` before `compose up`. */
destroy_before_deploy?: boolean;
/** Whether to skip secret interpolation into the stack environment variables. */
@@ -1720,6 +1778,8 @@ export interface StackServiceNames {
* Containers will be matched via regex like `^container_name-?[0-9]*$``
*/
container_name: string;
/** The services image. */
image?: string;
}
export interface StackInfo {
/**
@@ -1886,6 +1946,7 @@ export declare enum Operation {
DeleteStack = "DeleteStack",
WriteStackContents = "WriteStackContents",
RefreshStackCache = "RefreshStackCache",
PullStack = "PullStack",
DeployStack = "DeployStack",
StartStack = "StartStack",
RestartStack = "RestartStack",
@@ -1893,16 +1954,20 @@ export declare enum Operation {
UnpauseStack = "UnpauseStack",
StopStack = "StopStack",
DestroyStack = "DestroyStack",
DeployStackService = "DeployStackService",
PullStackService = "PullStackService",
StartStackService = "StartStackService",
RestartStackService = "RestartStackService",
PauseStackService = "PauseStackService",
UnpauseStackService = "UnpauseStackService",
StopStackService = "StopStackService",
DestroyStackService = "DestroyStackService",
CreateDeployment = "CreateDeployment",
UpdateDeployment = "UpdateDeployment",
RenameDeployment = "RenameDeployment",
DeleteDeployment = "DeleteDeployment",
Deploy = "Deploy",
PullDeployment = "PullDeployment",
StartDeployment = "StartDeployment",
RestartDeployment = "RestartDeployment",
PauseDeployment = "PauseDeployment",
@@ -3111,8 +3176,12 @@ export type ListServersResponse = ServerListItem[];
export interface StackService {
/** The service name */
service: string;
/** The service image */
image: string;
/** The container */
container?: ContainerListItem;
/** Whether there is an update available for this services image. */
update_available: boolean;
}
export type ListStackServicesResponse = StackService[];
export declare enum StackState {
@@ -3137,6 +3206,13 @@ export declare enum StackState {
/** Server not reachable */
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 server that stack is deployed on. */
server_id: string;
@@ -3155,11 +3231,11 @@ export interface StackListItemInfo {
/** A string given by docker conveying the status of the stack. */
status?: string;
/**
* The service names that are part of the stack.
* The services that are part of the stack.
* If deployed, will be `deployed_services`.
* Otherwise, its `latest_services`
*/
services: string[];
services: StackServiceWithUpdate[];
/**
* Whether the compose project is missing on the host.
* Ie, it does not show up in `docker compose ls`.
@@ -3394,7 +3470,7 @@ export interface AwsServerTemplateConfig {
/** The user data to deploy the instance with. */
user_data: string;
}
/** Builds multiple Repos in parallel that match pattern. Response: [BatchExecutionResult]. */
/** Builds multiple Repos in parallel that match pattern. Response: [BatchExecutionResponse]. */
export interface BatchBuildRepo {
/**
* Id or name or wildcard pattern or regex.
@@ -3410,7 +3486,7 @@ export interface BatchBuildRepo {
*/
pattern: string;
}
/** Clones multiple Repos in parallel that match pattern. Response: [BatchExecutionResult]. */
/** Clones multiple Repos in parallel that match pattern. Response: [BatchExecutionResponse]. */
export interface BatchCloneRepo {
/**
* Id or name or wildcard pattern or regex.
@@ -3426,7 +3502,7 @@ export interface BatchCloneRepo {
*/
pattern: string;
}
/** Deploys multiple Deployments in parallel that match pattern. Response: [BatchExecutionResult]. */
/** Deploys multiple Deployments in parallel that match pattern. Response: [BatchExecutionResponse]. */
export interface BatchDeploy {
/**
* Id or name or wildcard pattern or regex.
@@ -3442,7 +3518,7 @@ export interface BatchDeploy {
*/
pattern: string;
}
/** Deploys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResult]. */
/** Deploys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResponse]. */
export interface BatchDeployStack {
/**
* Id or name or wildcard pattern or regex.
@@ -3458,7 +3534,7 @@ export interface BatchDeployStack {
*/
pattern: string;
}
/** Deploys multiple Stacks if changed in parallel that match pattern. Response: [BatchExecutionResult]. */
/** Deploys multiple Stacks if changed in parallel that match pattern. Response: [BatchExecutionResponse]. */
export interface BatchDeployStackIfChanged {
/**
* Id or name or wildcard pattern or regex.
@@ -3474,7 +3550,7 @@ export interface BatchDeployStackIfChanged {
*/
pattern: string;
}
/** Destroys multiple Deployments in parallel that match pattern. Response: [BatchExecutionResult]. */
/** Destroys multiple Deployments in parallel that match pattern. Response: [BatchExecutionResponse]. */
export interface BatchDestroyDeployment {
/**
* Id or name or wildcard pattern or regex.
@@ -3490,7 +3566,7 @@ export interface BatchDestroyDeployment {
*/
pattern: string;
}
/** Destroys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResult]. */
/** Destroys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResponse]. */
export interface BatchDestroyStack {
/**
* Id or name or wildcard pattern or regex.
@@ -3510,7 +3586,7 @@ export interface BatchExecutionResponseItemErr {
name: string;
error: _Serror;
}
/** Pulls multiple Repos in parallel that match pattern. Response: [BatchExecutionResult]. */
/** Pulls multiple Repos in parallel that match pattern. Response: [BatchExecutionResponse]. */
export interface BatchPullRepo {
/**
* Id or name or wildcard pattern or regex.
@@ -3526,7 +3602,7 @@ export interface BatchPullRepo {
*/
pattern: string;
}
/** Runs multiple Actions in parallel that match pattern. Response: [BatchExecutionResult] */
/** Runs multiple Actions in parallel that match pattern. Response: [BatchExecutionResponse] */
export interface BatchRunAction {
/**
* Id or name or wildcard pattern or regex.
@@ -3542,7 +3618,7 @@ export interface BatchRunAction {
*/
pattern: string;
}
/** Runs multiple builds in parallel that match pattern. Response: [BatchExecutionResult]. */
/** Runs multiple builds in parallel that match pattern. Response: [BatchExecutionResponse]. */
export interface BatchRunBuild {
/**
* Id or name or wildcard pattern or regex.
@@ -3558,7 +3634,7 @@ export interface BatchRunBuild {
*/
pattern: string;
}
/** Runs multiple Procedures in parallel that match pattern. Response: [BatchExecutionResult]. */
/** Runs multiple Procedures in parallel that match pattern. Response: [BatchExecutionResponse]. */
export interface BatchRunProcedure {
/**
* Id or name or wildcard pattern or regex.
@@ -3621,7 +3697,7 @@ export interface CloneArgs {
provider: string;
/** Use https (vs http). */
https: boolean;
/** Full repo identifier. <namespace>/<repo_name> */
/** Full repo identifier. {namespace}/{repo_name} */
repo?: string;
/** Git Branch. Default: `main` */
branch: string;
@@ -3859,6 +3935,13 @@ export interface CreateDeployment {
/** Optional partial config to initialize the deployment with. */
config?: _PartialDeploymentConfig;
}
/** Create a Deployment from an existing container. Response: [Deployment]. */
export interface CreateDeploymentFromContainer {
/** The name or id of the existing container. */
name: string;
/** The server id or name on which container exists. */
server: string;
}
/**
* **Admin only.** Create a docker registry account.
* Response: [DockerRegistryAccount].
@@ -4117,7 +4200,7 @@ export interface DeleteDockerRegistryAccount {
}
/**
* **Admin only.** Delete a git provider account.
* Response: [User].
* Response: [DeleteGitProviderAccountResponse].
*/
export interface DeleteGitProviderAccount {
/** The id of the git provider to delete */
@@ -4288,6 +4371,8 @@ export interface Deploy {
export interface DeployStack {
/** Id or name */
stack: string;
/** Optionally specify a specific service to "compose up" */
service?: string;
/**
* Override the default termination max time.
* Only used if the stack needs to be taken down first.
@@ -4342,6 +4427,8 @@ export interface DestroyDeployment {
export interface DestroyStack {
/** Id or name */
stack: string;
/** Optionally specify a specific service to destroy */
service?: string;
/** Pass `--remove-orphans` */
remove_orphans?: boolean;
/** Override the default termination max time. */
@@ -4638,7 +4725,7 @@ export interface GetDeploymentLog {
}
/**
* Get the deployment container's stats using `docker stats`.
* Response: [DockerContainerStats].
* Response: [GetDeploymentStatsResponse].
*
* Note. This call will hit the underlying server directly for most up to date stats.
*/
@@ -4845,13 +4932,14 @@ export interface GetReposSummaryResponse {
/** The number of repos with unknown state. */
unknown: number;
}
/** Inspect a docker container on the server. Response: [Container]. */
/** Find the attached resource for a container. Either Deployment or Stack. Response: [GetResourceMatchingContainerResponse]. */
export interface GetResourceMatchingContainer {
/** Id or name */
server: string;
/** The container name */
container: string;
}
/** Response for [GetResourceMatchingContainer]. Resource is either Deployment, Stack, or None. */
export interface GetResourceMatchingContainerResponse {
resource?: ResourceTarget;
}
@@ -4949,7 +5037,7 @@ export interface GetStackActionState {
/** Id or name */
stack: string;
}
/** Get a stack service's log. Response: [GetStackContainersResponse]. */
/** Get a stack service's log. Response: [GetStackServiceLogResponse]. */
export interface GetStackServiceLog {
/** Id or name */
stack: string;
@@ -5329,7 +5417,7 @@ export interface ListApiKeysForServiceUser {
/**
* Retrieve versions of the build that were built in the past and available for deployment,
* sorted by most recent first.
* Response: [GetBuildVersionsResponse].
* Response: [ListBuildVersionsResponse].
*/
export interface ListBuildVersions {
/** Id or name */
@@ -5446,7 +5534,7 @@ export interface ListDockerRegistriesFromConfig {
}
/**
* List docker registry accounts matching optional query.
* Response: [ListDockerRegistrysResponse].
* Response: [ListDockerRegistryAccountsResponse].
*/
export interface ListDockerRegistryAccounts {
/** Optionally filter by accounts with a specific domain. */
@@ -5520,7 +5608,7 @@ export interface ListFullStacks {
}
/**
* List git provider accounts matching optional query.
* Response: [ListGitProvidersResponse].
* Response: [ListGitProviderAccountsResponse].
*/
export interface ListGitProviderAccounts {
/** Optionally filter by accounts with a specific domain. */
@@ -5845,6 +5933,11 @@ export interface PruneVolumes {
/** Id or name */
server: string;
}
/** Pulls the image for the target deployment. Response: [Update] */
export interface PullDeployment {
/** Name or id */
deployment: string;
}
/**
* Pulls the target repo. Response: [Update].
*
@@ -5857,6 +5950,13 @@ export interface PullRepo {
/** Id or name */
repo: string;
}
/** Pulls images for the target stack. `docker compose pull`. Response: [Update] */
export interface PullStack {
/** Id or name */
stack: string;
/** Optionally specify a specific service to start */
service?: string;
}
/**
* Push a resource to the front of the users 10 most recently viewed resources.
* Response: [NoData].
@@ -6818,6 +6918,9 @@ export type ExecuteRequest = {
} | {
type: "BatchDeploy";
params: BatchDeploy;
} | {
type: "PullDeployment";
params: PullDeployment;
} | {
type: "StartDeployment";
params: StartDeployment;
@@ -6836,6 +6939,9 @@ export type ExecuteRequest = {
} | {
type: "DestroyDeployment";
params: DestroyDeployment;
} | {
type: "BatchDestroyDeployment";
params: BatchDestroyDeployment;
} | {
type: "DeployStack";
params: DeployStack;
@@ -6848,6 +6954,9 @@ export type ExecuteRequest = {
} | {
type: "BatchDeployStackIfChanged";
params: BatchDeployStackIfChanged;
} | {
type: "PullStack";
params: PullStack;
} | {
type: "StartStack";
params: StartStack;
@@ -7376,6 +7485,9 @@ export type WriteRequest = {
} | {
type: "CopyDeployment";
params: CopyDeployment;
} | {
type: "CreateDeploymentFromContainer";
params: CreateDeploymentFromContainer;
} | {
type: "DeleteDeployment";
params: DeleteDeployment;

View File

@@ -1,5 +1,5 @@
/*
Generated by typeshare 1.11.0
Generated by typeshare 1.12.0
*/
/** The levels of permission that a User or UserGroup can have on a resource. */
export var PermissionLevel;
@@ -145,6 +145,7 @@ export var Operation;
Operation["DeleteStack"] = "DeleteStack";
Operation["WriteStackContents"] = "WriteStackContents";
Operation["RefreshStackCache"] = "RefreshStackCache";
Operation["PullStack"] = "PullStack";
Operation["DeployStack"] = "DeployStack";
Operation["StartStack"] = "StartStack";
Operation["RestartStack"] = "RestartStack";
@@ -152,16 +153,20 @@ export var Operation;
Operation["UnpauseStack"] = "UnpauseStack";
Operation["StopStack"] = "StopStack";
Operation["DestroyStack"] = "DestroyStack";
Operation["DeployStackService"] = "DeployStackService";
Operation["PullStackService"] = "PullStackService";
Operation["StartStackService"] = "StartStackService";
Operation["RestartStackService"] = "RestartStackService";
Operation["PauseStackService"] = "PauseStackService";
Operation["UnpauseStackService"] = "UnpauseStackService";
Operation["StopStackService"] = "StopStackService";
Operation["DestroyStackService"] = "DestroyStackService";
Operation["CreateDeployment"] = "CreateDeployment";
Operation["UpdateDeployment"] = "UpdateDeployment";
Operation["RenameDeployment"] = "RenameDeployment";
Operation["DeleteDeployment"] = "DeleteDeployment";
Operation["Deploy"] = "Deploy";
Operation["PullDeployment"] = "PullDeployment";
Operation["StartDeployment"] = "StartDeployment";
Operation["RestartDeployment"] = "RestartDeployment";
Operation["PauseDeployment"] = "PauseDeployment";

View File

@@ -5,6 +5,7 @@ import { Link, Outlet, useNavigate } from "react-router-dom";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
@@ -235,6 +236,7 @@ export const NewLayout = ({
<DialogContent>
<DialogHeader>
<DialogTitle>New {entityType}</DialogTitle>
<DialogDescription>Enter a unique name for the new {entityType}.</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-4 py-8">{children}</div>
<DialogFooter>

View File

@@ -118,17 +118,17 @@ export const ActionConfig = ({ id }: { id: string }) => {
value={branch}
onChange={(e) => setBranch(e.target.value)}
className="w-[200px]"
disabled={branch === "__ALL__"}
disabled={branch === "__ANY__"}
/>
<div className="flex items-center gap-2">
<div className="text-muted-foreground text-sm">
All branches:
No branch check:
</div>
<Switch
checked={branch === "__ALL__"}
checked={branch === "__ANY__"}
onCheckedChange={(checked) => {
if (checked) {
setBranch("__ALL__");
setBranch("__ANY__");
} else {
setBranch("main");
}

View File

@@ -1,23 +1,26 @@
import { ConfigItem } from "@components/config/util";
import { Types } from "komodo_client";
import { Badge } from "@ui/badge";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
} from "@ui/select";
import { Select, SelectContent, SelectItem, SelectTrigger } from "@ui/select";
import { MinusCircle } from "lucide-react";
const ALERT_TYPES: Types.AlertData["type"][] = [
// Server
"ServerUnreachable",
"ServerCpu",
"ServerMem",
"ServerDisk",
// State change
"ContainerStateChange",
"StackStateChange",
// Updates
"DeploymentImageUpdateAvailable",
"StackImageUpdateAvailable",
// Misc
"AwsBuilderTerminationFailed",
"ResourceSyncPendingUpdates",
"BuildFailed",
"AwsBuilderTerminationFailed",
"RepoBuildFailed",
];
export const AlertTypeConfig = ({
@@ -33,7 +36,11 @@ export const AlertTypeConfig = ({
(alert_type) => !alert_types.includes(alert_type)
);
return (
<ConfigItem label="Alert Types" description="Only send alerts of certain types." boldLabel>
<ConfigItem
label="Alert Types"
description="Only send alerts of certain types."
boldLabel
>
<div className="flex items-center gap-4">
{at.length ? (
<Select

View File

@@ -32,8 +32,10 @@ export const ResourcesConfig = ({
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
const servers = useRead("ListServers", {}).data ?? [];
const stacks = useRead("ListStacks", {}).data ?? [];
const deployments = useRead("ListDeployments", {}).data ?? [];
const builds = useRead("ListBuilds", {}).data ?? [];
const repos = useRead("ListRepos", {}).data ?? [];
const syncs = useRead("ListResourceSyncs", {}).data ?? [];
const all_resources = [
...servers.map((server) => {
@@ -48,6 +50,16 @@ export const ResourcesConfig = ({
: false,
};
}),
...stacks.map((stack) => {
return {
type: "Stack",
id: stack.id,
name: stack.name.toLowerCase(),
enabled: resources.find((r) => r.type === "Stack" && r.id === stack.id)
? true
: false,
};
}),
...deployments.map((deployment) => ({
type: "Deployment",
id: deployment.id,
@@ -66,6 +78,14 @@ export const ResourcesConfig = ({
? true
: false,
})),
...repos.map((repo) => ({
type: "Repo",
id: repo.id,
name: repo.name.toLowerCase(),
enabled: resources.find((r) => r.type === "Repo" && r.id === repo.id)
? true
: false,
})),
...syncs.map((sync) => ({
type: "ResourceSync",
id: sync.id,

View File

@@ -185,7 +185,7 @@ export const ResourceLink = ({
e.stopPropagation();
onClick?.();
}}
className="flex items-center gap-2 text-sm"
className="flex items-center gap-2 text-sm hover:underline"
>
<Components.Icon id={id} />
<ResourceName type={type} id={id} />
@@ -313,7 +313,7 @@ export const NewResource = ({
entityType={readable_type ?? type}
onConfirm={onConfirm}
enabled={!!name}
onOpenChange={() => setName("")}
onOpenChange={() => setName(_name)}
>
<div className="grid md:grid-cols-2 items-center">
{readable_type ?? type} Name

View File

@@ -1,5 +1,13 @@
import { ActionWithDialog, ConfirmButton } from "@components/util";
import { Play, Trash, Pause, Rocket, RefreshCcw, Square } from "lucide-react";
import {
Play,
Trash,
Pause,
Rocket,
RefreshCcw,
Square,
Download,
} from "lucide-react";
import { useExecute, useRead } from "@lib/hooks";
import { useEffect, useState } from "react";
import { Types } from "komodo_client";
@@ -138,6 +146,30 @@ export const DestroyDeployment = ({ id }: DeploymentId) => {
);
};
export const PullDeployment = ({ id }: DeploymentId) => {
const deployment = useDeployment(id);
const { mutate: pull, isPending: pullPending } = useExecute("PullDeployment");
const action_state = useRead(
"GetDeploymentActionState",
{
deployment: id,
},
{ refetchInterval: 5000 }
).data;
if (!deployment) return null;
return (
<ActionWithDialog
name={deployment.name}
title="Pull Image"
icon={<Download className="h-4 w-4" />}
onClick={() => pull({ deployment: id })}
disabled={pullPending}
loading={pullPending || action_state?.pulling}
/>
);
};
export const RestartDeployment = ({ id }: DeploymentId) => {
const deployment = useDeployment(id);
const state = deployment?.info.state;

View File

@@ -199,7 +199,7 @@ export const ImageConfig = ({
},
})
}
className="w-full lg:w-[300px]"
className="w-full"
placeholder="image name"
disabled={disabled}
/>

View File

@@ -39,6 +39,7 @@ export const NetworkModeSelector = ({
return (
<ConfigItem
label="Network Mode"
boldLabel
description="Choose the --network attached to container"
>
{customMode ? (

View File

@@ -20,7 +20,11 @@ export const RestartModeSelector = ({
set: (input: Partial<Types.DeploymentConfig>) => void;
disabled: boolean;
}) => (
<ConfigItem label="Restart Mode" description="Configure the --restart behavior.">
<ConfigItem
label="Restart Mode"
boldLabel
description="Configure the --restart behavior."
>
<Select
value={selected || undefined}
onValueChange={(restart: Types.RestartMode) => set({ restart })}

View File

@@ -70,7 +70,7 @@ export const DeploymentConfig = ({
<ConfigItem
label={
server_id ? (
<div className="flex gap-3 text-lg">
<div className="flex gap-3 text-lg font-bold">
Server:
<ResourceLink type="Server" id={server_id} />
</div>
@@ -99,7 +99,6 @@ export const DeploymentConfig = ({
: "Image",
description:
"Either pass a docker image directly, or choose a Build to deploy",
boldLabel: false,
components: {
image: (value, set) => (
<ImageConfig image={value} set={set} disabled={disabled} />
@@ -183,7 +182,6 @@ export const DeploymentConfig = ({
},
{
label: "Environment",
boldLabel: false,
description: "Pass these variables to the container",
components: {
environment: (env, set) => (
@@ -205,7 +203,6 @@ export const DeploymentConfig = ({
{
label: "Volumes",
description: "Configure the volume bindings.",
boldLabel: false,
components: {
volumes: (volumes, set) => (
<MonacoEditor
@@ -230,6 +227,17 @@ export const DeploymentConfig = ({
),
},
},
{
label: "Auto Update",
components: {
poll_for_updates: !(update.auto_update ?? config.auto_update) && {
description: "Check for updates to the image on an interval.",
},
auto_update: {
description: "Trigger a redeploy if a newer image is found.",
},
},
},
],
advanced: [
{
@@ -239,6 +247,7 @@ export const DeploymentConfig = ({
command: (value, set) => (
<ConfigItem
label="Command"
boldLabel
description={
<div className="flex flex-row flex-wrap gap-2">
<div>Replace the CMD, or extend the ENTRYPOINT.</div>
@@ -267,7 +276,6 @@ export const DeploymentConfig = ({
{
label: "Labels",
description: "Attach --labels to the container.",
boldLabel: false,
components: {
labels: (labels, set) => (
<MonacoEditor
@@ -286,6 +294,7 @@ export const DeploymentConfig = ({
extra_args: (value, set) => (
<ConfigItem
label="Extra Args"
boldLabel
description={
<div className="flex flex-row flex-wrap gap-2">
<div>Pass extra arguments to 'docker run'.</div>
@@ -326,7 +335,6 @@ export const DeploymentConfig = ({
},
{
label: "Termination",
boldLabel: false,
description:
"Configure the signals used to 'docker stop' the container. Options are SIGTERM, SIGQUIT, SIGINT, and SIGHUP.",
components: {

View File

@@ -10,6 +10,7 @@ import {
DestroyDeployment,
RestartDeployment,
PauseUnpauseDeployment,
PullDeployment,
} from "./actions";
import { DeploymentLogs } from "./log";
import {
@@ -22,8 +23,14 @@ import { RunBuild } from "../build/actions";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ui/tabs";
import { DeploymentConfig } from "./config";
import { DashboardPieChart } from "@pages/home/dashboard";
import { ResourcePageHeader, StatusBadge } from "@components/util";
import {
DockerResourceLink,
ResourcePageHeader,
StatusBadge,
} from "@components/util";
import { RenameResource } from "@components/config/util";
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@ui/hover-card";
import { Card } from "@ui/card";
// const configOrLog = atomWithStorage("config-or-log-v1", "Config");
@@ -178,17 +185,56 @@ export const DeploymentComponents: RequiredResourceComponents = {
);
},
Status: {},
Status: {
UpdateAvailable: ({ id }) => {
const info = useDeployment(id)?.info;
const state = info?.state ?? Types.DeploymentState.Unknown;
if (
!info ||
!info?.update_available ||
[
Types.DeploymentState.NotDeployed,
Types.DeploymentState.Unknown,
].includes(state)
) {
return null;
}
return (
<HoverCard openDelay={200}>
<HoverCardTrigger asChild>
<Card className="px-3 py-2 border-blue-400 hover:border-blue-500 transition-colors cursor-pointer">
<div className="text-sm text-nowrap overflow-hidden overflow-ellipsis">
Update Available
</div>
</Card>
</HoverCardTrigger>
<HoverCardContent align="start" className="w-fit text-sm">
There is a newer image available
</HoverCardContent>
</HoverCard>
);
},
},
Info: {
Image: ({ id }) => {
const config = useFullDeployment(id)?.config;
const info = useDeployment(id)?.info;
return info?.build_id ? (
<ResourceLink type="Build" id={info.build_id} />
) : (
<div className="flex gap-2 items-center text-sm">
<HardDrive className="w-4 h-4" />
<div>{info?.image || "N/A"}</div>
<div>
{info?.image.startsWith("sha256:")
? (
config?.image as Extract<
Types.DeploymentImage,
{ type: "Image" }
>
)?.params.image
: info?.image || "N/A"}
</div>
</div>
);
},
@@ -204,6 +250,24 @@ export const DeploymentComponents: RequiredResourceComponents = {
</div>
);
},
Container: ({ id }) => {
const deployment = useDeployment(id);
if (
!deployment ||
[
Types.DeploymentState.Unknown,
Types.DeploymentState.NotDeployed,
].includes(deployment.info.state)
)
return null;
return (
<DockerResourceLink
type="container"
name={deployment.name}
server_id={deployment.info.server_id}
/>
);
},
},
Actions: {
@@ -213,6 +277,7 @@ export const DeploymentComponents: RequiredResourceComponents = {
return <RunBuild id={build_id} />;
},
DeployDeployment,
PullDeployment,
RestartDeployment,
PauseUnpauseDeployment,
StartStopDeployment,

View File

@@ -238,17 +238,17 @@ const ProcedureConfigInner = ({
value={branch}
onChange={(e) => setBranch(e.target.value)}
className="w-[200px]"
disabled={branch === "__ALL__"}
disabled={branch === "__ANY__"}
/>
<div className="flex items-center gap-2">
<div className="text-muted-foreground text-sm">
All branches:
No branch check:
</div>
<Switch
checked={branch === "__ALL__"}
checked={branch === "__ANY__"}
onCheckedChange={(checked) => {
if (checked) {
setBranch("__ALL__");
setBranch("__ANY__");
} else {
setBranch("main");
}
@@ -776,6 +776,17 @@ const TARGET_COMPONENTS: ExecutionConfigs = {
/>
),
},
PullDeployment: {
params: { deployment: "" },
Component: ({ params, setParams, disabled }) => (
<ResourceSelector
type="Deployment"
selected={params.deployment}
onSelect={(deployment) => setParams({ deployment })}
disabled={disabled}
/>
),
},
StartDeployment: {
params: { deployment: "" },
Component: ({ params, setParams, disabled }) => (
@@ -913,6 +924,17 @@ const TARGET_COMPONENTS: ExecutionConfigs = {
/>
),
},
PullStack: {
params: { stack: "" },
Component: ({ params, setParams, disabled }) => (
<ResourceSelector
type="Stack"
selected={params.stack}
onSelect={(id) => setParams({ stack: id })}
disabled={disabled}
/>
),
},
StartStack: {
params: { stack: "" },
Component: ({ params, setParams, disabled }) => (

View File

@@ -1,10 +1,24 @@
import { ActionWithDialog, ConfirmButton } from "@components/util";
import { useExecute, useRead } from "@lib/hooks";
import { Pause, Play, RefreshCcw, Rocket, Square, Trash } from "lucide-react";
import {
Download,
Pause,
Play,
RefreshCcw,
Rocket,
Square,
Trash,
} from "lucide-react";
import { useStack } from ".";
import { Types } from "komodo_client";
export const DeployStack = ({ id }: { id: string }) => {
export const DeployStack = ({
id,
service,
}: {
id: string;
service?: string;
}) => {
const stack = useStack(id);
const state = stack?.info.state;
const { mutate: deploy, isPending } = useExecute("DeployStack");
@@ -13,27 +27,34 @@ export const DeployStack = ({ id }: { id: string }) => {
{ stack: id },
{ refetchInterval: 5000 }
).data?.deploying;
const services = useRead("ListStackServices", { stack: id }).data;
const container_state =
(service
? services?.find((s) => s.service === service)?.container?.state
: undefined) ?? Types.ContainerStateStatusEnum.Empty;
if (!stack || state === Types.StackState.Unknown) {
return null;
}
const deployed =
state !== undefined &&
[
Types.StackState.Running,
Types.StackState.Paused,
Types.StackState.Stopped,
Types.StackState.Restarting,
Types.StackState.Unhealthy,
].includes(state);
(service !== undefined
? container_state !== Types.ContainerStateStatusEnum.Empty
: [
Types.StackState.Running,
Types.StackState.Paused,
Types.StackState.Stopped,
Types.StackState.Restarting,
Types.StackState.Unhealthy,
].includes(state));
if (deployed) {
return (
<ActionWithDialog
name={stack.name}
name={`${stack?.name}${service ? ` - ${service}` : ""}`}
title="Redeploy"
icon={<Rocket className="h-4 w-4" />}
onClick={() => deploy({ stack: id })}
onClick={() => deploy({ stack: id, service })}
disabled={isPending}
loading={isPending || deploying}
/>
@@ -44,14 +65,20 @@ export const DeployStack = ({ id }: { id: string }) => {
<ConfirmButton
title="Deploy"
icon={<Rocket className="w-4 h-4" />}
onClick={() => deploy({ stack: id })}
onClick={() => deploy({ stack: id, service })}
disabled={isPending}
loading={isPending || deploying}
/>
);
};
export const DestroyStack = ({ id }: { id: string }) => {
export const DestroyStack = ({
id,
service,
}: {
id: string;
service?: string;
}) => {
const stack = useStack(id);
const state = stack?.info.state;
const { mutate: destroy, isPending } = useExecute("DestroyStack");
@@ -60,31 +87,64 @@ export const DestroyStack = ({ id }: { id: string }) => {
{ stack: id },
{ refetchInterval: 5000 }
).data?.destroying;
const services = useRead("ListStackServices", { stack: id }).data;
const container_state =
(service
? services?.find((s) => s.service === service)?.container?.state
: undefined) ?? Types.ContainerStateStatusEnum.Empty;
if (
!stack ||
state === undefined ||
[Types.StackState.Unknown, Types.StackState.Down].includes(state)
!stack || service !== undefined
? container_state === Types.ContainerStateStatusEnum.Empty
: state === undefined ||
[Types.StackState.Unknown, Types.StackState.Down].includes(state!)
) {
return null;
}
if (!stack) {
return null;
}
return (
<ActionWithDialog
name={stack.name}
name={`${stack?.name}${service ? ` - ${service}` : ""}`}
title="Destroy"
icon={<Trash className="h-4 w-4" />}
onClick={() => destroy({ stack: id })}
onClick={() => destroy({ stack: id, service })}
disabled={isPending}
loading={isPending || destroying}
/>
);
};
export const PullStack = ({
id,
service,
}: {
id: string;
service?: string;
}) => {
const stack = useStack(id);
const { mutate: pull, isPending: pullPending } = useExecute("PullStack");
const action_state = useRead(
"GetStackActionState",
{ stack: id },
{ refetchInterval: 5000 }
).data;
if (!stack || (stack?.info.missing_files.length ?? 0) > 0) {
return null;
}
return (
<ActionWithDialog
name={`${stack?.name}${service ? ` - ${service}` : ""}`}
title={`Pull Image${service ? "" : "s"}`}
icon={<Download className="h-4 w-4" />}
onClick={() => pull({ stack: id, service })}
disabled={pullPending}
loading={pullPending || action_state?.pulling}
/>
);
};
export const RestartStack = ({
id,
service,
@@ -103,9 +163,9 @@ export const RestartStack = ({
).data;
const services = useRead("ListStackServices", { stack: id }).data;
const container_state =
(service &&
services?.find((s) => s.service === service)?.container?.state) ??
Types.ContainerStateStatusEnum.Empty;
(service
? services?.find((s) => s.service === service)?.container?.state
: undefined) ?? Types.ContainerStateStatusEnum.Empty;
if (
!stack ||
@@ -147,9 +207,9 @@ export const StartStopStack = ({
).data;
const services = useRead("ListStackServices", { stack: id }).data;
const container_state =
(service &&
services?.find((s) => s.service === service)?.container?.state) ??
Types.DeploymentState.Unknown;
(service
? services?.find((s) => s.service === service)?.container?.state
: undefined) ?? Types.ContainerStateStatusEnum.Empty;
if (
!stack ||
@@ -213,9 +273,9 @@ export const PauseUnpauseStack = ({
).data;
const services = useRead("ListStackServices", { stack: id }).data;
const container_state =
(service &&
services?.find((s) => s.service === service)?.container?.state) ??
Types.DeploymentState.Unknown;
(service
? services?.find((s) => s.service === service)?.container?.state
: undefined) ?? Types.ContainerStateStatusEnum.Empty;
if (!stack || stack?.info.project_missing) {
return null;

View File

@@ -236,6 +236,17 @@ export const StackConfig = ({
),
},
},
{
label: "Auto Update",
components: {
poll_for_updates: !(update.auto_update ?? config.auto_update) && {
description: "Check for updates to the image on an interval.",
},
auto_update: {
description: "Trigger a redeploy if a newer image is found.",
},
},
},
{
label: "Links",
labelHidden: true,

View File

@@ -24,6 +24,7 @@ import {
DeployStack,
DestroyStack,
PauseUnpauseStack,
PullStack,
RestartStack,
StartStopStack,
} from "./actions";
@@ -256,6 +257,44 @@ export const StackComponents: RequiredResourceComponents = {
</HoverCard>
);
},
UpdateAvailable: ({ id }) => {
const info = useStack(id)?.info;
const state = info?.state ?? Types.StackState.Unknown;
if (
!info ||
!!info?.services.every((service) => !service.update_available) ||
[Types.StackState.Down, Types.StackState.Unknown].includes(state)
) {
return null;
}
return (
<HoverCard openDelay={200}>
<HoverCardTrigger asChild>
<Card className="px-3 py-2 border-blue-400 hover:border-blue-500 transition-colors cursor-pointer">
<div className="text-sm text-nowrap overflow-hidden overflow-ellipsis">
Update
{(info?.services.filter((s) => s.update_available).length ??
0) > 0
? "s"
: ""}{" "}
Available
</div>
</Card>
</HoverCardTrigger>
<HoverCardContent align="start" className="flex flex-col gap-2 w-fit">
{info?.services
.filter((service) => service.update_available)
.map((s) => (
<div className="text-sm flex gap-2">
<div className="text-muted-foreground">{s.service}</div>
<div className="text-muted-foreground"> - </div>
<div>{s.image}</div>
</div>
))}
</HoverCardContent>
</HoverCard>
);
},
Hash: ({ id }) => {
const info = useStack(id)?.info;
const fullInfo = useFullStack(id)?.info;
@@ -322,40 +361,6 @@ export const StackComponents: RequiredResourceComponents = {
</HoverCard>
);
},
// Latest: ({ id }) => {
// const info = useStack(id)?.info;
// const fullInfo = useFullStack(id)?.info;
// if (
// info?.project_missing ||
// !info?.latest_hash ||
// !fullInfo?.latest_message ||
// info?.latest_hash === info?.deployed_hash
// ) {
// return null;
// }
// return (
// <HoverCard openDelay={200}>
// <HoverCardTrigger asChild>
// <Card className="px-3 py-2 hover:bg-accent/50 transition-colors cursor-pointer">
// <div className="text-muted-foreground text-sm text-nowrap overflow-hidden overflow-ellipsis">
// latest: {info.latest_hash}
// </div>
// </Card>
// </HoverCardTrigger>
// <HoverCardContent align="start">
// <div className="grid gap-2">
// <Badge
// variant="secondary"
// className="w-fit text-muted-foreground"
// >
// commit message
// </Badge>
// {fullInfo.latest_message}
// </div>
// </HoverCardContent>
// </HoverCard>
// );
// },
Refresh: ({ id }) => {
const { toast } = useToast();
const inv = useInvalidate();
@@ -430,6 +435,7 @@ export const StackComponents: RequiredResourceComponents = {
Actions: {
DeployStack,
PullStack,
RestartStack,
PauseUnpauseStack,
StartStopStack,

View File

@@ -669,21 +669,19 @@ export const DockerResourceLink = ({
return (
<Link
to={`/servers/${server_id}/${type}/${encodeURIComponent(name)}`}
className="px-0"
className={cn(
"flex items-center gap-2 text-sm hover:underline py-1",
muted && "text-muted-foreground"
)}
>
<Button
variant="link"
className={cn("px-0 gap-2", muted && "text-muted-foreground")}
<Icon server_id={server_id} name={type === "image" ? id : name} />
<div
title={name}
className="max-w-[200px] lg:max-w-[250px] overflow-hidden overflow-ellipsis"
>
<Icon server_id={server_id} name={type === "image" ? id : name} />
<div
title={name}
className="max-w-[200px] lg:max-w-[250px] overflow-hidden overflow-ellipsis"
>
{name}
</div>
{extra && <div className="no-underline">{extra}</div>}
</Button>
{name}
</div>
{extra && <div className="no-underline">{extra}</div>}
</Link>
);
};

View File

@@ -1,14 +1,15 @@
import { Section } from "@components/layouts";
import { NewResource, ResourceLink } from "@components/resources/common";
import { ResourceLink } from "@components/resources/common";
import { useServer } from "@components/resources/server";
import {
ConfirmButton,
DOCKER_LINK_ICONS,
DockerLabelsSection,
DockerResourceLink,
ResourcePageHeader,
ShowHideButton,
} from "@components/util";
import { useRead, useSetTitle } from "@lib/hooks";
import { useRead, useSetTitle, useWrite } from "@lib/hooks";
import { Button } from "@ui/button";
import { DataTable } from "@ui/data-table";
import {
@@ -16,9 +17,10 @@ import {
Clapperboard,
Info,
Loader2,
PlusCircle,
SearchCode,
} from "lucide-react";
import { Link, useParams } from "react-router-dom";
import { Link, useNavigate, useParams } from "react-router-dom";
import { ContainerLogs } from "./log";
import { Actions } from "./actions";
import { Types } from "komodo_client";
@@ -101,7 +103,7 @@ const ContainerPageInner = ({
Back
</Button>
</Link>
<NewDeployment id={id} container={container_name} />
<NewDeployment id={id} name={container_name} />
</div>
<div className="flex flex-col xl:flex-row gap-4">
{/** HEADER */}
@@ -348,16 +350,10 @@ const AttachedResource = ({
);
};
const NewDeployment = ({
id,
container,
}: {
id: string;
container: string;
}) => {
const NewDeployment = ({ id, name }: { id: string; name: string }) => {
const { data: attached, isPending } = useRead(
"GetResourceMatchingContainer",
{ server: id, container }
{ server: id, container: name }
);
if (isPending) {
@@ -369,6 +365,28 @@ const NewDeployment = ({
}
if (!attached?.resource) {
return <NewResource type="Deployment" server_id={id} name={container} />;
return <NewDeploymentInner name={name} server_id={id} />;
}
};
const NewDeploymentInner = ({
server_id,
name,
}: {
name: string;
server_id: string;
}) => {
const nav = useNavigate();
const { mutateAsync, isPending } = useWrite("CreateDeploymentFromContainer");
return (
<ConfirmButton
title="New Deployment"
icon={<PlusCircle className="w-4 h-4" />}
onClick={async () => {
const id = (await mutateAsync({ name, server: server_id }))._id?.$oid!;
nav(`/deployments/${id}`);
}}
loading={isPending}
/>
);
};

View File

@@ -5,7 +5,10 @@ import {
} from "@components/resources/common";
import { useStack } from "@components/resources/stack";
import {
DeployStack,
DestroyStack,
PauseUnpauseStack,
PullStack,
RestartStack,
StartStopStack,
} from "@components/resources/stack/actions";
@@ -29,9 +32,12 @@ import { Fragment } from "react/jsx-runtime";
type IdServiceComponent = React.FC<{ id: string; service?: string }>;
const Actions: { [action: string]: IdServiceComponent } = {
DeployStack,
PullStack,
RestartStack,
PauseUnpauseStack,
StartStopStack,
DestroyStack,
};
export const StackServicePage = () => {

12
lib/cache/Cargo.toml vendored Normal file
View File

@@ -0,0 +1,12 @@
[package]
name = "cache"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
[dependencies]
anyhow.workspace = true
tokio.workspace = true

67
lib/cache/src/lib.rs vendored Normal file
View File

@@ -0,0 +1,67 @@
use std::{collections::HashMap, hash::Hash, sync::Arc};
use tokio::sync::Mutex;
/// Prevents simultaneous / rapid fire access to an action,
/// returning the cached result instead in these situations.
#[derive(Default)]
pub struct TimeoutCache<K, Res>(
Mutex<HashMap<K, Arc<Mutex<CacheEntry<Res>>>>>,
);
impl<K: Eq + Hash, Res: Default> TimeoutCache<K, Res> {
pub async fn get_lock(
&self,
key: K,
) -> Arc<Mutex<CacheEntry<Res>>> {
let mut lock = self.0.lock().await;
lock.entry(key).or_default().clone()
}
}
pub struct CacheEntry<Res> {
/// The last cached ts
pub last_ts: i64,
/// The last cached result
pub res: anyhow::Result<Res>,
}
impl<Res: Default> Default for CacheEntry<Res> {
fn default() -> Self {
CacheEntry {
last_ts: 0,
res: Ok(Res::default()),
}
}
}
impl<Res: Clone> CacheEntry<Res> {
pub fn set(&mut self, res: &anyhow::Result<Res>, timestamp: i64) {
self.res = res
.as_ref()
.map(|res| res.clone())
.map_err(clone_anyhow_error);
self.last_ts = timestamp;
}
pub fn clone_res(&self) -> anyhow::Result<Res> {
self
.res
.as_ref()
.map(|res| res.clone())
.map_err(clone_anyhow_error)
}
}
fn clone_anyhow_error(e: &anyhow::Error) -> anyhow::Error {
let mut reasons =
e.chain().map(|e| e.to_string()).collect::<Vec<_>>();
// Always guaranteed to be at least one reason
// Need to start the chain with the last reason
let mut e = anyhow::Error::msg(reasons.pop().unwrap());
// Need to reverse reason application from lowest context to highest context.
for reason in reasons.into_iter().rev() {
e = e.context(reason)
}
e
}

View File

@@ -11,6 +11,7 @@ homepage.workspace = true
komodo_client.workspace = true
formatting.workspace = true
command.workspace = true
cache.workspace = true
#
run_command.workspace = true
svi.workspace = true

View File

@@ -1,22 +1,28 @@
use std::{
collections::HashMap,
path::{Path, PathBuf},
sync::{Arc, OnceLock},
sync::OnceLock,
};
use anyhow::Context;
use cache::TimeoutCache;
use command::run_komodo_command;
use formatting::format_serror;
use komodo_client::entities::{
komodo_timestamp, update::Log, CloneArgs, EnvironmentVar,
};
use tokio::sync::Mutex;
use crate::{get_commit_hash_log, GitRes};
/// Wait this long after a pull to allow another pull through
const PULL_TIMEOUT: i64 = 5_000;
fn pull_cache() -> &'static TimeoutCache<PathBuf, GitRes> {
static PULL_CACHE: OnceLock<TimeoutCache<PathBuf, GitRes>> =
OnceLock::new();
PULL_CACHE.get_or_init(Default::default)
}
/// This will pull in a way that handles edge cases
/// from possible state of the repo. For example, the user
/// can change branch after clone, or even the remote.
@@ -56,8 +62,8 @@ where
let mut locked = lock.lock().await;
// Early return from cache if lasted pulled with PULL_TIMEOUT
if locked.last_pulled + PULL_TIMEOUT > komodo_timestamp() {
return clone_entry_res(&locked.res);
if locked.last_ts + PULL_TIMEOUT > komodo_timestamp() {
return locked.clone_res();
}
let res = async {
@@ -247,65 +253,9 @@ where
}
.await;
// Set the cache with results
locked.last_pulled = komodo_timestamp();
locked.res = clone_entry_res(&res);
// Set the cache with results. Any other calls waiting on the lock will
// then immediately also use this same result.
locked.set(&res, komodo_timestamp());
res
}
fn pull_cache() -> &'static PullCache {
static LAST_PULLED_MAP: OnceLock<PullCache> = OnceLock::new();
LAST_PULLED_MAP.get_or_init(|| Default::default())
}
struct PullCacheEntry {
last_pulled: i64,
res: anyhow::Result<GitRes>,
}
fn clone_entry_res(
res: &anyhow::Result<GitRes>,
) -> anyhow::Result<GitRes> {
match res {
Ok(res) => Ok(res.clone()),
Err(e) => Err(clone_anyhow_error(e)),
}
}
impl Default for PullCacheEntry {
fn default() -> Self {
PullCacheEntry {
last_pulled: 0,
res: Ok(GitRes::default()),
}
}
}
/// Prevents simulataneous pulls on the same repo,
/// as well as prevents redudant pulls within [PULL_TIMEOUT] milliseconds.
#[derive(Default)]
struct PullCache(Mutex<HashMap<PathBuf, Arc<Mutex<PullCacheEntry>>>>);
impl PullCache {
async fn get_lock(
&self,
path: PathBuf,
) -> Arc<Mutex<PullCacheEntry>> {
let mut lock = self.0.lock().await;
lock.entry(path).or_default().clone()
}
}
fn clone_anyhow_error(e: &anyhow::Error) -> anyhow::Error {
let mut reasons =
e.chain().map(|e| e.to_string()).collect::<Vec<_>>();
// Always guaranteed to be at least one reason
// Need to start the chain with the last reason
let mut e = anyhow::Error::msg(reasons.pop().unwrap());
// Need to reverse reason application from lowest context to highest context.
for reason in reasons.into_iter().rev() {
e = e.context(reason)
}
e
}

View File

@@ -5,10 +5,10 @@ use komodo_client::entities::{CloneArgs, EnvironmentVar};
use crate::GitRes;
/// This is a mix of clone / pull.
/// - If the folder doesn't exist, it will clone the repo.
/// - If it does, it will ensure the remote is correct,
/// ensure the correct branch is (force) checked out,
/// force pull the repo, and switch to specified hash if provided.
/// - If the folder doesn't exist, it will clone the repo.
/// - If it does, it will ensure the remote is correct,
/// ensure the correct branch is (force) checked out,
/// force pull the repo, and switch to specified hash if provided.
#[tracing::instrument(
level = "debug",
skip(

View File

@@ -39,5 +39,5 @@ resource_poll_interval = "1-min"
###########
logging.level = "info"
logging.stdio = "standard"
logging.otlp_endpoint = "http://tempo.grafana.orb.local:4317"
# logging.otlp_endpoint = "http://grafana.orb.local:4317"
logging.opentelemetry_service_name = "Komodo"

View File

@@ -20,5 +20,5 @@ ssl_cert_file = ".komodo/ssl/cert.pem"
###########
logging.level = "info"
logging.stdio = "standard"
logging.otlp_endpoint = "http://tempo.grafana.orb.local:4317"
# logging.otlp_endpoint = "http://grafana.orb.local:4317"
logging.opentelemetry_service_name = "Periphery"