mirror of
https://github.com/moghtech/komodo.git
synced 2025-12-05 19:17:36 -06:00
Compare commits
12 Commits
df016dfd8c
...
a3554565e9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3554565e9 | ||
|
|
bc835eaa2a | ||
|
|
73608e5020 | ||
|
|
fde1ce288a | ||
|
|
a87472a436 | ||
|
|
c4a1b74a28 | ||
|
|
1e0fc5d88b | ||
|
|
02ccd03800 | ||
|
|
f6b629ffdf | ||
|
|
dcb2459603 | ||
|
|
415a899bb6 | ||
|
|
1621043a21 |
44
Cargo.lock
generated
44
Cargo.lock
generated
@@ -908,7 +908,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cache"
|
||||
version = "2.0.0-dev-91"
|
||||
version = "2.0.0-dev-92"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"tokio",
|
||||
@@ -1100,7 +1100,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "command"
|
||||
version = "2.0.0-dev-91"
|
||||
version = "2.0.0-dev-92"
|
||||
dependencies = [
|
||||
"komodo_client",
|
||||
"shlex",
|
||||
@@ -1127,7 +1127,7 @@ checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d"
|
||||
|
||||
[[package]]
|
||||
name = "config"
|
||||
version = "2.0.0-dev-91"
|
||||
version = "2.0.0-dev-92"
|
||||
dependencies = [
|
||||
"colored",
|
||||
"indexmap 2.12.1",
|
||||
@@ -1449,7 +1449,7 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
|
||||
|
||||
[[package]]
|
||||
name = "database"
|
||||
version = "2.0.0-dev-91"
|
||||
version = "2.0.0-dev-92"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-compression",
|
||||
@@ -1748,7 +1748,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "encoding"
|
||||
version = "2.0.0-dev-91"
|
||||
version = "2.0.0-dev-92"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -1790,7 +1790,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "environment"
|
||||
version = "2.0.0-dev-91"
|
||||
version = "2.0.0-dev-92"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"formatting",
|
||||
@@ -1800,7 +1800,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "environment_file"
|
||||
version = "2.0.0-dev-91"
|
||||
version = "2.0.0-dev-92"
|
||||
dependencies = [
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
@@ -1902,7 +1902,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "formatting"
|
||||
version = "2.0.0-dev-91"
|
||||
version = "2.0.0-dev-92"
|
||||
dependencies = [
|
||||
"serror",
|
||||
]
|
||||
@@ -2068,7 +2068,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "git"
|
||||
version = "2.0.0-dev-91"
|
||||
version = "2.0.0-dev-92"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cache",
|
||||
@@ -2702,7 +2702,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "interpolate"
|
||||
version = "2.0.0-dev-91"
|
||||
version = "2.0.0-dev-92"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"komodo_client",
|
||||
@@ -2824,7 +2824,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_cli"
|
||||
version = "2.0.0-dev-91"
|
||||
version = "2.0.0-dev-92"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -2852,7 +2852,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_client"
|
||||
version = "2.0.0-dev-91"
|
||||
version = "2.0.0-dev-92"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -2888,7 +2888,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_core"
|
||||
version = "2.0.0-dev-91"
|
||||
version = "2.0.0-dev-92"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -2964,7 +2964,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_periphery"
|
||||
version = "2.0.0-dev-91"
|
||||
version = "2.0.0-dev-92"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -3085,7 +3085,7 @@ checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
||||
|
||||
[[package]]
|
||||
name = "logger"
|
||||
version = "2.0.0-dev-91"
|
||||
version = "2.0.0-dev-92"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"komodo_client",
|
||||
@@ -3377,7 +3377,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "noise"
|
||||
version = "2.0.0-dev-91"
|
||||
version = "2.0.0-dev-92"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -3801,7 +3801,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "periphery_client"
|
||||
version = "2.0.0-dev-91"
|
||||
version = "2.0.0-dev-92"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"derive_variants",
|
||||
@@ -4146,7 +4146,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rate_limit"
|
||||
version = "2.0.0-dev-91"
|
||||
version = "2.0.0-dev-92"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -4293,7 +4293,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "response"
|
||||
version = "2.0.0-dev-91"
|
||||
version = "2.0.0-dev-92"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -4563,7 +4563,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "secret_file"
|
||||
version = "2.0.0-dev-91"
|
||||
version = "2.0.0-dev-92"
|
||||
dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
@@ -5596,7 +5596,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "transport"
|
||||
version = "2.0.0-dev-91"
|
||||
version = "2.0.0-dev-92"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -5819,7 +5819,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "validations"
|
||||
version = "2.0.0-dev-91"
|
||||
version = "2.0.0-dev-92"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bson",
|
||||
|
||||
@@ -8,7 +8,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "2.0.0-dev-91"
|
||||
version = "2.0.0-dev-92"
|
||||
edition = "2024"
|
||||
authors = ["mbecker20 <becker.maxh@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
@@ -221,6 +221,21 @@ pub async fn handle(
|
||||
Execution::SendAlert(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::RemoveSwarmNodes(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::RemoveSwarmStacks(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::RemoveSwarmServices(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::RemoveSwarmConfigs(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::RemoveSwarmSecrets(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::ClearRepoCache(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
@@ -488,6 +503,26 @@ pub async fn handle(
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::RemoveSwarmNodes(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::RemoveSwarmStacks(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::RemoveSwarmServices(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::RemoveSwarmConfigs(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::RemoveSwarmSecrets(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::ClearRepoCache(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
use std::{sync::OnceLock, time::Instant};
|
||||
use std::{
|
||||
net::{IpAddr, SocketAddr},
|
||||
sync::OnceLock,
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use axum::{Router, extract::Path, http::HeaderMap, routing::post};
|
||||
use axum::{
|
||||
Router,
|
||||
extract::{ConnectInfo, Path},
|
||||
http::HeaderMap,
|
||||
routing::post,
|
||||
};
|
||||
use derive_variants::{EnumVariants, ExtractVariant};
|
||||
use komodo_client::{api::auth::*, entities::user::User};
|
||||
use rate_limit::WithFailureRateLimit;
|
||||
@@ -27,9 +36,11 @@ use crate::{
|
||||
|
||||
use super::Variant;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AuthArgs {
|
||||
pub headers: HeaderMap,
|
||||
/// Prefer extracting IP from headers.
|
||||
/// This IP will be the IP of reverse proxy itself.
|
||||
pub ip: IpAddr,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
@@ -79,6 +90,7 @@ pub fn router() -> Router {
|
||||
|
||||
async fn variant_handler(
|
||||
headers: HeaderMap,
|
||||
info: ConnectInfo<SocketAddr>,
|
||||
Path(Variant { variant }): Path<Variant>,
|
||||
Json(params): Json<serde_json::Value>,
|
||||
) -> serror::Result<axum::response::Response> {
|
||||
@@ -86,11 +98,12 @@ async fn variant_handler(
|
||||
"type": variant,
|
||||
"params": params,
|
||||
}))?;
|
||||
handler(headers, Json(req)).await
|
||||
handler(headers, info, Json(req)).await
|
||||
}
|
||||
|
||||
async fn handler(
|
||||
headers: HeaderMap,
|
||||
ConnectInfo(info): ConnectInfo<SocketAddr>,
|
||||
Json(request): Json<AuthRequest>,
|
||||
) -> serror::Result<axum::response::Response> {
|
||||
let timer = Instant::now();
|
||||
@@ -99,7 +112,12 @@ async fn handler(
|
||||
"/auth request {req_id} | METHOD: {:?}",
|
||||
request.extract_variant()
|
||||
);
|
||||
let res = request.resolve(&AuthArgs { headers }).await;
|
||||
let res = request
|
||||
.resolve(&AuthArgs {
|
||||
headers,
|
||||
ip: info.ip(),
|
||||
})
|
||||
.await;
|
||||
if let Err(e) = &res {
|
||||
debug!("/auth request {req_id} | error: {:#}", e.error);
|
||||
}
|
||||
@@ -136,13 +154,14 @@ impl Resolve<AuthArgs> for GetLoginOptions {
|
||||
impl Resolve<AuthArgs> for ExchangeForJwt {
|
||||
async fn resolve(
|
||||
self,
|
||||
AuthArgs { headers }: &AuthArgs,
|
||||
AuthArgs { headers, ip }: &AuthArgs,
|
||||
) -> serror::Result<ExchangeForJwtResponse> {
|
||||
jwt_client()
|
||||
.redeem_exchange_token(&self.token)
|
||||
.with_failure_rate_limit_using_headers(
|
||||
auth_rate_limiter(),
|
||||
headers,
|
||||
Some(*ip),
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -151,7 +170,7 @@ impl Resolve<AuthArgs> for ExchangeForJwt {
|
||||
impl Resolve<AuthArgs> for GetUser {
|
||||
async fn resolve(
|
||||
self,
|
||||
AuthArgs { headers }: &AuthArgs,
|
||||
AuthArgs { headers, ip }: &AuthArgs,
|
||||
) -> serror::Result<User> {
|
||||
async {
|
||||
let user_id = get_user_id_from_headers(headers)
|
||||
@@ -164,6 +183,7 @@ impl Resolve<AuthArgs> for GetUser {
|
||||
.with_failure_rate_limit_using_headers(
|
||||
auth_rate_limiter(),
|
||||
headers,
|
||||
Some(*ip),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use interpolate::Interpolator;
|
||||
use komodo_client::{
|
||||
api::execute::*,
|
||||
entities::{
|
||||
Version,
|
||||
SwarmOrServer, Version,
|
||||
build::{Build, ImageRegistryConfig},
|
||||
deployment::{
|
||||
Deployment, DeploymentImage, extract_registry_domain,
|
||||
@@ -20,16 +20,22 @@ use komodo_client::{
|
||||
},
|
||||
};
|
||||
use periphery_client::api;
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCodeError;
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
periphery_client,
|
||||
query::{VariablesAndSecrets, get_variables_and_secrets},
|
||||
query::{
|
||||
VariablesAndSecrets, get_swarm_or_server,
|
||||
get_variables_and_secrets,
|
||||
},
|
||||
registry_token,
|
||||
swarm::swarm_request,
|
||||
update::update_update,
|
||||
},
|
||||
monitor::update_cache_for_server,
|
||||
monitor::{update_cache_for_server, update_cache_for_swarm},
|
||||
permission::get_check_permissions,
|
||||
resource,
|
||||
state::action_states,
|
||||
@@ -73,7 +79,7 @@ impl Resolve<ExecuteArgs> for BatchDeploy {
|
||||
async fn setup_deployment_execution(
|
||||
deployment: &str,
|
||||
user: &User,
|
||||
) -> anyhow::Result<(Deployment, Server)> {
|
||||
) -> anyhow::Result<(Deployment, SwarmOrServer)> {
|
||||
let deployment = get_check_permissions::<Deployment>(
|
||||
deployment,
|
||||
user,
|
||||
@@ -81,18 +87,13 @@ async fn setup_deployment_execution(
|
||||
)
|
||||
.await?;
|
||||
|
||||
if deployment.config.server_id.is_empty() {
|
||||
return Err(anyhow!("Deployment has no Server configured"));
|
||||
}
|
||||
let swarm_or_server = get_swarm_or_server(
|
||||
&deployment.config.swarm_id,
|
||||
&deployment.config.server_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
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))
|
||||
Ok((deployment, swarm_or_server))
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for Deploy {
|
||||
@@ -112,7 +113,7 @@ impl Resolve<ExecuteArgs> for Deploy {
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let (mut deployment, server) =
|
||||
let (mut deployment, swarm_or_server) =
|
||||
setup_deployment_execution(&self.deployment, user).await?;
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
@@ -223,27 +224,51 @@ impl Resolve<ExecuteArgs> for Deploy {
|
||||
update.version = version;
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
match periphery_client(&server)
|
||||
.await?
|
||||
.request(api::container::Deploy {
|
||||
deployment,
|
||||
stop_signal: self.stop_signal,
|
||||
stop_time: self.stop_time,
|
||||
registry_token,
|
||||
replacers: secret_replacers.into_iter().collect(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(log) => update.logs.push(log),
|
||||
Err(e) => {
|
||||
update.push_error_log(
|
||||
"Deploy Container",
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
match swarm_or_server {
|
||||
SwarmOrServer::Swarm(swarm) => {
|
||||
match swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
api::swarm::CreateSwarmService {
|
||||
deployment,
|
||||
registry_token,
|
||||
replacers: secret_replacers.into_iter().collect(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(log) => update.logs.push(log),
|
||||
Err(e) => {
|
||||
update.push_error_log(
|
||||
"Create Service",
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
}
|
||||
};
|
||||
update_cache_for_swarm(&swarm, true).await;
|
||||
}
|
||||
};
|
||||
|
||||
update_cache_for_server(&server, true).await;
|
||||
SwarmOrServer::Server(server) => {
|
||||
match periphery_client(&server)
|
||||
.await?
|
||||
.request(api::container::RunContainer {
|
||||
deployment,
|
||||
stop_signal: self.stop_signal,
|
||||
stop_time: self.stop_time,
|
||||
registry_token,
|
||||
replacers: secret_replacers.into_iter().collect(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(log) => update.logs.push(log),
|
||||
Err(e) => {
|
||||
update.push_error_log(
|
||||
"Deploy Container",
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
}
|
||||
};
|
||||
update_cache_for_server(&server, true).await;
|
||||
}
|
||||
}
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
@@ -400,9 +425,16 @@ impl Resolve<ExecuteArgs> for PullDeployment {
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let (deployment, server) =
|
||||
let (deployment, swarm_or_server) =
|
||||
setup_deployment_execution(&self.deployment, user).await?;
|
||||
|
||||
let SwarmOrServer::Server(server) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!("PullDeployment should not be called for Deployment in Swarm Mode")
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
};
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
let action_state = action_states()
|
||||
.deployment
|
||||
@@ -443,9 +475,16 @@ impl Resolve<ExecuteArgs> for StartDeployment {
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let (deployment, server) =
|
||||
let (deployment, swarm_or_server) =
|
||||
setup_deployment_execution(&self.deployment, user).await?;
|
||||
|
||||
let SwarmOrServer::Server(server) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!("StartDeployment should not be called for Deployment in Swarm Mode")
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
};
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
let action_state = action_states()
|
||||
.deployment
|
||||
@@ -500,9 +539,16 @@ impl Resolve<ExecuteArgs> for RestartDeployment {
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let (deployment, server) =
|
||||
let (deployment, swarm_or_server) =
|
||||
setup_deployment_execution(&self.deployment, user).await?;
|
||||
|
||||
let SwarmOrServer::Server(server) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!("RestartDeployment should not be called for Deployment in Swarm Mode")
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
};
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
let action_state = action_states()
|
||||
.deployment
|
||||
@@ -559,9 +605,16 @@ impl Resolve<ExecuteArgs> for PauseDeployment {
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let (deployment, server) =
|
||||
let (deployment, swarm_or_server) =
|
||||
setup_deployment_execution(&self.deployment, user).await?;
|
||||
|
||||
let SwarmOrServer::Server(server) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!("PauseDeployment should not be called for Deployment in Swarm Mode")
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
};
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
let action_state = action_states()
|
||||
.deployment
|
||||
@@ -616,9 +669,16 @@ impl Resolve<ExecuteArgs> for UnpauseDeployment {
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let (deployment, server) =
|
||||
let (deployment, swarm_or_server) =
|
||||
setup_deployment_execution(&self.deployment, user).await?;
|
||||
|
||||
let SwarmOrServer::Server(server) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!("UnpauseDeployment should not be called for Deployment in Swarm Mode")
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
};
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
let action_state = action_states()
|
||||
.deployment
|
||||
@@ -677,9 +737,16 @@ impl Resolve<ExecuteArgs> for StopDeployment {
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let (deployment, server) =
|
||||
let (deployment, swarm_or_server) =
|
||||
setup_deployment_execution(&self.deployment, user).await?;
|
||||
|
||||
let SwarmOrServer::Server(server) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!("StopDeployment should not be called for Deployment in Swarm Mode")
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
};
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
let action_state = action_states()
|
||||
.deployment
|
||||
@@ -779,9 +846,16 @@ impl Resolve<ExecuteArgs> for DestroyDeployment {
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let (deployment, server) =
|
||||
let (deployment, swarm_or_server) =
|
||||
setup_deployment_execution(&self.deployment, user).await?;
|
||||
|
||||
let SwarmOrServer::Server(server) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!("DestroyDeployment should not be called for Deployment in Swarm Mode")
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
};
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
let action_state = action_states()
|
||||
.deployment
|
||||
|
||||
@@ -43,6 +43,7 @@ mod procedure;
|
||||
mod repo;
|
||||
mod server;
|
||||
mod stack;
|
||||
mod swarm;
|
||||
mod sync;
|
||||
|
||||
use super::Variant;
|
||||
@@ -69,28 +70,7 @@ pub struct ExecuteArgs {
|
||||
#[error(serror::Error)]
|
||||
#[serde(tag = "type", content = "params")]
|
||||
pub enum ExecuteRequest {
|
||||
// ==== SERVER ====
|
||||
StartContainer(StartContainer),
|
||||
RestartContainer(RestartContainer),
|
||||
PauseContainer(PauseContainer),
|
||||
UnpauseContainer(UnpauseContainer),
|
||||
StopContainer(StopContainer),
|
||||
DestroyContainer(DestroyContainer),
|
||||
StartAllContainers(StartAllContainers),
|
||||
RestartAllContainers(RestartAllContainers),
|
||||
PauseAllContainers(PauseAllContainers),
|
||||
UnpauseAllContainers(UnpauseAllContainers),
|
||||
StopAllContainers(StopAllContainers),
|
||||
PruneContainers(PruneContainers),
|
||||
DeleteNetwork(DeleteNetwork),
|
||||
PruneNetworks(PruneNetworks),
|
||||
DeleteImage(DeleteImage),
|
||||
PruneImages(PruneImages),
|
||||
DeleteVolume(DeleteVolume),
|
||||
PruneVolumes(PruneVolumes),
|
||||
PruneDockerBuilders(PruneDockerBuilders),
|
||||
PruneBuildx(PruneBuildx),
|
||||
PruneSystem(PruneSystem),
|
||||
// ==== SWARM ====
|
||||
|
||||
// ==== STACK ====
|
||||
DeployStack(DeployStack),
|
||||
@@ -149,6 +129,36 @@ pub enum ExecuteRequest {
|
||||
TestAlerter(TestAlerter),
|
||||
SendAlert(SendAlert),
|
||||
|
||||
// ==== SERVER ====
|
||||
StartContainer(StartContainer),
|
||||
RestartContainer(RestartContainer),
|
||||
PauseContainer(PauseContainer),
|
||||
UnpauseContainer(UnpauseContainer),
|
||||
StopContainer(StopContainer),
|
||||
DestroyContainer(DestroyContainer),
|
||||
StartAllContainers(StartAllContainers),
|
||||
RestartAllContainers(RestartAllContainers),
|
||||
PauseAllContainers(PauseAllContainers),
|
||||
UnpauseAllContainers(UnpauseAllContainers),
|
||||
StopAllContainers(StopAllContainers),
|
||||
PruneContainers(PruneContainers),
|
||||
DeleteNetwork(DeleteNetwork),
|
||||
PruneNetworks(PruneNetworks),
|
||||
DeleteImage(DeleteImage),
|
||||
PruneImages(PruneImages),
|
||||
DeleteVolume(DeleteVolume),
|
||||
PruneVolumes(PruneVolumes),
|
||||
PruneDockerBuilders(PruneDockerBuilders),
|
||||
PruneBuildx(PruneBuildx),
|
||||
PruneSystem(PruneSystem),
|
||||
|
||||
// ==== SWARM ====
|
||||
RemoveSwarmNodes(RemoveSwarmNodes),
|
||||
RemoveSwarmStacks(RemoveSwarmStacks),
|
||||
RemoveSwarmServices(RemoveSwarmServices),
|
||||
RemoveSwarmConfigs(RemoveSwarmConfigs),
|
||||
RemoveSwarmSecrets(RemoveSwarmSecrets),
|
||||
|
||||
// ==== MAINTENANCE ====
|
||||
ClearRepoCache(ClearRepoCache),
|
||||
BackupCoreDatabase(BackupCoreDatabase),
|
||||
|
||||
@@ -70,8 +70,8 @@ impl Resolve<ExecuteArgs> for StartContainer {
|
||||
{
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"start container",
|
||||
format_serror(&e.context("failed to start container").into()),
|
||||
"Start Container",
|
||||
format_serror(&e.context("Failed to start container").into()),
|
||||
),
|
||||
};
|
||||
|
||||
@@ -134,9 +134,9 @@ impl Resolve<ExecuteArgs> for RestartContainer {
|
||||
{
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"restart container",
|
||||
"Restart Container",
|
||||
format_serror(
|
||||
&e.context("failed to restart container").into(),
|
||||
&e.context("Failed to restart container").into(),
|
||||
),
|
||||
),
|
||||
};
|
||||
@@ -200,8 +200,8 @@ impl Resolve<ExecuteArgs> for PauseContainer {
|
||||
{
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"pause container",
|
||||
format_serror(&e.context("failed to pause container").into()),
|
||||
"Pause Container",
|
||||
format_serror(&e.context("Failed to pause container").into()),
|
||||
),
|
||||
};
|
||||
|
||||
@@ -264,9 +264,9 @@ impl Resolve<ExecuteArgs> for UnpauseContainer {
|
||||
{
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"unpause container",
|
||||
"Unpause Container",
|
||||
format_serror(
|
||||
&e.context("failed to unpause container").into(),
|
||||
&e.context("Failed to unpause container").into(),
|
||||
),
|
||||
),
|
||||
};
|
||||
@@ -334,8 +334,8 @@ impl Resolve<ExecuteArgs> for StopContainer {
|
||||
{
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"stop container",
|
||||
format_serror(&e.context("failed to stop container").into()),
|
||||
"Stop Container",
|
||||
format_serror(&e.context("Failed to stop container").into()),
|
||||
),
|
||||
};
|
||||
|
||||
@@ -408,8 +408,10 @@ impl Resolve<ExecuteArgs> for DestroyContainer {
|
||||
{
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"stop container",
|
||||
format_serror(&e.context("failed to stop container").into()),
|
||||
"Remove Container",
|
||||
format_serror(
|
||||
&e.context("Failed to remove container").into(),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
@@ -464,13 +466,13 @@ impl Resolve<ExecuteArgs> for StartAllContainers {
|
||||
.await?
|
||||
.request(api::container::StartAllContainers {})
|
||||
.await
|
||||
.context("failed to start all containers on host")?;
|
||||
.context("Failed to start all containers on host")?;
|
||||
|
||||
update.logs.extend(logs);
|
||||
|
||||
if all_logs_success(&update.logs) {
|
||||
update.push_simple_log(
|
||||
"start all containers",
|
||||
"Start All Containers",
|
||||
String::from("All containers have been started on the host."),
|
||||
);
|
||||
}
|
||||
@@ -524,13 +526,13 @@ impl Resolve<ExecuteArgs> for RestartAllContainers {
|
||||
.await?
|
||||
.request(api::container::RestartAllContainers {})
|
||||
.await
|
||||
.context("failed to restart all containers on host")?;
|
||||
.context("Failed to restart all containers on host")?;
|
||||
|
||||
update.logs.extend(logs);
|
||||
|
||||
if all_logs_success(&update.logs) {
|
||||
update.push_simple_log(
|
||||
"restart all containers",
|
||||
"Restart All Containers",
|
||||
String::from(
|
||||
"All containers have been restarted on the host.",
|
||||
),
|
||||
@@ -586,13 +588,13 @@ impl Resolve<ExecuteArgs> for PauseAllContainers {
|
||||
.await?
|
||||
.request(api::container::PauseAllContainers {})
|
||||
.await
|
||||
.context("failed to pause all containers on host")?;
|
||||
.context("Failed to pause all containers on host")?;
|
||||
|
||||
update.logs.extend(logs);
|
||||
|
||||
if all_logs_success(&update.logs) {
|
||||
update.push_simple_log(
|
||||
"pause all containers",
|
||||
"Pause All Containers",
|
||||
String::from("All containers have been paused on the host."),
|
||||
);
|
||||
}
|
||||
@@ -646,13 +648,13 @@ impl Resolve<ExecuteArgs> for UnpauseAllContainers {
|
||||
.await?
|
||||
.request(api::container::UnpauseAllContainers {})
|
||||
.await
|
||||
.context("failed to unpause all containers on host")?;
|
||||
.context("Failed to unpause all containers on host")?;
|
||||
|
||||
update.logs.extend(logs);
|
||||
|
||||
if all_logs_success(&update.logs) {
|
||||
update.push_simple_log(
|
||||
"unpause all containers",
|
||||
"Unpause All Containers",
|
||||
String::from(
|
||||
"All containers have been unpaused on the host.",
|
||||
),
|
||||
@@ -708,13 +710,13 @@ impl Resolve<ExecuteArgs> for StopAllContainers {
|
||||
.await?
|
||||
.request(api::container::StopAllContainers {})
|
||||
.await
|
||||
.context("failed to stop all containers on host")?;
|
||||
.context("Failed to stop all containers on host")?;
|
||||
|
||||
update.logs.extend(logs);
|
||||
|
||||
if all_logs_success(&update.logs) {
|
||||
update.push_simple_log(
|
||||
"stop all containers",
|
||||
"Stop All Containers",
|
||||
String::from("All containers have been stopped on the host."),
|
||||
);
|
||||
}
|
||||
@@ -770,14 +772,14 @@ impl Resolve<ExecuteArgs> for PruneContainers {
|
||||
.request(api::container::PruneContainers {})
|
||||
.await
|
||||
.context(format!(
|
||||
"failed to prune containers on server {}",
|
||||
"Failed to prune containers on server {}",
|
||||
server.name
|
||||
)) {
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"prune containers",
|
||||
"Prune Containers",
|
||||
format_serror(
|
||||
&e.context("failed to prune containers").into(),
|
||||
&e.context("Failed to prune containers").into(),
|
||||
),
|
||||
),
|
||||
};
|
||||
@@ -827,15 +829,15 @@ impl Resolve<ExecuteArgs> for DeleteNetwork {
|
||||
})
|
||||
.await
|
||||
.context(format!(
|
||||
"failed to delete network {} on server {}",
|
||||
"Failed to delete network {} on server {}",
|
||||
self.name, server.name
|
||||
)) {
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"delete network",
|
||||
"Delete Network",
|
||||
format_serror(
|
||||
&e.context(format!(
|
||||
"failed to delete network {}",
|
||||
"Failed to delete network {}",
|
||||
self.name
|
||||
))
|
||||
.into(),
|
||||
@@ -896,13 +898,13 @@ impl Resolve<ExecuteArgs> for PruneNetworks {
|
||||
.request(api::docker::PruneNetworks {})
|
||||
.await
|
||||
.context(format!(
|
||||
"failed to prune networks on server {}",
|
||||
"Failed to prune networks on server {}",
|
||||
server.name
|
||||
)) {
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"prune networks",
|
||||
format_serror(&e.context("failed to prune networks").into()),
|
||||
"Prune Networks",
|
||||
format_serror(&e.context("Failed to prune networks").into()),
|
||||
),
|
||||
};
|
||||
|
||||
@@ -951,14 +953,14 @@ impl Resolve<ExecuteArgs> for DeleteImage {
|
||||
})
|
||||
.await
|
||||
.context(format!(
|
||||
"failed to delete image {} on server {}",
|
||||
"Failed to delete image {} on server {}",
|
||||
self.name, server.name
|
||||
)) {
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"delete image",
|
||||
format_serror(
|
||||
&e.context(format!("failed to delete image {}", self.name))
|
||||
&e.context(format!("Failed to delete image {}", self.name))
|
||||
.into(),
|
||||
),
|
||||
),
|
||||
@@ -1017,9 +1019,9 @@ impl Resolve<ExecuteArgs> for PruneImages {
|
||||
match periphery.request(api::docker::PruneImages {}).await {
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"prune images",
|
||||
"Prune Images",
|
||||
format!(
|
||||
"failed to prune images on server {} | {e:#?}",
|
||||
"Failed to prune images on server {} | {e:#?}",
|
||||
server.name
|
||||
),
|
||||
),
|
||||
@@ -1070,7 +1072,7 @@ impl Resolve<ExecuteArgs> for DeleteVolume {
|
||||
})
|
||||
.await
|
||||
.context(format!(
|
||||
"failed to delete volume {} on server {}",
|
||||
"Failed to delete volume {} on server {}",
|
||||
self.name, server.name
|
||||
)) {
|
||||
Ok(log) => log,
|
||||
@@ -1078,7 +1080,7 @@ impl Resolve<ExecuteArgs> for DeleteVolume {
|
||||
"delete volume",
|
||||
format_serror(
|
||||
&e.context(format!(
|
||||
"failed to delete volume {}",
|
||||
"Failed to delete volume {}",
|
||||
self.name
|
||||
))
|
||||
.into(),
|
||||
@@ -1139,9 +1141,9 @@ impl Resolve<ExecuteArgs> for PruneVolumes {
|
||||
match periphery.request(api::docker::PruneVolumes {}).await {
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"prune volumes",
|
||||
"Prune Volumes",
|
||||
format!(
|
||||
"failed to prune volumes on server {} | {e:#?}",
|
||||
"Failed to prune volumes on server {} | {e:#?}",
|
||||
server.name
|
||||
),
|
||||
),
|
||||
@@ -1200,9 +1202,9 @@ impl Resolve<ExecuteArgs> for PruneDockerBuilders {
|
||||
match periphery.request(api::build::PruneBuilders {}).await {
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"prune builders",
|
||||
"Prune Builders",
|
||||
format!(
|
||||
"failed to docker builder prune on server {} | {e:#?}",
|
||||
"Failed to docker builder prune on server {} | {e:#?}",
|
||||
server.name
|
||||
),
|
||||
),
|
||||
@@ -1261,9 +1263,9 @@ impl Resolve<ExecuteArgs> for PruneBuildx {
|
||||
match periphery.request(api::build::PruneBuildx {}).await {
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"prune buildx",
|
||||
"Prune Buildx",
|
||||
format!(
|
||||
"failed to docker buildx prune on server {} | {e:#?}",
|
||||
"Failed to docker buildx prune on server {} | {e:#?}",
|
||||
server.name
|
||||
),
|
||||
),
|
||||
@@ -1321,9 +1323,9 @@ impl Resolve<ExecuteArgs> for PruneSystem {
|
||||
let log = match periphery.request(api::PruneSystem {}).await {
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"prune system",
|
||||
"Prune System",
|
||||
format!(
|
||||
"failed to docker system prune on server {} | {e:#?}",
|
||||
"Failed to docker system prune on server {} | {e:#?}",
|
||||
server.name
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{collections::HashSet, str::FromStr};
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::{Context, anyhow};
|
||||
use database::mungos::mongodb::bson::{
|
||||
doc, oid::ObjectId, to_bson, to_document,
|
||||
};
|
||||
@@ -9,7 +9,7 @@ use interpolate::Interpolator;
|
||||
use komodo_client::{
|
||||
api::{execute::*, write::RefreshStackCache},
|
||||
entities::{
|
||||
FileContents,
|
||||
FileContents, SwarmOrServer,
|
||||
permission::PermissionLevel,
|
||||
repo::Repo,
|
||||
server::Server,
|
||||
@@ -20,8 +20,12 @@ use komodo_client::{
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use periphery_client::api::compose::*;
|
||||
use periphery_client::api::{
|
||||
DeployStackResponse, compose::*, swarm::DeploySwarmStack,
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCodeError as _;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
@@ -30,14 +34,15 @@ use crate::{
|
||||
periphery_client,
|
||||
query::{VariablesAndSecrets, get_variables_and_secrets},
|
||||
stack_git_token,
|
||||
swarm::swarm_request,
|
||||
update::{
|
||||
add_update_without_send, init_execution_update, update_update,
|
||||
},
|
||||
},
|
||||
monitor::update_cache_for_server,
|
||||
monitor::{update_cache_for_server, update_cache_for_swarm},
|
||||
permission::get_check_permissions,
|
||||
resource,
|
||||
stack::{execute::execute_compose, get_stack_and_server},
|
||||
stack::{execute::execute_compose, setup_stack_execution},
|
||||
state::{action_states, db_client},
|
||||
};
|
||||
|
||||
@@ -92,11 +97,10 @@ impl Resolve<ExecuteArgs> for DeployStack {
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let (mut stack, server) = get_stack_and_server(
|
||||
let (mut stack, swarm_or_server) = setup_stack_execution(
|
||||
&self.stack,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -165,27 +169,44 @@ impl Resolve<ExecuteArgs> for DeployStack {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
let ComposeUpResponse {
|
||||
let DeployStackResponse {
|
||||
logs,
|
||||
deployed,
|
||||
services,
|
||||
file_contents,
|
||||
missing_files,
|
||||
remote_errors,
|
||||
compose_config,
|
||||
merged_config,
|
||||
commit_hash,
|
||||
commit_message,
|
||||
} = periphery_client(&server)
|
||||
.await?
|
||||
.request(ComposeUp {
|
||||
stack: stack.clone(),
|
||||
services: self.services,
|
||||
repo,
|
||||
git_token,
|
||||
registry_token,
|
||||
replacers: secret_replacers.into_iter().collect(),
|
||||
})
|
||||
.await?;
|
||||
} = match &swarm_or_server {
|
||||
SwarmOrServer::Swarm(swarm) => {
|
||||
swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
DeploySwarmStack {
|
||||
stack: stack.clone(),
|
||||
repo,
|
||||
git_token,
|
||||
registry_token,
|
||||
replacers: secret_replacers.into_iter().collect(),
|
||||
},
|
||||
)
|
||||
.await?
|
||||
}
|
||||
SwarmOrServer::Server(server) => {
|
||||
periphery_client(server)
|
||||
.await?
|
||||
.request(ComposeUp {
|
||||
stack: stack.clone(),
|
||||
services: self.services,
|
||||
repo,
|
||||
git_token,
|
||||
registry_token,
|
||||
replacers: secret_replacers.into_iter().collect(),
|
||||
})
|
||||
.await?
|
||||
}
|
||||
};
|
||||
|
||||
update.logs.extend(logs);
|
||||
|
||||
@@ -219,7 +240,7 @@ impl Resolve<ExecuteArgs> for DeployStack {
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
compose_config,
|
||||
merged_config,
|
||||
commit_hash.clone(),
|
||||
commit_message.clone(),
|
||||
)
|
||||
@@ -257,7 +278,7 @@ impl Resolve<ExecuteArgs> for DeployStack {
|
||||
};
|
||||
|
||||
let info = to_document(&info)
|
||||
.context("failed to serialize stack info to bson")?;
|
||||
.context("Failed to serialize stack info to bson")?;
|
||||
|
||||
db_client()
|
||||
.stacks
|
||||
@@ -266,22 +287,29 @@ impl Resolve<ExecuteArgs> for DeployStack {
|
||||
doc! { "$set": { "info": info } },
|
||||
)
|
||||
.await
|
||||
.context("failed to update stack info on db")?;
|
||||
.context("Failed to update stack info on db")?;
|
||||
anyhow::Ok(())
|
||||
};
|
||||
|
||||
// This will be weird with single service deploys. Come back to it.
|
||||
if let Err(e) = update_info.await {
|
||||
update.push_error_log(
|
||||
"refresh stack info",
|
||||
"Refresh Stack Info",
|
||||
format_serror(
|
||||
&e.context("failed to refresh stack info on db").into(),
|
||||
&e.context("Failed to refresh stack info on db").into(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Ensure cached stack state up to date by updating server cache
|
||||
update_cache_for_server(&server, true).await;
|
||||
match swarm_or_server {
|
||||
SwarmOrServer::Swarm(swarm) => {
|
||||
update_cache_for_swarm(&swarm, true).await;
|
||||
}
|
||||
SwarmOrServer::Server(server) => {
|
||||
update_cache_for_server(&server, true).await;
|
||||
}
|
||||
}
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
@@ -862,14 +890,22 @@ impl Resolve<ExecuteArgs> for PullStack {
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let (stack, server) = get_stack_and_server(
|
||||
let (stack, swarm_or_server) = setup_stack_execution(
|
||||
&self.stack,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let SwarmOrServer::Server(server) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"PullStack should not be called for Stack in Swarm Mode"
|
||||
)
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
};
|
||||
|
||||
let repo = if !stack.config.files_on_host
|
||||
&& !stack.config.linked_repo.is_empty()
|
||||
{
|
||||
@@ -1136,14 +1172,22 @@ impl Resolve<ExecuteArgs> for RunStackService {
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let (mut stack, server) = get_stack_and_server(
|
||||
let (mut stack, swarm_or_server) = setup_stack_execution(
|
||||
&self.stack,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let SwarmOrServer::Server(server) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"RunStackService should not be called for Stack in Swarm Mode"
|
||||
)
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
};
|
||||
|
||||
let mut repo = if !stack.config.files_on_host
|
||||
&& !stack.config.linked_repo.is_empty()
|
||||
{
|
||||
|
||||
274
bin/core/src/api/execute/swarm.rs
Normal file
274
bin/core/src/api/execute/swarm.rs
Normal file
@@ -0,0 +1,274 @@
|
||||
use formatting::format_serror;
|
||||
use komodo_client::{
|
||||
api::execute::{
|
||||
RemoveSwarmConfigs, RemoveSwarmNodes, RemoveSwarmSecrets,
|
||||
RemoveSwarmServices, RemoveSwarmStacks,
|
||||
},
|
||||
entities::{permission::PermissionLevel, swarm::Swarm},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
api::execute::ExecuteArgs,
|
||||
helpers::{swarm::swarm_request, update::update_update},
|
||||
permission::get_check_permissions,
|
||||
};
|
||||
|
||||
impl Resolve<ExecuteArgs> for RemoveSwarmNodes {
|
||||
#[instrument(
|
||||
"RemoveSwarmNodes",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
swarm = self.swarm,
|
||||
nodes = serde_json::to_string(&self.nodes).unwrap_or_else(|e| e.to_string()),
|
||||
force = self.force,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
match swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::RemoveSwarmNodes {
|
||||
nodes: self.nodes,
|
||||
force: self.force,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(log) => update.logs.push(log),
|
||||
Err(e) => update.push_error_log(
|
||||
"Remove Swarm Nodes",
|
||||
format_serror(
|
||||
&e.context("Failed to remove swarm nodes").into(),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for RemoveSwarmStacks {
|
||||
#[instrument(
|
||||
"RemoveSwarmStacks",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
swarm = self.swarm,
|
||||
stacks = serde_json::to_string(&self.stacks).unwrap_or_else(|e| e.to_string()),
|
||||
detach = self.detach,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
match swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::RemoveSwarmStacks {
|
||||
stacks: self.stacks,
|
||||
detach: self.detach,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(log) => update.logs.push(log),
|
||||
Err(e) => update.push_error_log(
|
||||
"Remove Swarm Stacks",
|
||||
format_serror(
|
||||
&e.context("Failed to remove swarm stacks").into(),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for RemoveSwarmServices {
|
||||
#[instrument(
|
||||
"RemoveSwarmServices",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
swarm = self.swarm,
|
||||
services = serde_json::to_string(&self.services).unwrap_or_else(|e| e.to_string()),
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
match swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::RemoveSwarmServices {
|
||||
services: self.services,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(log) => update.logs.push(log),
|
||||
Err(e) => update.push_error_log(
|
||||
"Remove Swarm Services",
|
||||
format_serror(
|
||||
&e.context("Failed to remove swarm services").into(),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for RemoveSwarmConfigs {
|
||||
#[instrument(
|
||||
"RemoveSwarmConfigs",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
swarm = self.swarm,
|
||||
configs = serde_json::to_string(&self.configs).unwrap_or_else(|e| e.to_string()),
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
match swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::RemoveSwarmConfigs {
|
||||
configs: self.configs,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(log) => update.logs.push(log),
|
||||
Err(e) => update.push_error_log(
|
||||
"Remove Swarm Configs",
|
||||
format_serror(
|
||||
&e.context("Failed to remove swarm configs").into(),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for RemoveSwarmSecrets {
|
||||
#[instrument(
|
||||
"RemoveSwarmSecrets",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
swarm = self.swarm,
|
||||
secrets = serde_json::to_string(&self.secrets).unwrap_or_else(|e| e.to_string()),
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
match swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::RemoveSwarmSecrets {
|
||||
secrets: self.secrets,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(log) => update.logs.push(log),
|
||||
Err(e) => update.push_error_log(
|
||||
"Remove Swarm Secrets",
|
||||
format_serror(
|
||||
&e.context("Failed to remove swarm secrets").into(),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,11 @@
|
||||
use axum::{Router, extract::Path, http::HeaderMap, routing::post};
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
|
||||
use axum::{
|
||||
Router,
|
||||
extract::{ConnectInfo, Path},
|
||||
http::HeaderMap,
|
||||
routing::post,
|
||||
};
|
||||
use komodo_client::entities::{
|
||||
action::Action, build::Build, procedure::Procedure, repo::Repo,
|
||||
resource::Resource, stack::Stack, sync::ResourceSync,
|
||||
@@ -48,9 +55,9 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
|
||||
.route(
|
||||
"/build/{id}",
|
||||
post(
|
||||
|Path(Id { id }), headers: HeaderMap, body: String| async move {
|
||||
|Path(Id { id }), headers: HeaderMap, ConnectInfo(info): ConnectInfo<SocketAddr>, body: String| async move {
|
||||
let build =
|
||||
auth_webhook::<P, Build>(&id, &headers, &body).await?;
|
||||
auth_webhook::<P, Build>(&id, &headers, info.ip(), &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("BuildWebhook", id);
|
||||
async {
|
||||
@@ -74,9 +81,9 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
|
||||
.route(
|
||||
"/repo/{id}/{option}",
|
||||
post(
|
||||
|Path(IdAndOption::<RepoWebhookOption> { id, option }), headers: HeaderMap, body: String| async move {
|
||||
|Path(IdAndOption::<RepoWebhookOption> { id, option }), headers: HeaderMap, ConnectInfo(info): ConnectInfo<SocketAddr>, body: String| async move {
|
||||
let repo =
|
||||
auth_webhook::<P, Repo>(&id, &headers, &body).await?;
|
||||
auth_webhook::<P, Repo>(&id, &headers, info.ip(), &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("RepoWebhook", id);
|
||||
async {
|
||||
@@ -100,9 +107,9 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
|
||||
.route(
|
||||
"/stack/{id}/{option}",
|
||||
post(
|
||||
|Path(IdAndOption::<StackWebhookOption> { id, option }), headers: HeaderMap, body: String| async move {
|
||||
|Path(IdAndOption::<StackWebhookOption> { id, option }), headers: HeaderMap, ConnectInfo(info): ConnectInfo<SocketAddr>, body: String| async move {
|
||||
let stack =
|
||||
auth_webhook::<P, Stack>(&id, &headers, &body).await?;
|
||||
auth_webhook::<P, Stack>(&id, &headers, info.ip(), &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("StackWebhook", id);
|
||||
async {
|
||||
@@ -126,9 +133,9 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
|
||||
.route(
|
||||
"/sync/{id}/{option}",
|
||||
post(
|
||||
|Path(IdAndOption::<SyncWebhookOption> { id, option }), headers: HeaderMap, body: String| async move {
|
||||
|Path(IdAndOption::<SyncWebhookOption> { id, option }), headers: HeaderMap, ConnectInfo(info): ConnectInfo<SocketAddr>, body: String| async move {
|
||||
let sync =
|
||||
auth_webhook::<P, ResourceSync>(&id, &headers, &body).await?;
|
||||
auth_webhook::<P, ResourceSync>(&id, &headers, info.ip(), &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("ResourceSyncWebhook", id);
|
||||
async {
|
||||
@@ -152,9 +159,9 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
|
||||
.route(
|
||||
"/procedure/{id}/{branch}",
|
||||
post(
|
||||
|Path(IdAndBranch { id, branch }), headers: HeaderMap, body: String| async move {
|
||||
|Path(IdAndBranch { id, branch }), headers: HeaderMap, ConnectInfo(info): ConnectInfo<SocketAddr>, body: String| async move {
|
||||
let procedure =
|
||||
auth_webhook::<P, Procedure>(&id, &headers, &body).await?;
|
||||
auth_webhook::<P, Procedure>(&id, &headers, info.ip(), &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("ProcedureWebhook", id);
|
||||
async {
|
||||
@@ -178,9 +185,9 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
|
||||
.route(
|
||||
"/action/{id}/{branch}",
|
||||
post(
|
||||
|Path(IdAndBranch { id, branch }), headers: HeaderMap, body: String| async move {
|
||||
|Path(IdAndBranch { id, branch }), headers: HeaderMap, ConnectInfo(info): ConnectInfo<SocketAddr>, body: String| async move {
|
||||
let action =
|
||||
auth_webhook::<P, Action>(&id, &headers, &body).await?;
|
||||
auth_webhook::<P, Action>(&id, &headers, info.ip(), &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("ActionWebhook", id);
|
||||
async {
|
||||
@@ -206,6 +213,7 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
|
||||
async fn auth_webhook<P, R>(
|
||||
id: &str,
|
||||
headers: &HeaderMap,
|
||||
ip: IpAddr,
|
||||
body: &str,
|
||||
) -> serror::Result<Resource<R::Config, R::Info>>
|
||||
where
|
||||
@@ -220,6 +228,10 @@ where
|
||||
.status_code(StatusCode::UNAUTHORIZED)?;
|
||||
serror::Result::Ok(resource)
|
||||
}
|
||||
.with_failure_rate_limit_using_headers(auth_rate_limiter(), headers)
|
||||
.with_failure_rate_limit_using_headers(
|
||||
auth_rate_limiter(),
|
||||
headers,
|
||||
Some(ip),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ use anyhow::{Context, anyhow};
|
||||
use komodo_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
SwarmOrServer,
|
||||
docker::container::Container,
|
||||
permission::PermissionLevel,
|
||||
server::{Server, ServerState},
|
||||
stack::{Stack, StackActionState, StackListItem, StackState},
|
||||
},
|
||||
};
|
||||
@@ -14,14 +14,18 @@ use periphery_client::api::{
|
||||
compose::{GetComposeLog, GetComposeLogSearch},
|
||||
container::InspectContainer,
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCodeError as _;
|
||||
|
||||
use crate::{
|
||||
helpers::{periphery_client, query::get_all_tags},
|
||||
helpers::{
|
||||
periphery_client, query::get_all_tags, swarm::swarm_request,
|
||||
},
|
||||
permission::get_check_permissions,
|
||||
resource,
|
||||
stack::get_stack_and_server,
|
||||
state::{action_states, server_status_cache, stack_status_cache},
|
||||
stack::setup_stack_execution,
|
||||
state::{action_states, stack_status_cache},
|
||||
};
|
||||
|
||||
use super::ReadArgs;
|
||||
@@ -73,28 +77,49 @@ impl Resolve<ReadArgs> for GetStackLog {
|
||||
) -> serror::Result<GetStackLogResponse> {
|
||||
let GetStackLog {
|
||||
stack,
|
||||
services,
|
||||
mut services,
|
||||
tail,
|
||||
timestamps,
|
||||
} = self;
|
||||
let (stack, server) = get_stack_and_server(
|
||||
let (stack, swarm_or_server) = setup_stack_execution(
|
||||
&stack,
|
||||
user,
|
||||
PermissionLevel::Read.logs(),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
let res = periphery_client(&server)
|
||||
.await?
|
||||
.request(GetComposeLog {
|
||||
project: stack.project_name(false),
|
||||
services,
|
||||
tail,
|
||||
timestamps,
|
||||
})
|
||||
.await
|
||||
.context("Failed to get stack log from periphery")?;
|
||||
Ok(res)
|
||||
|
||||
let log = match swarm_or_server {
|
||||
SwarmOrServer::Swarm(swarm) => {
|
||||
let service = services.pop().context(
|
||||
"Must pass single service for Swarm mode Stack logs",
|
||||
)?;
|
||||
swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::GetSwarmServiceLog {
|
||||
service,
|
||||
tail,
|
||||
timestamps,
|
||||
no_task_ids: false,
|
||||
no_resolve: false,
|
||||
details: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("Failed to get stack service log from swarm")?
|
||||
}
|
||||
SwarmOrServer::Server(server) => periphery_client(&server)
|
||||
.await?
|
||||
.request(GetComposeLog {
|
||||
project: stack.project_name(false),
|
||||
services,
|
||||
tail,
|
||||
timestamps,
|
||||
})
|
||||
.await
|
||||
.context("Failed to get stack log from periphery")?,
|
||||
};
|
||||
|
||||
Ok(log)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,32 +130,55 @@ impl Resolve<ReadArgs> for SearchStackLog {
|
||||
) -> serror::Result<SearchStackLogResponse> {
|
||||
let SearchStackLog {
|
||||
stack,
|
||||
services,
|
||||
mut services,
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
} = self;
|
||||
let (stack, server) = get_stack_and_server(
|
||||
let (stack, swarm_or_server) = setup_stack_execution(
|
||||
&stack,
|
||||
user,
|
||||
PermissionLevel::Read.logs(),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
let res = periphery_client(&server)
|
||||
.await?
|
||||
.request(GetComposeLogSearch {
|
||||
project: stack.project_name(false),
|
||||
services,
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
})
|
||||
.await
|
||||
.context("Failed to search stack log from periphery")?;
|
||||
Ok(res)
|
||||
|
||||
let log = match swarm_or_server {
|
||||
SwarmOrServer::Swarm(swarm) => {
|
||||
let service = services.pop().context(
|
||||
"Must pass single service for Swarm mode Stack logs",
|
||||
)?;
|
||||
swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::GetSwarmServiceLogSearch {
|
||||
service,
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
no_task_ids: false,
|
||||
no_resolve: false,
|
||||
details: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("Failed to get stack service log from swarm")?
|
||||
}
|
||||
SwarmOrServer::Server(server) => periphery_client(&server)
|
||||
.await?
|
||||
.request(GetComposeLogSearch {
|
||||
project: stack.project_name(false),
|
||||
services,
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
})
|
||||
.await
|
||||
.context("Failed to search stack log from periphery")?,
|
||||
};
|
||||
|
||||
Ok(log)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,38 +188,29 @@ impl Resolve<ReadArgs> for InspectStackContainer {
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Container> {
|
||||
let InspectStackContainer { stack, service } = self;
|
||||
let stack = get_check_permissions::<Stack>(
|
||||
let (stack, swarm_or_server) = setup_stack_execution(
|
||||
&stack,
|
||||
user,
|
||||
PermissionLevel::Read.inspect(),
|
||||
PermissionLevel::Read.logs(),
|
||||
)
|
||||
.await?;
|
||||
if stack.config.server_id.is_empty() {
|
||||
return Err(
|
||||
anyhow!("Cannot inspect stack, not attached to any server")
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
let server =
|
||||
resource::get::<Server>(&stack.config.server_id).await?;
|
||||
let cache = server_status_cache()
|
||||
.get_or_insert_default(&server.id)
|
||||
.await;
|
||||
if cache.state != ServerState::Ok {
|
||||
|
||||
let SwarmOrServer::Server(server) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"Cannot inspect container: server is {:?}",
|
||||
cache.state
|
||||
"InspectStackContainer should not be called for Stack in Swarm Mode"
|
||||
)
|
||||
.into(),
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let services = &stack_status_cache()
|
||||
.get(&stack.id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.curr
|
||||
.services;
|
||||
|
||||
let Some(name) = services
|
||||
.iter()
|
||||
.find(|s| s.service == service)
|
||||
@@ -181,10 +220,12 @@ impl Resolve<ReadArgs> for InspectStackContainer {
|
||||
"No service found matching '{service}'. Was the stack last deployed manually?"
|
||||
).into());
|
||||
};
|
||||
|
||||
let res = periphery_client(&server)
|
||||
.await?
|
||||
.request(InspectContainer { name })
|
||||
.await?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ use komodo_client::{
|
||||
all_logs_success,
|
||||
permission::PermissionLevel,
|
||||
repo::Repo,
|
||||
server::ServerState,
|
||||
stack::{Stack, StackInfo},
|
||||
update::Update,
|
||||
user::stack_user,
|
||||
@@ -25,9 +24,8 @@ use resolver_api::Resolve;
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::{
|
||||
periphery_client,
|
||||
query::get_server_with_state,
|
||||
stack_git_token,
|
||||
query::get_swarm_or_server,
|
||||
stack_git_token, swarm_or_server_request,
|
||||
update::{add_update, make_update},
|
||||
},
|
||||
permission::get_check_permissions,
|
||||
@@ -204,32 +202,24 @@ async fn write_stack_file_contents_on_host(
|
||||
contents: String,
|
||||
mut update: Update,
|
||||
) -> serror::Result<Update> {
|
||||
if stack.config.server_id.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"Cannot write file, Files on host Stack has not configured a Server"
|
||||
).into());
|
||||
}
|
||||
let (server, state) =
|
||||
get_server_with_state(&stack.config.server_id).await?;
|
||||
if state != ServerState::Ok {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"Cannot write file when server is unreachable or disabled"
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
match periphery_client(&server)
|
||||
.await?
|
||||
.request(WriteComposeContentsToHost {
|
||||
let swarm_or_server = get_swarm_or_server(
|
||||
&stack.config.swarm_id,
|
||||
&stack.config.server_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let res = swarm_or_server_request(
|
||||
&swarm_or_server,
|
||||
WriteComposeContentsToHost {
|
||||
name: stack.name,
|
||||
run_directory: stack.config.run_directory,
|
||||
file_path,
|
||||
contents,
|
||||
})
|
||||
.await
|
||||
.context("Failed to write contents to host")
|
||||
{
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(log) => {
|
||||
update.logs.push(log);
|
||||
}
|
||||
@@ -239,7 +229,7 @@ async fn write_stack_file_contents_on_host(
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if !all_logs_success(&update.logs) {
|
||||
update.finalize();
|
||||
@@ -459,26 +449,22 @@ impl Resolve<WriteArgs> for RefreshStackCache {
|
||||
// =============
|
||||
// FILES ON HOST
|
||||
// =============
|
||||
let (server, state) = if stack.config.server_id.is_empty() {
|
||||
(None, ServerState::Disabled)
|
||||
} else {
|
||||
let (server, state) =
|
||||
get_server_with_state(&stack.config.server_id).await?;
|
||||
(Some(server), state)
|
||||
};
|
||||
if state != ServerState::Ok {
|
||||
(vec![], None, None, None, None)
|
||||
} else if let Some(server) = server {
|
||||
if let Ok(swarm_or_server) = get_swarm_or_server(
|
||||
&stack.config.swarm_id,
|
||||
&stack.config.server_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
let GetComposeContentsOnHostResponse { contents, errors } =
|
||||
match periphery_client(&server)
|
||||
.await?
|
||||
.request(GetComposeContentsOnHost {
|
||||
match swarm_or_server_request(
|
||||
&swarm_or_server,
|
||||
GetComposeContentsOnHost {
|
||||
file_paths: stack.all_file_dependencies(),
|
||||
name: stack.name.clone(),
|
||||
run_directory: stack.config.run_directory.clone(),
|
||||
})
|
||||
.await
|
||||
.context("failed to get compose file contents from host")
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(res) => res,
|
||||
Err(e) => GetComposeContentsOnHostResponse {
|
||||
@@ -489,7 +475,6 @@ impl Resolve<WriteArgs> for RefreshStackCache {
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
let project_name = stack.project_name(true);
|
||||
|
||||
let mut services = Vec::new();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::net::IpAddr;
|
||||
|
||||
use crate::{
|
||||
auth::{auth_api_key_check_enabled, auth_jwt_check_enabled},
|
||||
helpers::query::get_user,
|
||||
@@ -30,6 +32,7 @@ pub fn router() -> Router {
|
||||
async fn user_ws_login(
|
||||
mut socket: WebSocket,
|
||||
headers: &HeaderMap,
|
||||
fallback_ip: IpAddr,
|
||||
) -> Option<(WebSocket, User)> {
|
||||
let res = async {
|
||||
let message = match socket
|
||||
@@ -66,7 +69,11 @@ async fn user_ws_login(
|
||||
}
|
||||
}
|
||||
}
|
||||
.with_failure_rate_limit_using_headers(auth_rate_limiter(), headers)
|
||||
.with_failure_rate_limit_using_headers(
|
||||
auth_rate_limiter(),
|
||||
headers,
|
||||
Some(fallback_ip),
|
||||
)
|
||||
.await;
|
||||
match res {
|
||||
Ok(user) => {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use axum::{
|
||||
extract::{FromRequestParts, WebSocketUpgrade, ws},
|
||||
extract::{ConnectInfo, FromRequestParts, WebSocketUpgrade, ws},
|
||||
http::{HeaderMap, request},
|
||||
response::IntoResponse,
|
||||
};
|
||||
@@ -22,12 +24,14 @@ use crate::{
|
||||
#[instrument("ConnectTerminal", skip(ws))]
|
||||
pub async fn handler(
|
||||
Qs(query): Qs<ConnectTerminalQuery>,
|
||||
ConnectInfo(info): ConnectInfo<SocketAddr>,
|
||||
headers: HeaderMap,
|
||||
ws: WebSocketUpgrade,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(|socket| async move {
|
||||
let ip = info.ip();
|
||||
ws.on_upgrade(move |socket| async move {
|
||||
let Some((mut client_socket, user)) =
|
||||
super::user_ws_login(socket, &headers).await
|
||||
super::user_ws_login(socket, &headers, ip).await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use axum::{
|
||||
extract::{WebSocketUpgrade, ws::Message},
|
||||
extract::{ConnectInfo, WebSocketUpgrade, ws::Message},
|
||||
http::HeaderMap,
|
||||
response::IntoResponse,
|
||||
};
|
||||
@@ -19,15 +21,17 @@ use crate::helpers::{
|
||||
|
||||
pub async fn handler(
|
||||
headers: HeaderMap,
|
||||
ConnectInfo(info): ConnectInfo<SocketAddr>,
|
||||
ws: WebSocketUpgrade,
|
||||
) -> impl IntoResponse {
|
||||
// get a reveiver for internal update messages.
|
||||
let mut receiver = update_channel().receiver.resubscribe();
|
||||
let ip = info.ip();
|
||||
|
||||
// handle http -> ws updgrade
|
||||
ws.on_upgrade(|socket| async move {
|
||||
ws.on_upgrade(move |socket| async move {
|
||||
let Some((client_socket, user)) =
|
||||
super::user_ws_login(socket, &headers).await
|
||||
super::user_ws_login(socket, &headers, ip).await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use axum::{
|
||||
Router, extract::Query, http::HeaderMap, response::Redirect,
|
||||
Router,
|
||||
extract::{ConnectInfo, Query},
|
||||
http::HeaderMap,
|
||||
response::Redirect,
|
||||
routing::get,
|
||||
};
|
||||
use database::mongo_indexed::Document;
|
||||
@@ -42,15 +47,20 @@ pub fn router() -> Router {
|
||||
)
|
||||
.route(
|
||||
"/callback",
|
||||
get(|query, headers: HeaderMap| async move {
|
||||
callback(query)
|
||||
.map_err(|e| e.status_code(StatusCode::UNAUTHORIZED))
|
||||
.with_failure_rate_limit_using_headers(
|
||||
auth_rate_limiter(),
|
||||
&headers,
|
||||
)
|
||||
.await
|
||||
}),
|
||||
get(
|
||||
|query,
|
||||
headers: HeaderMap,
|
||||
ConnectInfo(info): ConnectInfo<SocketAddr>| async move {
|
||||
callback(query)
|
||||
.map_err(|e| e.status_code(StatusCode::UNAUTHORIZED))
|
||||
.with_failure_rate_limit_using_headers(
|
||||
auth_rate_limiter(),
|
||||
&headers,
|
||||
Some(info.ip()),
|
||||
)
|
||||
.await
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use async_timing_util::unix_timestamp_ms;
|
||||
use axum::{
|
||||
Router, extract::Query, http::HeaderMap, response::Redirect,
|
||||
Router,
|
||||
extract::{ConnectInfo, Query},
|
||||
http::HeaderMap,
|
||||
response::Redirect,
|
||||
routing::get,
|
||||
};
|
||||
use database::mongo_indexed::Document;
|
||||
@@ -43,15 +48,20 @@ pub fn router() -> Router {
|
||||
)
|
||||
.route(
|
||||
"/callback",
|
||||
get(|query, headers: HeaderMap| async move {
|
||||
callback(query)
|
||||
.map_err(|e| e.status_code(StatusCode::UNAUTHORIZED))
|
||||
.with_failure_rate_limit_using_headers(
|
||||
auth_rate_limiter(),
|
||||
&headers,
|
||||
)
|
||||
.await
|
||||
}),
|
||||
get(
|
||||
|query,
|
||||
headers: HeaderMap,
|
||||
ConnectInfo(info): ConnectInfo<SocketAddr>| async move {
|
||||
callback(query)
|
||||
.map_err(|e| e.status_code(StatusCode::UNAUTHORIZED))
|
||||
.with_failure_rate_limit_using_headers(
|
||||
auth_rate_limiter(),
|
||||
&headers,
|
||||
Some(info.ip()),
|
||||
)
|
||||
.await
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -29,12 +29,13 @@ impl Resolve<AuthArgs> for SignUpLocalUser {
|
||||
#[instrument("SignUpLocalUser", skip(self))]
|
||||
async fn resolve(
|
||||
self,
|
||||
AuthArgs { headers }: &AuthArgs,
|
||||
AuthArgs { headers, ip }: &AuthArgs,
|
||||
) -> serror::Result<SignUpLocalUserResponse> {
|
||||
sign_up_local_user(self)
|
||||
.with_failure_rate_limit_using_headers(
|
||||
auth_rate_limiter(),
|
||||
headers,
|
||||
Some(*ip),
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -139,12 +140,13 @@ fn login_local_user_rate_limiter() -> &'static RateLimiter {
|
||||
impl Resolve<AuthArgs> for LoginLocalUser {
|
||||
async fn resolve(
|
||||
self,
|
||||
AuthArgs { headers }: &AuthArgs,
|
||||
AuthArgs { headers, ip }: &AuthArgs,
|
||||
) -> serror::Result<LoginLocalUserResponse> {
|
||||
login_local_user(self)
|
||||
.with_failure_rate_limit_using_headers(
|
||||
login_local_user_rate_limiter(),
|
||||
headers,
|
||||
Some(*ip),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use async_timing_util::unix_timestamp_ms;
|
||||
use axum::{
|
||||
extract::Request, http::HeaderMap, middleware::Next,
|
||||
extract::{ConnectInfo, Request},
|
||||
http::HeaderMap,
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
};
|
||||
use database::mungos::mongodb::bson::doc;
|
||||
@@ -45,11 +49,16 @@ pub async fn auth_request(
|
||||
mut req: Request,
|
||||
next: Next,
|
||||
) -> serror::Result<Response> {
|
||||
let fallback = req
|
||||
.extensions()
|
||||
.get::<ConnectInfo<SocketAddr>>()
|
||||
.map(|addr| addr.ip());
|
||||
let user = authenticate_check_enabled(&headers)
|
||||
.map_err(|e| e.status_code(StatusCode::UNAUTHORIZED))
|
||||
.with_failure_rate_limit_using_headers(
|
||||
auth_rate_limiter(),
|
||||
&headers,
|
||||
fallback,
|
||||
)
|
||||
.await?;
|
||||
req.extensions_mut().insert(user);
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
use std::sync::OnceLock;
|
||||
use std::{net::SocketAddr, sync::OnceLock};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use axum::{
|
||||
Router, extract::Query, http::HeaderMap, response::Redirect,
|
||||
Router,
|
||||
extract::{ConnectInfo, Query},
|
||||
http::HeaderMap,
|
||||
response::Redirect,
|
||||
routing::get,
|
||||
};
|
||||
use client::oidc_client;
|
||||
@@ -71,15 +74,20 @@ pub fn router() -> Router {
|
||||
)
|
||||
.route(
|
||||
"/callback",
|
||||
get(|query, headers: HeaderMap| async move {
|
||||
callback(query)
|
||||
.map_err(|e| e.status_code(StatusCode::UNAUTHORIZED))
|
||||
.with_failure_rate_limit_using_headers(
|
||||
auth_rate_limiter(),
|
||||
&headers,
|
||||
)
|
||||
.await
|
||||
}),
|
||||
get(
|
||||
|query,
|
||||
headers: HeaderMap,
|
||||
ConnectInfo(info): ConnectInfo<SocketAddr>| async move {
|
||||
callback(query)
|
||||
.map_err(|e| e.status_code(StatusCode::UNAUTHORIZED))
|
||||
.with_failure_rate_limit_using_headers(
|
||||
auth_rate_limiter(),
|
||||
&headers,
|
||||
Some(info.ip()),
|
||||
)
|
||||
.await
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ use anyhow::{Context, anyhow};
|
||||
use database::mongo_indexed::Document;
|
||||
use database::mungos::mongodb::bson::{Bson, doc};
|
||||
use indexmap::IndexSet;
|
||||
use komodo_client::entities::SwarmOrServer;
|
||||
use komodo_client::entities::{
|
||||
ResourceTarget,
|
||||
build::Build,
|
||||
@@ -15,7 +16,11 @@ use komodo_client::entities::{
|
||||
stack::Stack,
|
||||
user::User,
|
||||
};
|
||||
use resolver_api::HasResponse;
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
use crate::helpers::swarm::swarm_request;
|
||||
use crate::{
|
||||
config::core_config, connection::PeripheryConnectionArgs,
|
||||
periphery::PeripheryClient, state::db_client,
|
||||
@@ -264,3 +269,21 @@ pub fn repo_link(
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
pub async fn swarm_or_server_request<T>(
|
||||
swarm_or_server: &SwarmOrServer,
|
||||
request: T,
|
||||
) -> anyhow::Result<T::Response>
|
||||
where
|
||||
T: std::fmt::Debug + Clone + Serialize + HasResponse,
|
||||
T::Response: DeserializeOwned,
|
||||
{
|
||||
match swarm_or_server {
|
||||
SwarmOrServer::Swarm(swarm) => {
|
||||
swarm_request(&swarm.config.server_ids, request).await
|
||||
}
|
||||
SwarmOrServer::Server(server) => {
|
||||
periphery_client(server).await?.request(request).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1162,6 +1162,91 @@ async fn execute_execution(
|
||||
)
|
||||
.await?
|
||||
}
|
||||
Execution::RemoveSwarmNodes(req) => {
|
||||
let req = ExecuteRequest::RemoveSwarmNodes(req);
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::RemoveSwarmNodes(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
let update_id = update.id.clone();
|
||||
handle_resolve_result(
|
||||
req
|
||||
.resolve(&ExecuteArgs { user, update, id })
|
||||
.await
|
||||
.map_err(|e| e.error)
|
||||
.context("Failed at RemoveSwarmNodes"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
Execution::RemoveSwarmStacks(req) => {
|
||||
let req = ExecuteRequest::RemoveSwarmStacks(req);
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::RemoveSwarmStacks(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
let update_id = update.id.clone();
|
||||
handle_resolve_result(
|
||||
req
|
||||
.resolve(&ExecuteArgs { user, update, id })
|
||||
.await
|
||||
.map_err(|e| e.error)
|
||||
.context("Failed at RemoveSwarmStacks"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
Execution::RemoveSwarmServices(req) => {
|
||||
let req = ExecuteRequest::RemoveSwarmServices(req);
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::RemoveSwarmServices(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
let update_id = update.id.clone();
|
||||
handle_resolve_result(
|
||||
req
|
||||
.resolve(&ExecuteArgs { user, update, id })
|
||||
.await
|
||||
.map_err(|e| e.error)
|
||||
.context("Failed at RemoveSwarmServices"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
Execution::RemoveSwarmConfigs(req) => {
|
||||
let req = ExecuteRequest::RemoveSwarmConfigs(req);
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::RemoveSwarmConfigs(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
let update_id = update.id.clone();
|
||||
handle_resolve_result(
|
||||
req
|
||||
.resolve(&ExecuteArgs { user, update, id })
|
||||
.await
|
||||
.map_err(|e| e.error)
|
||||
.context("Failed at RemoveSwarmConfigs"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
Execution::RemoveSwarmSecrets(req) => {
|
||||
let req = ExecuteRequest::RemoveSwarmSecrets(req);
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::RemoveSwarmSecrets(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
let update_id = update.id.clone();
|
||||
handle_resolve_result(
|
||||
req
|
||||
.resolve(&ExecuteArgs { user, update, id })
|
||||
.await
|
||||
.map_err(|e| e.error)
|
||||
.context("Failed at RemoveSwarmSecrets"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
Execution::ClearRepoCache(req) => {
|
||||
let req = ExecuteRequest::ClearRepoCache(req);
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
|
||||
@@ -11,7 +11,7 @@ use database::mungos::{
|
||||
use komodo_client::{
|
||||
busy::Busy,
|
||||
entities::{
|
||||
Operation, ResourceTarget, ResourceTargetVariant,
|
||||
Operation, ResourceTarget, ResourceTargetVariant, SwarmOrServer,
|
||||
action::{Action, ActionState},
|
||||
alerter::Alerter,
|
||||
build::Build,
|
||||
@@ -37,6 +37,7 @@ use komodo_client::{
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::swarm::swarm_request,
|
||||
permission::get_user_permission_on_resource,
|
||||
resource::{self, KomodoResource},
|
||||
stack::compose_container_match_regex,
|
||||
@@ -60,6 +61,25 @@ pub async fn get_user(user: &str) -> anyhow::Result<User> {
|
||||
.with_context(|| format!("No user found matching '{user}'"))
|
||||
}
|
||||
|
||||
pub async fn get_swarm_with_reachability(
|
||||
swarm_id_or_name: &str,
|
||||
) -> anyhow::Result<(Swarm, bool)> {
|
||||
let swarm = resource::get::<Swarm>(swarm_id_or_name).await?;
|
||||
let reachable = get_swarm_reachability(&swarm).await.is_ok();
|
||||
Ok((swarm, reachable))
|
||||
}
|
||||
|
||||
pub async fn get_swarm_reachability(
|
||||
swarm: &Swarm,
|
||||
) -> anyhow::Result<()> {
|
||||
swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::GetVersion {},
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
pub async fn get_server_with_state(
|
||||
server_id_or_name: &str,
|
||||
) -> anyhow::Result<(Server, ServerState)> {
|
||||
@@ -451,3 +471,35 @@ pub async fn get_procedure_state(id: &String) -> ProcedureState {
|
||||
}
|
||||
procedure_state_cache().get(id).await.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Get's a resource's assigned swarm or server, with swarm taking precedence.
|
||||
/// Makes sure the target is reachable before passing along for commands.
|
||||
pub async fn get_swarm_or_server(
|
||||
swarm_id: &str,
|
||||
server_id: &str,
|
||||
) -> anyhow::Result<SwarmOrServer> {
|
||||
if !swarm_id.is_empty() {
|
||||
let swarm = resource::get::<Swarm>(swarm_id).await?;
|
||||
|
||||
// Errors if not reachable, and returns the error
|
||||
get_swarm_reachability(&swarm).await?;
|
||||
|
||||
return Ok(SwarmOrServer::Swarm(swarm));
|
||||
}
|
||||
|
||||
if server_id.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"Neither Swarm nor Server has been configured in this resource."
|
||||
));
|
||||
}
|
||||
|
||||
let (server, state) = get_server_with_state(server_id).await?;
|
||||
|
||||
if state != ServerState::Ok {
|
||||
return Err(anyhow!(
|
||||
"Cannot send command when Server is unreachable or disabled"
|
||||
));
|
||||
}
|
||||
|
||||
Ok(SwarmOrServer::Server(server))
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use komodo_client::entities::{
|
||||
repo::Repo,
|
||||
server::Server,
|
||||
stack::Stack,
|
||||
swarm::Swarm,
|
||||
sync::ResourceSync,
|
||||
update::{Update, UpdateListItem},
|
||||
user::User,
|
||||
@@ -121,6 +122,38 @@ pub async fn init_execution_update(
|
||||
user: &User,
|
||||
) -> anyhow::Result<Update> {
|
||||
let (operation, target) = match &request {
|
||||
// Swarm
|
||||
ExecuteRequest::RemoveSwarmNodes(data) => (
|
||||
Operation::RemoveSwarmNodes,
|
||||
ResourceTarget::Swarm(
|
||||
resource::get::<Swarm>(&data.swarm).await?.id,
|
||||
),
|
||||
),
|
||||
ExecuteRequest::RemoveSwarmStacks(data) => (
|
||||
Operation::RemoveSwarmStacks,
|
||||
ResourceTarget::Swarm(
|
||||
resource::get::<Swarm>(&data.swarm).await?.id,
|
||||
),
|
||||
),
|
||||
ExecuteRequest::RemoveSwarmServices(data) => (
|
||||
Operation::RemoveSwarmServices,
|
||||
ResourceTarget::Swarm(
|
||||
resource::get::<Swarm>(&data.swarm).await?.id,
|
||||
),
|
||||
),
|
||||
ExecuteRequest::RemoveSwarmConfigs(data) => (
|
||||
Operation::RemoveSwarmConfigs,
|
||||
ResourceTarget::Swarm(
|
||||
resource::get::<Swarm>(&data.swarm).await?.id,
|
||||
),
|
||||
),
|
||||
ExecuteRequest::RemoveSwarmSecrets(data) => (
|
||||
Operation::RemoveSwarmSecrets,
|
||||
ResourceTarget::Swarm(
|
||||
resource::get::<Swarm>(&data.swarm).await?.id,
|
||||
),
|
||||
),
|
||||
|
||||
// Server
|
||||
ExecuteRequest::StartContainer(data) => (
|
||||
Operation::StartContainer,
|
||||
|
||||
@@ -82,7 +82,8 @@ async fn app() -> anyhow::Result<()> {
|
||||
.instrument(startup_span)
|
||||
.await;
|
||||
|
||||
let app = api::app().into_make_service();
|
||||
let app =
|
||||
api::app().into_make_service_with_connect_info::<SocketAddr>();
|
||||
|
||||
let addr =
|
||||
format!("{}:{}", core_config().bind_ip, core_config().port);
|
||||
|
||||
@@ -13,6 +13,7 @@ use komodo_client::entities::{
|
||||
server::{Server, ServerState},
|
||||
stack::Stack,
|
||||
stats::SystemStats,
|
||||
swarm::Swarm,
|
||||
};
|
||||
use periphery_client::api::{
|
||||
self, git::GetLatestCommit, poll::PollStatusResponse,
|
||||
@@ -114,7 +115,7 @@ pub async fn update_cache_for_server(server: &Server, force: bool) {
|
||||
|
||||
*lock = now;
|
||||
|
||||
let resources = UpdateCacheResources::load(server).await;
|
||||
let resources = UpdateCacheResources::load_server(server).await;
|
||||
|
||||
// Handle server disabled
|
||||
if !server.config.enabled {
|
||||
@@ -255,7 +256,34 @@ struct UpdateCacheResources {
|
||||
}
|
||||
|
||||
impl UpdateCacheResources {
|
||||
pub async fn load(server: &Server) -> Self {
|
||||
pub async fn load_swarm(swarm: &Swarm) -> Self {
|
||||
let (stacks, deployments, builds) = tokio::join!(
|
||||
find_collect(
|
||||
&db_client().stacks,
|
||||
doc! { "config.swarm_id": &swarm.id },
|
||||
None,
|
||||
),
|
||||
find_collect(
|
||||
&db_client().deployments,
|
||||
doc! { "config.swarm_id": &swarm.id },
|
||||
None,
|
||||
),
|
||||
find_collect(&db_client().builds, doc! {}, None,),
|
||||
);
|
||||
|
||||
let stacks = stacks.inspect_err(|e| error!("Failed to get stacks list from db (update swarm status cache) | swarm: {} | {e:#}", swarm.name)).unwrap_or_default();
|
||||
let deployments = deployments.inspect_err(|e| error!("Failed to get deployments list from db (update swarm status cache) | swarm : {} | {e:#}", swarm.name)).unwrap_or_default();
|
||||
let builds = builds.inspect_err(|e| error!("Failed to get builds list from db (update swarm status cache) | swarm : {} | {e:#}", swarm.name)).unwrap_or_default();
|
||||
|
||||
Self {
|
||||
stacks,
|
||||
deployments,
|
||||
builds,
|
||||
repos: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn load_server(server: &Server) -> Self {
|
||||
let (stacks, deployments, builds, repos) = tokio::join!(
|
||||
find_collect(
|
||||
&db_client().stacks,
|
||||
@@ -275,10 +303,10 @@ impl UpdateCacheResources {
|
||||
),
|
||||
);
|
||||
|
||||
let stacks = stacks.inspect_err(|e| error!("failed to get stacks list from db (update status cache) | server: {} | {e:#}", server.name)).unwrap_or_default();
|
||||
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 server status cache) | server: {} | {e:#}", server.name)).unwrap_or_default();
|
||||
let deployments = deployments.inspect_err(|e| error!("Failed to get deployments list from db (update server status cache) | server : {} | {e:#}", server.name)).unwrap_or_default();
|
||||
let builds = builds.inspect_err(|e| error!("Failed to get builds list from db (update server status cache) | server : {} | {e:#}", server.name)).unwrap_or_default();
|
||||
let repos = repos.inspect_err(|e| error!("Failed to get repos list from db (update server status cache) | server: {} | {e:#}", server.name)).unwrap_or_default();
|
||||
|
||||
Self {
|
||||
stacks,
|
||||
|
||||
@@ -22,6 +22,7 @@ use tokio::sync::Mutex;
|
||||
use crate::{
|
||||
config::monitoring_interval,
|
||||
helpers::swarm::swarm_request_custom_timeout,
|
||||
monitor::UpdateCacheResources,
|
||||
state::{CachedSwarmStatus, db_client, swarm_status_cache},
|
||||
};
|
||||
|
||||
@@ -91,7 +92,10 @@ pub async fn update_cache_for_swarm(swarm: &Swarm, force: bool) {
|
||||
|
||||
*lock = now;
|
||||
|
||||
let resources = UpdateCacheResources::load_swarm(swarm).await;
|
||||
|
||||
if swarm.config.server_ids.is_empty() {
|
||||
resources.insert_status_unknown().await;
|
||||
swarm_status_cache()
|
||||
.insert(
|
||||
swarm.id.clone(),
|
||||
@@ -119,6 +123,7 @@ pub async fn update_cache_for_swarm(swarm: &Swarm, force: bool) {
|
||||
{
|
||||
Ok(info) => info,
|
||||
Err(e) => {
|
||||
resources.insert_status_unknown().await;
|
||||
swarm_status_cache()
|
||||
.insert(
|
||||
swarm.id.clone(),
|
||||
@@ -143,6 +148,8 @@ pub async fn update_cache_for_swarm(swarm: &Swarm, force: bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: UPDATE STACKS / DEPLOYMENT CACHES
|
||||
|
||||
swarm_status_cache()
|
||||
.insert(
|
||||
swarm.id.clone(),
|
||||
|
||||
@@ -24,6 +24,7 @@ use komodo_client::{
|
||||
resource::Resource,
|
||||
server::Server,
|
||||
stack::Stack,
|
||||
swarm::Swarm,
|
||||
sync::ResourceSync,
|
||||
update::Update,
|
||||
user::User,
|
||||
@@ -753,6 +754,51 @@ async fn validate_config(
|
||||
.try_collect::<Vec<_>>()
|
||||
.await?;
|
||||
}
|
||||
Execution::RemoveSwarmNodes(params) => {
|
||||
let swarm = super::get_check_permissions::<Swarm>(
|
||||
¶ms.swarm,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
params.swarm = swarm.id;
|
||||
}
|
||||
Execution::RemoveSwarmStacks(params) => {
|
||||
let swarm = super::get_check_permissions::<Swarm>(
|
||||
¶ms.swarm,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
params.swarm = swarm.id;
|
||||
}
|
||||
Execution::RemoveSwarmServices(params) => {
|
||||
let swarm = super::get_check_permissions::<Swarm>(
|
||||
¶ms.swarm,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
params.swarm = swarm.id;
|
||||
}
|
||||
Execution::RemoveSwarmConfigs(params) => {
|
||||
let swarm = super::get_check_permissions::<Swarm>(
|
||||
¶ms.swarm,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
params.swarm = swarm.id;
|
||||
}
|
||||
Execution::RemoveSwarmSecrets(params) => {
|
||||
let swarm = super::get_check_permissions::<Swarm>(
|
||||
¶ms.swarm,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
params.swarm = swarm.id;
|
||||
}
|
||||
Execution::ClearRepoCache(_params) => {
|
||||
if !user.admin {
|
||||
return Err(anyhow!(
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use anyhow::anyhow;
|
||||
use komodo_client::{
|
||||
api::execute::*,
|
||||
entities::{
|
||||
SwarmOrServer,
|
||||
permission::PermissionLevel,
|
||||
stack::{Stack, StackActionState},
|
||||
update::{Log, Update},
|
||||
@@ -16,7 +18,7 @@ use crate::{
|
||||
state::action_states,
|
||||
};
|
||||
|
||||
use super::get_stack_and_server;
|
||||
use super::setup_stack_execution;
|
||||
|
||||
pub trait ExecuteCompose {
|
||||
type Extras;
|
||||
@@ -37,14 +39,19 @@ pub async fn execute_compose<T: ExecuteCompose>(
|
||||
mut update: Update,
|
||||
extras: T::Extras,
|
||||
) -> anyhow::Result<Update> {
|
||||
let (stack, server) = get_stack_and_server(
|
||||
let (stack, swarm_or_server) = setup_stack_execution(
|
||||
stack,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let SwarmOrServer::Server(server) = swarm_or_server else {
|
||||
return Err(anyhow!(
|
||||
"Compose executions (Start, Stop, Restart) should not be called for Stack in Swarm Mode"
|
||||
));
|
||||
};
|
||||
|
||||
// get the action state for the stack (or insert default).
|
||||
let action_state =
|
||||
action_states().stack.get_or_insert_default(&stack.id).await;
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
use anyhow::{Context, anyhow};
|
||||
use anyhow::Context;
|
||||
use komodo_client::entities::{
|
||||
permission::PermissionLevelAndSpecifics,
|
||||
server::{Server, ServerState},
|
||||
stack::Stack,
|
||||
user::User,
|
||||
SwarmOrServer, permission::PermissionLevelAndSpecifics,
|
||||
stack::Stack, user::User,
|
||||
};
|
||||
use regex::Regex;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_server_with_state,
|
||||
helpers::query::get_swarm_or_server,
|
||||
permission::get_check_permissions,
|
||||
};
|
||||
|
||||
@@ -16,28 +14,21 @@ pub mod execute;
|
||||
pub mod remote;
|
||||
pub mod services;
|
||||
|
||||
pub async fn get_stack_and_server(
|
||||
pub async fn setup_stack_execution(
|
||||
stack: &str,
|
||||
user: &User,
|
||||
permissions: PermissionLevelAndSpecifics,
|
||||
block_if_server_unreachable: bool,
|
||||
) -> anyhow::Result<(Stack, Server)> {
|
||||
) -> anyhow::Result<(Stack, SwarmOrServer)> {
|
||||
let stack =
|
||||
get_check_permissions::<Stack>(stack, user, permissions).await?;
|
||||
|
||||
if stack.config.server_id.is_empty() {
|
||||
return Err(anyhow!("Stack has no server configured"));
|
||||
}
|
||||
let swarm_or_server = get_swarm_or_server(
|
||||
&stack.config.swarm_id,
|
||||
&stack.config.server_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (server, state) =
|
||||
get_server_with_state(&stack.config.server_id).await?;
|
||||
if block_if_server_unreachable && state != ServerState::Ok {
|
||||
return Err(anyhow!(
|
||||
"Cannot send command when server is unreachable or disabled"
|
||||
));
|
||||
}
|
||||
|
||||
Ok((stack, server))
|
||||
Ok((stack, swarm_or_server))
|
||||
}
|
||||
|
||||
pub fn compose_container_match_regex(
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use std::str::FromStr;
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use colored::Colorize;
|
||||
@@ -304,7 +307,10 @@ async fn ensure_init_user_and_resources() {
|
||||
username: username.clone(),
|
||||
password: config.init_admin_password.clone(),
|
||||
})
|
||||
.resolve(&AuthArgs::default())
|
||||
.resolve(&AuthArgs {
|
||||
headers: Default::default(),
|
||||
ip: IpAddr::V4(Ipv4Addr::UNSPECIFIED),
|
||||
})
|
||||
.await
|
||||
{
|
||||
error!("Failed to create init admin user | {:#}", e.error);
|
||||
|
||||
@@ -700,6 +700,41 @@ impl ResourceSyncTrait for Procedure {
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
Execution::RemoveSwarmNodes(config) => {
|
||||
config.swarm = resources
|
||||
.swarms
|
||||
.get(&config.swarm)
|
||||
.map(|s| s.name.clone())
|
||||
.unwrap_or_default();
|
||||
}
|
||||
Execution::RemoveSwarmStacks(config) => {
|
||||
config.swarm = resources
|
||||
.swarms
|
||||
.get(&config.swarm)
|
||||
.map(|s| s.name.clone())
|
||||
.unwrap_or_default();
|
||||
}
|
||||
Execution::RemoveSwarmServices(config) => {
|
||||
config.swarm = resources
|
||||
.swarms
|
||||
.get(&config.swarm)
|
||||
.map(|s| s.name.clone())
|
||||
.unwrap_or_default();
|
||||
}
|
||||
Execution::RemoveSwarmConfigs(config) => {
|
||||
config.swarm = resources
|
||||
.swarms
|
||||
.get(&config.swarm)
|
||||
.map(|s| s.name.clone())
|
||||
.unwrap_or_default();
|
||||
}
|
||||
Execution::RemoveSwarmSecrets(config) => {
|
||||
config.swarm = resources
|
||||
.swarms
|
||||
.get(&config.swarm)
|
||||
.map(|s| s.name.clone())
|
||||
.unwrap_or_default();
|
||||
}
|
||||
Execution::ClearRepoCache(_) => {}
|
||||
Execution::BackupCoreDatabase(_) => {}
|
||||
Execution::GlobalAutoUpdate(_) => {}
|
||||
|
||||
@@ -843,6 +843,49 @@ impl ToToml for Procedure {
|
||||
)
|
||||
})
|
||||
}
|
||||
Execution::RemoveSwarmNodes(exec) => exec.swarm.clone_from(
|
||||
all
|
||||
.swarms
|
||||
.get(&exec.swarm)
|
||||
.map(|a| &a.name)
|
||||
.unwrap_or(&String::new()),
|
||||
),
|
||||
Execution::RemoveSwarmStacks(exec) => {
|
||||
exec.swarm.clone_from(
|
||||
all
|
||||
.swarms
|
||||
.get(&exec.swarm)
|
||||
.map(|a| &a.name)
|
||||
.unwrap_or(&String::new()),
|
||||
)
|
||||
}
|
||||
Execution::RemoveSwarmServices(exec) => {
|
||||
exec.swarm.clone_from(
|
||||
all
|
||||
.swarms
|
||||
.get(&exec.swarm)
|
||||
.map(|a| &a.name)
|
||||
.unwrap_or(&String::new()),
|
||||
)
|
||||
}
|
||||
Execution::RemoveSwarmConfigs(exec) => {
|
||||
exec.swarm.clone_from(
|
||||
all
|
||||
.swarms
|
||||
.get(&exec.swarm)
|
||||
.map(|a| &a.name)
|
||||
.unwrap_or(&String::new()),
|
||||
)
|
||||
}
|
||||
Execution::RemoveSwarmSecrets(exec) => {
|
||||
exec.swarm.clone_from(
|
||||
all
|
||||
.swarms
|
||||
.get(&exec.swarm)
|
||||
.map(|a| &a.name)
|
||||
.unwrap_or(&String::new()),
|
||||
)
|
||||
}
|
||||
Execution::None(_)
|
||||
| Execution::Sleep(_)
|
||||
| Execution::ClearRepoCache(_)
|
||||
|
||||
@@ -35,10 +35,10 @@ use crate::{
|
||||
mod helpers;
|
||||
use helpers::*;
|
||||
|
||||
impl Resolve<super::Args> for GetDockerfileContentsOnHost {
|
||||
impl Resolve<crate::api::Args> for GetDockerfileContentsOnHost {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &super::Args,
|
||||
_: &crate::api::Args,
|
||||
) -> anyhow::Result<GetDockerfileContentsOnHostResponse> {
|
||||
let GetDockerfileContentsOnHost {
|
||||
name,
|
||||
@@ -75,7 +75,7 @@ impl Resolve<super::Args> for GetDockerfileContentsOnHost {
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<super::Args> for WriteDockerfileContentsToHost {
|
||||
impl Resolve<crate::api::Args> for WriteDockerfileContentsToHost {
|
||||
#[instrument(
|
||||
"WriteDockerfileContentsToHost",
|
||||
skip_all,
|
||||
@@ -87,7 +87,10 @@ impl Resolve<super::Args> for WriteDockerfileContentsToHost {
|
||||
dockerfile_path = &self.dockerfile_path,
|
||||
)
|
||||
)]
|
||||
async fn resolve(self, args: &super::Args) -> anyhow::Result<Log> {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
let WriteDockerfileContentsToHost {
|
||||
name,
|
||||
build_path,
|
||||
@@ -123,7 +126,7 @@ impl Resolve<super::Args> for WriteDockerfileContentsToHost {
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<super::Args> for build::Build {
|
||||
impl Resolve<crate::api::Args> for build::Build {
|
||||
#[instrument(
|
||||
"Build",
|
||||
skip_all,
|
||||
@@ -136,7 +139,7 @@ impl Resolve<super::Args> for build::Build {
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &super::Args,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Vec<Log>> {
|
||||
let build::Build {
|
||||
mut build,
|
||||
@@ -339,7 +342,7 @@ impl Resolve<super::Args> for build::Build {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for PruneBuilders {
|
||||
impl Resolve<crate::api::Args> for PruneBuilders {
|
||||
#[instrument(
|
||||
"PruneBuilders",
|
||||
skip_all,
|
||||
@@ -348,7 +351,10 @@ impl Resolve<super::Args> for PruneBuilders {
|
||||
core = args.core,
|
||||
)
|
||||
)]
|
||||
async fn resolve(self, args: &super::Args) -> anyhow::Result<Log> {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
let command = String::from("docker builder prune -a -f");
|
||||
Ok(
|
||||
run_komodo_standard_command("Prune Builders", None, command)
|
||||
@@ -359,7 +365,7 @@ impl Resolve<super::Args> for PruneBuilders {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for PruneBuildx {
|
||||
impl Resolve<crate::api::Args> for PruneBuildx {
|
||||
#[instrument(
|
||||
"PruneBuildx",
|
||||
skip_all,
|
||||
@@ -368,7 +374,10 @@ impl Resolve<super::Args> for PruneBuildx {
|
||||
core = args.core,
|
||||
)
|
||||
)]
|
||||
async fn resolve(self, args: &super::Args) -> anyhow::Result<Log> {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
let command = String::from("docker buildx prune -a -f");
|
||||
Ok(
|
||||
run_komodo_standard_command("Prune Buildx", None, command)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{borrow::Cow, path::PathBuf};
|
||||
use std::{borrow::Cow, fmt::Write, path::PathBuf};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use command::{
|
||||
@@ -12,15 +12,16 @@ use komodo_client::{
|
||||
entities::{
|
||||
FileContents, RepoExecutionResponse, all_logs_success,
|
||||
stack::{
|
||||
ComposeFile, ComposeService, ComposeServiceDeploy,
|
||||
StackRemoteFileContents, StackServiceNames,
|
||||
AdditionalEnvFile, ComposeFile, ComposeService,
|
||||
ComposeServiceDeploy, StackRemoteFileContents,
|
||||
StackServiceNames,
|
||||
},
|
||||
to_path_compatible_name,
|
||||
update::Log,
|
||||
},
|
||||
parsers::parse_multiline_command,
|
||||
};
|
||||
use periphery_client::api::compose::*;
|
||||
use periphery_client::api::{DeployStackResponse, compose::*};
|
||||
use resolver_api::Resolve;
|
||||
use shell_escape::unix::escape;
|
||||
use tracing::Instrument;
|
||||
@@ -29,15 +30,17 @@ use crate::{
|
||||
config::periphery_config,
|
||||
docker::compose::docker_compose,
|
||||
helpers::{format_extra_args, format_log_grep},
|
||||
stack::{
|
||||
maybe_login_registry, pull_or_clone_stack, validate_files,
|
||||
write::write_stack,
|
||||
},
|
||||
};
|
||||
|
||||
mod helpers;
|
||||
mod write;
|
||||
|
||||
use helpers::*;
|
||||
|
||||
impl Resolve<super::Args> for GetComposeLog {
|
||||
async fn resolve(self, _: &super::Args) -> anyhow::Result<Log> {
|
||||
impl Resolve<crate::api::Args> for GetComposeLog {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
let GetComposeLog {
|
||||
project,
|
||||
services,
|
||||
@@ -61,8 +64,11 @@ impl Resolve<super::Args> for GetComposeLog {
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<super::Args> for GetComposeLogSearch {
|
||||
async fn resolve(self, _: &super::Args) -> anyhow::Result<Log> {
|
||||
impl Resolve<crate::api::Args> for GetComposeLogSearch {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
let GetComposeLogSearch {
|
||||
project,
|
||||
services,
|
||||
@@ -91,10 +97,10 @@ impl Resolve<super::Args> for GetComposeLogSearch {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for GetComposeContentsOnHost {
|
||||
impl Resolve<crate::api::Args> for GetComposeContentsOnHost {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &super::Args,
|
||||
_: &crate::api::Args,
|
||||
) -> anyhow::Result<GetComposeContentsOnHostResponse> {
|
||||
let GetComposeContentsOnHost {
|
||||
name,
|
||||
@@ -146,7 +152,7 @@ impl Resolve<super::Args> for GetComposeContentsOnHost {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for WriteComposeContentsToHost {
|
||||
impl Resolve<crate::api::Args> for WriteComposeContentsToHost {
|
||||
#[instrument(
|
||||
"WriteComposeContentsToHost",
|
||||
skip_all,
|
||||
@@ -158,7 +164,10 @@ impl Resolve<super::Args> for WriteComposeContentsToHost {
|
||||
file_path = self.file_path,
|
||||
)
|
||||
)]
|
||||
async fn resolve(self, args: &super::Args) -> anyhow::Result<Log> {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
let WriteComposeContentsToHost {
|
||||
name,
|
||||
run_directory,
|
||||
@@ -188,7 +197,7 @@ impl Resolve<super::Args> for WriteComposeContentsToHost {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for WriteCommitComposeContents {
|
||||
impl Resolve<crate::api::Args> for WriteCommitComposeContents {
|
||||
#[instrument(
|
||||
"WriteCommitComposeContents",
|
||||
skip_all,
|
||||
@@ -202,7 +211,7 @@ impl Resolve<super::Args> for WriteCommitComposeContents {
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &super::Args,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<RepoExecutionResponse> {
|
||||
let WriteCommitComposeContents {
|
||||
stack,
|
||||
@@ -243,7 +252,7 @@ impl Resolve<super::Args> for WriteCommitComposeContents {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for ComposePull {
|
||||
impl Resolve<crate::api::Args> for ComposePull {
|
||||
#[instrument(
|
||||
"ComposePull",
|
||||
skip_all,
|
||||
@@ -256,7 +265,7 @@ impl Resolve<super::Args> for ComposePull {
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &super::Args,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<ComposePullResponse> {
|
||||
let ComposePull {
|
||||
mut stack,
|
||||
@@ -278,7 +287,7 @@ impl Resolve<super::Args> for ComposePull {
|
||||
.push_logs(&mut res.logs);
|
||||
replacers.extend(interpolator.secret_replacers);
|
||||
|
||||
let (run_directory, env_file_path) = match write::stack(
|
||||
let (run_directory, env_file_path) = match write_stack(
|
||||
&stack,
|
||||
repo.as_ref(),
|
||||
git_token,
|
||||
@@ -362,7 +371,7 @@ impl Resolve<super::Args> for ComposePull {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for ComposeUp {
|
||||
impl Resolve<crate::api::Args> for ComposeUp {
|
||||
#[instrument(
|
||||
"ComposeUp",
|
||||
skip_all,
|
||||
@@ -376,8 +385,8 @@ impl Resolve<super::Args> for ComposeUp {
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &super::Args,
|
||||
) -> anyhow::Result<ComposeUpResponse> {
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<DeployStackResponse> {
|
||||
let ComposeUp {
|
||||
mut stack,
|
||||
repo,
|
||||
@@ -387,7 +396,7 @@ impl Resolve<super::Args> for ComposeUp {
|
||||
mut replacers,
|
||||
} = self;
|
||||
|
||||
let mut res = ComposeUpResponse::default();
|
||||
let mut res = DeployStackResponse::default();
|
||||
|
||||
let mut interpolator =
|
||||
Interpolator::new(None, &periphery_config().secrets);
|
||||
@@ -398,7 +407,7 @@ impl Resolve<super::Args> for ComposeUp {
|
||||
.push_logs(&mut res.logs);
|
||||
replacers.extend(interpolator.secret_replacers);
|
||||
|
||||
let (run_directory, env_file_path) = match write::stack(
|
||||
let (run_directory, env_file_path) = match write_stack(
|
||||
&stack,
|
||||
repo.as_ref(),
|
||||
git_token,
|
||||
@@ -436,7 +445,7 @@ impl Resolve<super::Args> for ComposeUp {
|
||||
if !stack.config.pre_deploy.is_none() {
|
||||
let pre_deploy_path =
|
||||
run_directory.join(&stack.config.pre_deploy.path);
|
||||
let span = info_span!("RunPreDeploy");
|
||||
let span = info_span!("ExecutePreDeploy");
|
||||
if let Some(log) = run_komodo_command_with_sanitization(
|
||||
"Pre Deploy",
|
||||
pre_deploy_path.as_path(),
|
||||
@@ -502,8 +511,8 @@ impl Resolve<super::Args> for ComposeUp {
|
||||
let compose =
|
||||
serde_yaml_ng::from_str::<ComposeFile>(&config_log.stdout)
|
||||
.context("Failed to parse compose contents")?;
|
||||
// Record sanitized compose config output
|
||||
res.compose_config = Some(config_log.stdout);
|
||||
// Store sanitized compose config output
|
||||
res.merged_config = Some(config_log.stdout);
|
||||
for (
|
||||
service_name,
|
||||
ComposeService {
|
||||
@@ -547,7 +556,7 @@ impl Resolve<super::Args> for ComposeUp {
|
||||
let command = format!(
|
||||
"{docker_compose} -p {project_name} -f {file_args}{env_file_args} build{build_extra_args}{service_args}",
|
||||
);
|
||||
let span = info_span!("RunComposeBuild");
|
||||
let span = info_span!("ExecuteComposeBuild");
|
||||
let Some(log) = run_komodo_command_with_sanitization(
|
||||
"Compose Build",
|
||||
run_directory.as_path(),
|
||||
@@ -591,11 +600,11 @@ impl Resolve<super::Args> for ComposeUp {
|
||||
// Also check if project name changed, which also requires taking down.
|
||||
|| last_project_name != project_name
|
||||
{
|
||||
// Take down the existing containers.
|
||||
// Take down the existing compose stack.
|
||||
// This one tries to use the previously deployed service name, to ensure the right stack is taken down.
|
||||
helpers::compose_down(&last_project_name, &services, &mut res)
|
||||
compose_down(&last_project_name, &services, &mut res)
|
||||
.await
|
||||
.context("failed to destroy existing containers")?;
|
||||
.context("Failed to take down existing compose stack")?;
|
||||
}
|
||||
|
||||
// Run compose up
|
||||
@@ -619,7 +628,7 @@ impl Resolve<super::Args> for ComposeUp {
|
||||
compose_cmd_wrapper.replace("[[COMPOSE_COMMAND]]", &command);
|
||||
}
|
||||
|
||||
let span = info_span!("RunComposeUp");
|
||||
let span = info_span!("ExecuteComposeUp");
|
||||
let Some(log) = run_komodo_command_with_sanitization(
|
||||
"Compose Up",
|
||||
run_directory.as_path(),
|
||||
@@ -639,7 +648,7 @@ impl Resolve<super::Args> for ComposeUp {
|
||||
if res.deployed && !stack.config.post_deploy.is_none() {
|
||||
let post_deploy_path =
|
||||
run_directory.join(&stack.config.post_deploy.path);
|
||||
let span = info_span!("RunPostDeploy");
|
||||
let span = info_span!("ExecutePostDeploy");
|
||||
if let Some(log) = run_komodo_command_with_sanitization(
|
||||
"Post Deploy",
|
||||
post_deploy_path.as_path(),
|
||||
@@ -660,7 +669,7 @@ impl Resolve<super::Args> for ComposeUp {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for ComposeExecution {
|
||||
impl Resolve<crate::api::Args> for ComposeExecution {
|
||||
#[instrument(
|
||||
"ComposeExecution",
|
||||
skip_all,
|
||||
@@ -671,7 +680,10 @@ impl Resolve<super::Args> for ComposeExecution {
|
||||
command = self.command,
|
||||
)
|
||||
)]
|
||||
async fn resolve(self, args: &super::Args) -> anyhow::Result<Log> {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
let ComposeExecution { project, command } = self;
|
||||
let docker_compose = docker_compose();
|
||||
let log = run_komodo_standard_command(
|
||||
@@ -686,7 +698,7 @@ impl Resolve<super::Args> for ComposeExecution {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for ComposeRun {
|
||||
impl Resolve<crate::api::Args> for ComposeRun {
|
||||
#[instrument(
|
||||
"ComposeRun",
|
||||
skip_all,
|
||||
@@ -698,7 +710,10 @@ impl Resolve<super::Args> for ComposeRun {
|
||||
service = &self.service
|
||||
)
|
||||
)]
|
||||
async fn resolve(self, args: &super::Args) -> anyhow::Result<Log> {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
let ComposeRun {
|
||||
mut stack,
|
||||
repo,
|
||||
@@ -726,7 +741,7 @@ impl Resolve<super::Args> for ComposeRun {
|
||||
replacers.extend(interpolator.secret_replacers);
|
||||
|
||||
let mut res = ComposeRunResponse::default();
|
||||
let (run_directory, env_file_path) = match write::stack(
|
||||
let (run_directory, env_file_path) = match write_stack(
|
||||
&stack,
|
||||
repo.as_ref(),
|
||||
git_token,
|
||||
@@ -843,3 +858,59 @@ impl Resolve<super::Args> for ComposeRun {
|
||||
Ok(log)
|
||||
}
|
||||
}
|
||||
|
||||
fn env_file_args(
|
||||
env_file_path: Option<&str>,
|
||||
additional_env_files: &[AdditionalEnvFile],
|
||||
) -> anyhow::Result<String> {
|
||||
let mut res = String::new();
|
||||
|
||||
// Add additional env files (except komodo's own, which comes last)
|
||||
for file in additional_env_files
|
||||
.iter()
|
||||
.filter(|f| env_file_path != Some(f.path.as_str()))
|
||||
{
|
||||
let path = &file.path;
|
||||
write!(res, " --env-file {path}").with_context(|| {
|
||||
format!("Failed to write --env-file arg for {path}")
|
||||
})?;
|
||||
}
|
||||
|
||||
// Add komodo's env file last for highest priority
|
||||
if let Some(file) = env_file_path {
|
||||
write!(res, " --env-file {file}").with_context(|| {
|
||||
format!("Failed to write --env-file arg for {file}")
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[instrument("ComposeDown", skip(res))]
|
||||
async fn compose_down(
|
||||
project: &str,
|
||||
services: &[String],
|
||||
res: &mut DeployStackResponse,
|
||||
) -> anyhow::Result<()> {
|
||||
let docker_compose = docker_compose();
|
||||
let service_args = if services.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" {}", services.join(" "))
|
||||
};
|
||||
let log = run_komodo_standard_command(
|
||||
"Compose Down",
|
||||
None,
|
||||
format!("{docker_compose} -p {project} down{service_args}"),
|
||||
)
|
||||
.await;
|
||||
let success = log.success;
|
||||
res.logs.push(log);
|
||||
if !success {
|
||||
return Err(anyhow!(
|
||||
"Failed to bring down existing container(s) with docker compose down. Stopping run."
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -19,16 +19,18 @@ use crate::{
|
||||
state::docker_client,
|
||||
};
|
||||
|
||||
mod run;
|
||||
|
||||
// ======
|
||||
// READ
|
||||
// ======
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for InspectContainer {
|
||||
impl Resolve<crate::api::Args> for InspectContainer {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &super::Args,
|
||||
_: &crate::api::Args,
|
||||
) -> anyhow::Result<Container> {
|
||||
let client = docker_client().load();
|
||||
let client = client
|
||||
@@ -41,8 +43,11 @@ impl Resolve<super::Args> for InspectContainer {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for GetContainerLog {
|
||||
async fn resolve(self, _: &super::Args) -> anyhow::Result<Log> {
|
||||
impl Resolve<crate::api::Args> for GetContainerLog {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
let GetContainerLog {
|
||||
name,
|
||||
tail,
|
||||
@@ -64,8 +69,11 @@ impl Resolve<super::Args> for GetContainerLog {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for GetContainerLogSearch {
|
||||
async fn resolve(self, _: &super::Args) -> anyhow::Result<Log> {
|
||||
impl Resolve<crate::api::Args> for GetContainerLogSearch {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
let GetContainerLogSearch {
|
||||
name,
|
||||
terms,
|
||||
@@ -95,10 +103,10 @@ impl Resolve<super::Args> for GetContainerLogSearch {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for GetContainerStats {
|
||||
impl Resolve<crate::api::Args> for GetContainerStats {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &super::Args,
|
||||
_: &crate::api::Args,
|
||||
) -> anyhow::Result<ContainerStats> {
|
||||
let mut stats = get_container_stats(Some(self.name)).await?;
|
||||
let stats =
|
||||
@@ -109,10 +117,10 @@ impl Resolve<super::Args> for GetContainerStats {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for GetFullContainerStats {
|
||||
impl Resolve<crate::api::Args> for GetFullContainerStats {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &super::Args,
|
||||
_: &crate::api::Args,
|
||||
) -> anyhow::Result<FullContainerStats> {
|
||||
let client = docker_client().load();
|
||||
let client = client
|
||||
@@ -125,10 +133,10 @@ impl Resolve<super::Args> for GetFullContainerStats {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for GetContainerStatsList {
|
||||
impl Resolve<crate::api::Args> for GetContainerStatsList {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &super::Args,
|
||||
_: &crate::api::Args,
|
||||
) -> anyhow::Result<Vec<ContainerStats>> {
|
||||
get_container_stats(None).await
|
||||
}
|
||||
@@ -138,7 +146,7 @@ impl Resolve<super::Args> for GetContainerStatsList {
|
||||
// ACTIONS
|
||||
// =========
|
||||
|
||||
impl Resolve<super::Args> for StartContainer {
|
||||
impl Resolve<crate::api::Args> for StartContainer {
|
||||
#[instrument(
|
||||
"StartContainer",
|
||||
skip_all,
|
||||
@@ -148,7 +156,10 @@ impl Resolve<super::Args> for StartContainer {
|
||||
container = self.name,
|
||||
)
|
||||
)]
|
||||
async fn resolve(self, args: &super::Args) -> anyhow::Result<Log> {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
Ok(
|
||||
run_komodo_standard_command(
|
||||
"Docker Start",
|
||||
@@ -162,7 +173,7 @@ impl Resolve<super::Args> for StartContainer {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for RestartContainer {
|
||||
impl Resolve<crate::api::Args> for RestartContainer {
|
||||
#[instrument(
|
||||
"RestartContainer",
|
||||
skip_all,
|
||||
@@ -172,7 +183,10 @@ impl Resolve<super::Args> for RestartContainer {
|
||||
container = self.name,
|
||||
)
|
||||
)]
|
||||
async fn resolve(self, args: &super::Args) -> anyhow::Result<Log> {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
Ok(
|
||||
run_komodo_standard_command(
|
||||
"Docker Restart",
|
||||
@@ -186,7 +200,7 @@ impl Resolve<super::Args> for RestartContainer {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for PauseContainer {
|
||||
impl Resolve<crate::api::Args> for PauseContainer {
|
||||
#[instrument(
|
||||
"PauseContainer",
|
||||
skip_all,
|
||||
@@ -196,7 +210,10 @@ impl Resolve<super::Args> for PauseContainer {
|
||||
container = self.name,
|
||||
)
|
||||
)]
|
||||
async fn resolve(self, args: &super::Args) -> anyhow::Result<Log> {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
Ok(
|
||||
run_komodo_standard_command(
|
||||
"Docker Pause",
|
||||
@@ -208,7 +225,7 @@ impl Resolve<super::Args> for PauseContainer {
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<super::Args> for UnpauseContainer {
|
||||
impl Resolve<crate::api::Args> for UnpauseContainer {
|
||||
#[instrument(
|
||||
"UnpauseContainer",
|
||||
skip_all,
|
||||
@@ -218,7 +235,10 @@ impl Resolve<super::Args> for UnpauseContainer {
|
||||
container = self.name,
|
||||
)
|
||||
)]
|
||||
async fn resolve(self, args: &super::Args) -> anyhow::Result<Log> {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
Ok(
|
||||
run_komodo_standard_command(
|
||||
"Docker Unpause",
|
||||
@@ -232,7 +252,7 @@ impl Resolve<super::Args> for UnpauseContainer {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for StopContainer {
|
||||
impl Resolve<crate::api::Args> for StopContainer {
|
||||
#[instrument(
|
||||
"StopContainer",
|
||||
skip_all,
|
||||
@@ -242,7 +262,10 @@ impl Resolve<super::Args> for StopContainer {
|
||||
container = self.name,
|
||||
)
|
||||
)]
|
||||
async fn resolve(self, args: &super::Args) -> anyhow::Result<Log> {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
let StopContainer { name, signal, time } = self;
|
||||
let command = stop_container_command(&name, signal, time);
|
||||
let log =
|
||||
@@ -269,7 +292,7 @@ impl Resolve<super::Args> for StopContainer {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for RemoveContainer {
|
||||
impl Resolve<crate::api::Args> for RemoveContainer {
|
||||
#[instrument(
|
||||
"RemoveContainer",
|
||||
skip_all,
|
||||
@@ -279,7 +302,10 @@ impl Resolve<super::Args> for RemoveContainer {
|
||||
container = self.name,
|
||||
)
|
||||
)]
|
||||
async fn resolve(self, args: &super::Args) -> anyhow::Result<Log> {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
let RemoveContainer { name, signal, time } = self;
|
||||
let stop_command = stop_container_command(&name, signal, time);
|
||||
let command =
|
||||
@@ -317,7 +343,7 @@ impl Resolve<super::Args> for RemoveContainer {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for RenameContainer {
|
||||
impl Resolve<crate::api::Args> for RenameContainer {
|
||||
#[instrument(
|
||||
"RenameContainer",
|
||||
skip_all,
|
||||
@@ -328,7 +354,10 @@ impl Resolve<super::Args> for RenameContainer {
|
||||
new = self.new_name,
|
||||
)
|
||||
)]
|
||||
async fn resolve(self, args: &super::Args) -> anyhow::Result<Log> {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
let RenameContainer {
|
||||
curr_name,
|
||||
new_name,
|
||||
@@ -343,7 +372,7 @@ impl Resolve<super::Args> for RenameContainer {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for PruneContainers {
|
||||
impl Resolve<crate::api::Args> for PruneContainers {
|
||||
#[instrument(
|
||||
"PruneContainers",
|
||||
skip_all,
|
||||
@@ -352,7 +381,10 @@ impl Resolve<super::Args> for PruneContainers {
|
||||
core = args.core
|
||||
)
|
||||
)]
|
||||
async fn resolve(self, args: &super::Args) -> anyhow::Result<Log> {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
let command = String::from("docker container prune -f");
|
||||
Ok(
|
||||
run_komodo_standard_command("Prune Containers", None, command)
|
||||
@@ -363,7 +395,7 @@ impl Resolve<super::Args> for PruneContainers {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for StartAllContainers {
|
||||
impl Resolve<crate::api::Args> for StartAllContainers {
|
||||
#[instrument(
|
||||
"StartAllContainers",
|
||||
skip_all,
|
||||
@@ -374,7 +406,7 @@ impl Resolve<super::Args> for StartAllContainers {
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &super::Args,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Vec<Log>> {
|
||||
let client = docker_client().load();
|
||||
let client = client
|
||||
@@ -405,7 +437,7 @@ impl Resolve<super::Args> for StartAllContainers {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for RestartAllContainers {
|
||||
impl Resolve<crate::api::Args> for RestartAllContainers {
|
||||
#[instrument(
|
||||
"RestartAllContainers",
|
||||
skip_all,
|
||||
@@ -416,7 +448,7 @@ impl Resolve<super::Args> for RestartAllContainers {
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &super::Args,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Vec<Log>> {
|
||||
let client = docker_client().load();
|
||||
let client = client
|
||||
@@ -447,7 +479,7 @@ impl Resolve<super::Args> for RestartAllContainers {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for PauseAllContainers {
|
||||
impl Resolve<crate::api::Args> for PauseAllContainers {
|
||||
#[instrument(
|
||||
"PauseAllContainers",
|
||||
skip_all,
|
||||
@@ -458,7 +490,7 @@ impl Resolve<super::Args> for PauseAllContainers {
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &super::Args,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Vec<Log>> {
|
||||
let client = docker_client().load();
|
||||
let client = client
|
||||
@@ -489,7 +521,7 @@ impl Resolve<super::Args> for PauseAllContainers {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for UnpauseAllContainers {
|
||||
impl Resolve<crate::api::Args> for UnpauseAllContainers {
|
||||
#[instrument(
|
||||
"UnpauseAllContainers",
|
||||
skip_all,
|
||||
@@ -500,7 +532,7 @@ impl Resolve<super::Args> for UnpauseAllContainers {
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &super::Args,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Vec<Log>> {
|
||||
let client = docker_client().load();
|
||||
let client = client
|
||||
@@ -531,7 +563,7 @@ impl Resolve<super::Args> for UnpauseAllContainers {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for StopAllContainers {
|
||||
impl Resolve<crate::api::Args> for StopAllContainers {
|
||||
#[instrument(
|
||||
"StopAllContainers",
|
||||
skip_all,
|
||||
@@ -542,7 +574,7 @@ impl Resolve<super::Args> for StopAllContainers {
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &super::Args,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Vec<Log>> {
|
||||
let client = docker_client().load();
|
||||
let client = client
|
||||
@@ -1,34 +1,36 @@
|
||||
use std::fmt::Write;
|
||||
|
||||
use anyhow::Context;
|
||||
use command::{
|
||||
KomodoCommandMode, run_komodo_command_with_sanitization,
|
||||
};
|
||||
use formatting::format_serror;
|
||||
use interpolate::Interpolator;
|
||||
use komodo_client::{
|
||||
entities::{
|
||||
EnvironmentVar,
|
||||
deployment::{
|
||||
Conversion, Deployment, DeploymentConfig, DeploymentImage,
|
||||
RestartMode, conversions_from_str, extract_registry_domain,
|
||||
},
|
||||
environment_vars_from_str,
|
||||
update::Log,
|
||||
use komodo_client::entities::{
|
||||
deployment::{
|
||||
Deployment, DeploymentConfig, DeploymentImage, RestartMode,
|
||||
conversions_from_str, extract_registry_domain,
|
||||
},
|
||||
parsers::QUOTE_PATTERN,
|
||||
environment_vars_from_str,
|
||||
update::Log,
|
||||
};
|
||||
use periphery_client::api::container::{
|
||||
RemoveContainer, RunContainer,
|
||||
};
|
||||
use periphery_client::api::container::{Deploy, RemoveContainer};
|
||||
use resolver_api::Resolve;
|
||||
use tracing::Instrument;
|
||||
|
||||
use crate::{
|
||||
config::periphery_config,
|
||||
docker::{docker_login, pull_image},
|
||||
helpers::{format_extra_args, format_labels},
|
||||
helpers::{
|
||||
push_conversions, push_environment, push_extra_args, push_labels,
|
||||
},
|
||||
};
|
||||
|
||||
impl Resolve<super::Args> for Deploy {
|
||||
impl Resolve<crate::api::Args> for RunContainer {
|
||||
#[instrument(
|
||||
"Deploy",
|
||||
"DeployContainer",
|
||||
skip_all,
|
||||
fields(
|
||||
id = args.id.to_string(),
|
||||
@@ -38,8 +40,11 @@ impl Resolve<super::Args> for Deploy {
|
||||
stop_time = self.stop_time,
|
||||
)
|
||||
)]
|
||||
async fn resolve(self, args: &super::Args) -> anyhow::Result<Log> {
|
||||
let Deploy {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
let RunContainer {
|
||||
mut deployment,
|
||||
stop_signal,
|
||||
stop_time,
|
||||
@@ -57,15 +62,15 @@ impl Resolve<super::Args> for Deploy {
|
||||
{
|
||||
if image.is_empty() {
|
||||
return Ok(Log::error(
|
||||
"get image",
|
||||
String::from("deployment does not have image attached"),
|
||||
"Get Image",
|
||||
String::from("Deployment does not have image attached"),
|
||||
));
|
||||
}
|
||||
image
|
||||
} else {
|
||||
return Ok(Log::error(
|
||||
"get image",
|
||||
String::from("deployment does not have image attached"),
|
||||
"Get Image",
|
||||
String::from("Deployment does not have image attached"),
|
||||
));
|
||||
};
|
||||
|
||||
@@ -77,9 +82,9 @@ impl Resolve<super::Args> for Deploy {
|
||||
.await
|
||||
{
|
||||
return Ok(Log::error(
|
||||
"docker login",
|
||||
"Docker Login",
|
||||
format_serror(
|
||||
&e.context("failed to login to docker registry").into(),
|
||||
&e.context("Failed to login to docker registry").into(),
|
||||
),
|
||||
));
|
||||
}
|
||||
@@ -99,7 +104,7 @@ impl Resolve<super::Args> for Deploy {
|
||||
let command = docker_run_command(&deployment, image)
|
||||
.context("Unable to generate valid docker run command")?;
|
||||
|
||||
let span = info_span!("RunDockerRun");
|
||||
let span = info_span!("ExecuteDockerRun");
|
||||
let Some(log) = run_komodo_command_with_sanitization(
|
||||
"Docker Run",
|
||||
None,
|
||||
@@ -138,75 +143,53 @@ fn docker_run_command(
|
||||
}: &Deployment,
|
||||
image: &str,
|
||||
) -> anyhow::Result<String> {
|
||||
let ports = parse_conversions(
|
||||
let mut res =
|
||||
format!("docker run -d --name {name} --network {network}");
|
||||
|
||||
push_conversions(
|
||||
&mut res,
|
||||
&conversions_from_str(ports).context("Invalid ports")?,
|
||||
"-p",
|
||||
);
|
||||
let volumes = parse_conversions(
|
||||
)?;
|
||||
|
||||
push_conversions(
|
||||
&mut res,
|
||||
&conversions_from_str(volumes).context("Invalid volumes")?,
|
||||
"-v",
|
||||
);
|
||||
let network = parse_network(network);
|
||||
let restart = parse_restart(restart);
|
||||
let environment = parse_environment(
|
||||
)?;
|
||||
|
||||
push_environment(
|
||||
&mut res,
|
||||
&environment_vars_from_str(environment)
|
||||
.context("Invalid environment")?,
|
||||
);
|
||||
let labels = format_labels(
|
||||
)?;
|
||||
|
||||
push_restart(&mut res, restart)?;
|
||||
|
||||
push_labels(
|
||||
&mut res,
|
||||
&environment_vars_from_str(labels).context("Invalid labels")?,
|
||||
);
|
||||
let command = parse_command(command);
|
||||
let extra_args = format_extra_args(extra_args);
|
||||
let command = format!(
|
||||
"docker run -d --name {name}{ports}{volumes}{network}{restart}{environment}{labels}{extra_args} {image}{command}"
|
||||
);
|
||||
Ok(command)
|
||||
}
|
||||
)?;
|
||||
|
||||
fn parse_conversions(
|
||||
conversions: &[Conversion],
|
||||
flag: &str,
|
||||
) -> String {
|
||||
conversions
|
||||
.iter()
|
||||
.map(|p| format!(" {flag} {}:{}", p.local, p.container))
|
||||
.collect::<Vec<_>>()
|
||||
.join("")
|
||||
}
|
||||
push_extra_args(&mut res, extra_args)?;
|
||||
|
||||
fn parse_environment(environment: &[EnvironmentVar]) -> String {
|
||||
environment
|
||||
.iter()
|
||||
.map(|p| {
|
||||
if p.value.starts_with(QUOTE_PATTERN)
|
||||
&& p.value.ends_with(QUOTE_PATTERN)
|
||||
{
|
||||
// If the value already wrapped in quotes, don't wrap it again
|
||||
format!(" --env {}={}", p.variable, p.value)
|
||||
} else {
|
||||
format!(" --env {}=\"{}\"", p.variable, p.value)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("")
|
||||
}
|
||||
write!(&mut res, " {image}")?;
|
||||
|
||||
fn parse_network(network: &str) -> String {
|
||||
format!(" --network {network}")
|
||||
}
|
||||
|
||||
fn parse_restart(restart: &RestartMode) -> String {
|
||||
let restart = match restart {
|
||||
RestartMode::OnFailure => "on-failure:10".to_string(),
|
||||
_ => restart.to_string(),
|
||||
};
|
||||
format!(" --restart {restart}")
|
||||
}
|
||||
|
||||
fn parse_command(command: &str) -> String {
|
||||
if command.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" {command}")
|
||||
if !command.is_empty() {
|
||||
write!(&mut res, " {command}")?;
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn push_restart(
|
||||
command: &mut String,
|
||||
restart: &RestartMode,
|
||||
) -> anyhow::Result<()> {
|
||||
let restart = match restart {
|
||||
RestartMode::OnFailure => "on-failure:10",
|
||||
_ => restart.as_ref(),
|
||||
};
|
||||
write!(command, " --restart {restart}")
|
||||
.context("Failed to write restart mode")
|
||||
}
|
||||
@@ -22,8 +22,11 @@ use crate::{docker::docker_login, state::docker_client};
|
||||
// IMAGE
|
||||
// =====
|
||||
|
||||
impl Resolve<super::Args> for InspectImage {
|
||||
async fn resolve(self, _: &super::Args) -> anyhow::Result<Image> {
|
||||
impl Resolve<crate::api::Args> for InspectImage {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &crate::api::Args,
|
||||
) -> anyhow::Result<Image> {
|
||||
let client = docker_client().load();
|
||||
let client = client
|
||||
.iter()
|
||||
@@ -35,10 +38,10 @@ impl Resolve<super::Args> for InspectImage {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for ImageHistory {
|
||||
impl Resolve<crate::api::Args> for ImageHistory {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &super::Args,
|
||||
_: &crate::api::Args,
|
||||
) -> anyhow::Result<Vec<ImageHistoryResponseItem>> {
|
||||
let client = docker_client().load();
|
||||
let client = client
|
||||
@@ -60,7 +63,7 @@ fn pull_cache() -> &'static TimeoutCache<String, Log> {
|
||||
PULL_CACHE.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
impl Resolve<super::Args> for PullImage {
|
||||
impl Resolve<crate::api::Args> for PullImage {
|
||||
#[instrument(
|
||||
"PullImage",
|
||||
skip_all,
|
||||
@@ -71,7 +74,10 @@ impl Resolve<super::Args> for PullImage {
|
||||
account = self.account,
|
||||
)
|
||||
)]
|
||||
async fn resolve(self, args: &super::Args) -> anyhow::Result<Log> {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
let PullImage {
|
||||
name,
|
||||
account,
|
||||
@@ -118,7 +124,7 @@ impl Resolve<super::Args> for PullImage {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for DeleteImage {
|
||||
impl Resolve<crate::api::Args> for DeleteImage {
|
||||
#[instrument(
|
||||
"DeleteImage",
|
||||
skip_all,
|
||||
@@ -128,7 +134,10 @@ impl Resolve<super::Args> for DeleteImage {
|
||||
image = self.name,
|
||||
)
|
||||
)]
|
||||
async fn resolve(self, args: &super::Args) -> anyhow::Result<Log> {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
let command = format!("docker image rm {}", self.name);
|
||||
Ok(
|
||||
run_komodo_standard_command("Delete Image", None, command)
|
||||
@@ -139,7 +148,7 @@ impl Resolve<super::Args> for DeleteImage {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for PruneImages {
|
||||
impl Resolve<crate::api::Args> for PruneImages {
|
||||
#[instrument(
|
||||
"PruneImages",
|
||||
skip_all,
|
||||
@@ -148,7 +157,10 @@ impl Resolve<super::Args> for PruneImages {
|
||||
core = args.core,
|
||||
)
|
||||
)]
|
||||
async fn resolve(self, args: &super::Args) -> anyhow::Result<Log> {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
let command = String::from("docker image prune -a -f");
|
||||
Ok(
|
||||
run_komodo_standard_command("Prune Images", None, command)
|
||||
@@ -161,8 +173,11 @@ impl Resolve<super::Args> for PruneImages {
|
||||
// NETWORK
|
||||
// =======
|
||||
|
||||
impl Resolve<super::Args> for InspectNetwork {
|
||||
async fn resolve(self, _: &super::Args) -> anyhow::Result<Network> {
|
||||
impl Resolve<crate::api::Args> for InspectNetwork {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &crate::api::Args,
|
||||
) -> anyhow::Result<Network> {
|
||||
let client = docker_client().load();
|
||||
let client = client
|
||||
.iter()
|
||||
@@ -174,7 +189,7 @@ impl Resolve<super::Args> for InspectNetwork {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for CreateNetwork {
|
||||
impl Resolve<crate::api::Args> for CreateNetwork {
|
||||
#[instrument(
|
||||
"CreateNetwork",
|
||||
skip_all,
|
||||
@@ -185,7 +200,10 @@ impl Resolve<super::Args> for CreateNetwork {
|
||||
driver = self.driver,
|
||||
)
|
||||
)]
|
||||
async fn resolve(self, args: &super::Args) -> anyhow::Result<Log> {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
let CreateNetwork { name, driver } = self;
|
||||
let driver = match driver {
|
||||
Some(driver) => format!(" -d {driver}"),
|
||||
@@ -201,7 +219,7 @@ impl Resolve<super::Args> for CreateNetwork {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for DeleteNetwork {
|
||||
impl Resolve<crate::api::Args> for DeleteNetwork {
|
||||
#[instrument(
|
||||
"DeleteNetwork",
|
||||
skip_all,
|
||||
@@ -211,7 +229,10 @@ impl Resolve<super::Args> for DeleteNetwork {
|
||||
network = self.name,
|
||||
)
|
||||
)]
|
||||
async fn resolve(self, args: &super::Args) -> anyhow::Result<Log> {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
let command = format!("docker network rm {}", self.name);
|
||||
Ok(
|
||||
run_komodo_standard_command("Delete Network", None, command)
|
||||
@@ -222,7 +243,7 @@ impl Resolve<super::Args> for DeleteNetwork {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for PruneNetworks {
|
||||
impl Resolve<crate::api::Args> for PruneNetworks {
|
||||
#[instrument(
|
||||
"PruneNetworks",
|
||||
skip_all,
|
||||
@@ -231,7 +252,10 @@ impl Resolve<super::Args> for PruneNetworks {
|
||||
core = args.core,
|
||||
)
|
||||
)]
|
||||
async fn resolve(self, args: &super::Args) -> anyhow::Result<Log> {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
let command = String::from("docker network prune -f");
|
||||
Ok(
|
||||
run_komodo_standard_command("Prune Networks", None, command)
|
||||
@@ -244,8 +268,11 @@ impl Resolve<super::Args> for PruneNetworks {
|
||||
// VOLUME
|
||||
// ======
|
||||
|
||||
impl Resolve<super::Args> for InspectVolume {
|
||||
async fn resolve(self, _: &super::Args) -> anyhow::Result<Volume> {
|
||||
impl Resolve<crate::api::Args> for InspectVolume {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &crate::api::Args,
|
||||
) -> anyhow::Result<Volume> {
|
||||
let client = docker_client().load();
|
||||
let client = client
|
||||
.iter()
|
||||
@@ -257,7 +284,7 @@ impl Resolve<super::Args> for InspectVolume {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for DeleteVolume {
|
||||
impl Resolve<crate::api::Args> for DeleteVolume {
|
||||
#[instrument(
|
||||
"DeleteVolume",
|
||||
skip_all,
|
||||
@@ -267,7 +294,10 @@ impl Resolve<super::Args> for DeleteVolume {
|
||||
volume = self.name,
|
||||
)
|
||||
)]
|
||||
async fn resolve(self, args: &super::Args) -> anyhow::Result<Log> {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
let command = format!("docker volume rm {}", self.name);
|
||||
Ok(
|
||||
run_komodo_standard_command("Delete Volume", None, command)
|
||||
@@ -278,7 +308,7 @@ impl Resolve<super::Args> for DeleteVolume {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for PruneVolumes {
|
||||
impl Resolve<crate::api::Args> for PruneVolumes {
|
||||
#[instrument(
|
||||
"PruneVolumes",
|
||||
skip_all,
|
||||
@@ -287,7 +317,10 @@ impl Resolve<super::Args> for PruneVolumes {
|
||||
core = args.core,
|
||||
)
|
||||
)]
|
||||
async fn resolve(self, args: &super::Args) -> anyhow::Result<Log> {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
let command = String::from("docker volume prune -a -f");
|
||||
Ok(
|
||||
run_komodo_standard_command("Prune Volumes", None, command)
|
||||
|
||||
@@ -16,10 +16,10 @@ use crate::{
|
||||
config::periphery_config, helpers::handle_post_repo_execution,
|
||||
};
|
||||
|
||||
impl Resolve<super::Args> for GetLatestCommit {
|
||||
impl Resolve<crate::api::Args> for GetLatestCommit {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &super::Args,
|
||||
_: &crate::api::Args,
|
||||
) -> anyhow::Result<Option<LatestCommit>> {
|
||||
let repo_path = match self.path {
|
||||
Some(p) => PathBuf::from(p),
|
||||
@@ -33,7 +33,7 @@ impl Resolve<super::Args> for GetLatestCommit {
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<super::Args> for CloneRepo {
|
||||
impl Resolve<crate::api::Args> for CloneRepo {
|
||||
#[instrument(
|
||||
"CloneRepo",
|
||||
skip_all,
|
||||
@@ -46,7 +46,7 @@ impl Resolve<super::Args> for CloneRepo {
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &super::Args,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<PeripheryRepoExecutionResponse> {
|
||||
let CloneRepo {
|
||||
args,
|
||||
@@ -79,7 +79,7 @@ impl Resolve<super::Args> for CloneRepo {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for PullRepo {
|
||||
impl Resolve<crate::api::Args> for PullRepo {
|
||||
#[instrument(
|
||||
"PullRepo",
|
||||
skip_all,
|
||||
@@ -92,7 +92,7 @@ impl Resolve<super::Args> for PullRepo {
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &super::Args,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<PeripheryRepoExecutionResponse> {
|
||||
let PullRepo {
|
||||
args,
|
||||
@@ -124,7 +124,7 @@ impl Resolve<super::Args> for PullRepo {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for PullOrCloneRepo {
|
||||
impl Resolve<crate::api::Args> for PullOrCloneRepo {
|
||||
#[instrument(
|
||||
"PullOrCloneRepo",
|
||||
skip_all,
|
||||
@@ -137,7 +137,7 @@ impl Resolve<super::Args> for PullOrCloneRepo {
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &super::Args,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<PeripheryRepoExecutionResponse> {
|
||||
let PullOrCloneRepo {
|
||||
args,
|
||||
@@ -171,7 +171,7 @@ impl Resolve<super::Args> for PullOrCloneRepo {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for RenameRepo {
|
||||
impl Resolve<crate::api::Args> for RenameRepo {
|
||||
#[instrument(
|
||||
"RenameRepo",
|
||||
skip_all,
|
||||
@@ -182,7 +182,10 @@ impl Resolve<super::Args> for RenameRepo {
|
||||
new_name = self.new_name,
|
||||
)
|
||||
)]
|
||||
async fn resolve(self, args: &super::Args) -> anyhow::Result<Log> {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
let RenameRepo {
|
||||
curr_name,
|
||||
new_name,
|
||||
@@ -201,7 +204,7 @@ impl Resolve<super::Args> for RenameRepo {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for DeleteRepo {
|
||||
impl Resolve<crate::api::Args> for DeleteRepo {
|
||||
#[instrument(
|
||||
"DeleteRepo",
|
||||
skip_all,
|
||||
@@ -212,7 +215,10 @@ impl Resolve<super::Args> for DeleteRepo {
|
||||
is_build = self.is_build,
|
||||
)
|
||||
)]
|
||||
async fn resolve(self, args: &super::Args) -> anyhow::Result<Log> {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
let DeleteRepo { name, is_build } = self;
|
||||
// If using custom clone path, it will be passed by core instead of name.
|
||||
// So the join will resolve to just the absolute path.
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::{
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for RotatePrivateKey {
|
||||
impl Resolve<crate::api::Args> for RotatePrivateKey {
|
||||
#[instrument(
|
||||
"RotatePrivateKey",
|
||||
skip_all,
|
||||
@@ -23,7 +23,7 @@ impl Resolve<super::Args> for RotatePrivateKey {
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &super::Args,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<RotatePrivateKeyResponse> {
|
||||
let public_key = periphery_keys().rotate().await?.into_inner();
|
||||
info!("New Public Key: {public_key}");
|
||||
@@ -33,7 +33,7 @@ impl Resolve<super::Args> for RotatePrivateKey {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for RotateCorePublicKey {
|
||||
impl Resolve<crate::api::Args> for RotateCorePublicKey {
|
||||
#[instrument(
|
||||
"RotateCorePublicKey",
|
||||
skip_all,
|
||||
@@ -45,7 +45,7 @@ impl Resolve<super::Args> for RotateCorePublicKey {
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &super::Args,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<NoData> {
|
||||
let config = periphery_config();
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ pub mod terminal;
|
||||
mod build;
|
||||
mod compose;
|
||||
mod container;
|
||||
mod deploy;
|
||||
mod docker;
|
||||
mod git;
|
||||
mod keys;
|
||||
@@ -94,7 +93,7 @@ pub enum PeripheryRequest {
|
||||
GetFullContainerStats(GetFullContainerStats),
|
||||
|
||||
// Container (Write)
|
||||
Deploy(Deploy),
|
||||
RunContainer(RunContainer),
|
||||
StartContainer(StartContainer),
|
||||
RestartContainer(RestartContainer),
|
||||
PauseContainer(PauseContainer),
|
||||
@@ -136,16 +135,26 @@ pub enum PeripheryRequest {
|
||||
// All in one (Write)
|
||||
PruneSystem(PruneSystem),
|
||||
|
||||
// Swarm
|
||||
// Swarm (Read)
|
||||
PollSwarmStatus(PollSwarmStatus),
|
||||
InspectSwarmNode(InspectSwarmNode),
|
||||
InspectSwarmConfig(InspectSwarmConfig),
|
||||
InspectSwarmSecret(InspectSwarmSecret),
|
||||
InspectSwarmStack(InspectSwarmStack),
|
||||
InspectSwarmTask(InspectSwarmTask),
|
||||
InspectSwarmService(InspectSwarmService),
|
||||
GetSwarmServiceLog(GetSwarmServiceLog),
|
||||
GetSwarmServiceLogSearch(GetSwarmServiceLogSearch),
|
||||
InspectSwarmTask(InspectSwarmTask),
|
||||
InspectSwarmConfig(InspectSwarmConfig),
|
||||
InspectSwarmSecret(InspectSwarmSecret),
|
||||
|
||||
// Swarm (Write)
|
||||
UpdateSwarmNode(UpdateSwarmNode),
|
||||
RemoveSwarmNodes(RemoveSwarmNodes),
|
||||
DeploySwarmStack(DeploySwarmStack),
|
||||
RemoveSwarmStacks(RemoveSwarmStacks),
|
||||
CreateSwarmService(CreateSwarmService),
|
||||
RemoveSwarmServices(RemoveSwarmServices),
|
||||
RemoveSwarmConfigs(RemoveSwarmConfigs),
|
||||
RemoveSwarmSecrets(RemoveSwarmSecrets),
|
||||
|
||||
// Terminal
|
||||
ListTerminals(ListTerminals),
|
||||
|
||||
@@ -13,10 +13,10 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
impl Resolve<super::Args> for PollStatus {
|
||||
impl Resolve<crate::api::Args> for PollStatus {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &super::Args,
|
||||
_: &crate::api::Args,
|
||||
) -> anyhow::Result<PollStatusResponse> {
|
||||
let stats_client = stats_client().read().await;
|
||||
|
||||
|
||||
@@ -1,245 +0,0 @@
|
||||
use anyhow::Context as _;
|
||||
use command::{
|
||||
run_komodo_shell_command, run_komodo_standard_command,
|
||||
};
|
||||
use komodo_client::entities::{
|
||||
docker::{
|
||||
SwarmLists, config::SwarmConfig, node::SwarmNode,
|
||||
secret::SwarmSecret, service::SwarmService,
|
||||
stack::SwarmStackLists, task::SwarmTask,
|
||||
},
|
||||
update::Log,
|
||||
};
|
||||
use periphery_client::api::swarm::*;
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
docker::{
|
||||
config::{inspect_swarm_config, list_swarm_configs},
|
||||
stack::{inspect_swarm_stack, list_swarm_stacks},
|
||||
},
|
||||
helpers::format_log_grep,
|
||||
state::docker_client,
|
||||
};
|
||||
|
||||
impl Resolve<super::Args> for PollSwarmStatus {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &super::Args,
|
||||
) -> anyhow::Result<PollSwarmStatusResponse> {
|
||||
let client = docker_client().load();
|
||||
let client = client
|
||||
.iter()
|
||||
.next()
|
||||
.context("Could not connect to docker client")?;
|
||||
let (inspect, nodes, services, tasks, secrets, configs, stacks) = tokio::join!(
|
||||
client.inspect_swarm(),
|
||||
client.list_swarm_nodes(),
|
||||
client.list_swarm_services(),
|
||||
client.list_swarm_tasks(),
|
||||
client.list_swarm_secrets(),
|
||||
list_swarm_configs(),
|
||||
list_swarm_stacks(),
|
||||
);
|
||||
Ok(PollSwarmStatusResponse {
|
||||
inspect: inspect.ok(),
|
||||
lists: SwarmLists {
|
||||
nodes: nodes.unwrap_or_default(),
|
||||
services: services.unwrap_or_default(),
|
||||
tasks: tasks.unwrap_or_default(),
|
||||
secrets: secrets.unwrap_or_default(),
|
||||
configs: configs.unwrap_or_default(),
|
||||
stacks: stacks.unwrap_or_default(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ======
|
||||
// Node
|
||||
// ======
|
||||
|
||||
impl Resolve<super::Args> for InspectSwarmNode {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &super::Args,
|
||||
) -> anyhow::Result<SwarmNode> {
|
||||
let client = docker_client().load();
|
||||
let client = client
|
||||
.iter()
|
||||
.next()
|
||||
.context("Could not connect to docker client")?;
|
||||
client.inspect_swarm_node(&self.node).await
|
||||
}
|
||||
}
|
||||
|
||||
// =========
|
||||
// Service
|
||||
// =========
|
||||
|
||||
impl Resolve<super::Args> for InspectSwarmService {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &super::Args,
|
||||
) -> anyhow::Result<SwarmService> {
|
||||
let client = docker_client().load();
|
||||
let client = client
|
||||
.iter()
|
||||
.next()
|
||||
.context("Could not connect to docker client")?;
|
||||
client.inspect_swarm_service(&self.service).await
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<super::Args> for GetSwarmServiceLog {
|
||||
async fn resolve(self, _: &super::Args) -> anyhow::Result<Log> {
|
||||
let GetSwarmServiceLog {
|
||||
service,
|
||||
tail,
|
||||
timestamps,
|
||||
no_task_ids,
|
||||
no_resolve,
|
||||
details,
|
||||
} = self;
|
||||
let timestamps = if timestamps {
|
||||
" --timestamps"
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let no_task_ids = if no_task_ids {
|
||||
" --no-task-ids"
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let no_resolve = if no_resolve {
|
||||
" --no-resolve"
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let details = if details {
|
||||
" --details"
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let command = format!(
|
||||
"docker service logs --tail {tail}{timestamps}{no_task_ids}{no_resolve}{details} {service}",
|
||||
);
|
||||
Ok(
|
||||
run_komodo_standard_command(
|
||||
"Get Swarm Service Log",
|
||||
None,
|
||||
command,
|
||||
)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<super::Args> for GetSwarmServiceLogSearch {
|
||||
async fn resolve(self, _: &super::Args) -> anyhow::Result<Log> {
|
||||
let GetSwarmServiceLogSearch {
|
||||
service,
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
no_task_ids,
|
||||
no_resolve,
|
||||
details,
|
||||
} = self;
|
||||
let timestamps = if timestamps {
|
||||
" --timestamps"
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let no_task_ids = if no_task_ids {
|
||||
" --no-task-ids"
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let no_resolve = if no_resolve {
|
||||
" --no-resolve"
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let details = if details {
|
||||
" --details"
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let grep = format_log_grep(&terms, combinator, invert);
|
||||
let command = format!(
|
||||
"docker service logs --tail 5000{timestamps}{no_task_ids}{no_resolve}{details} {service} 2>&1 | {grep}",
|
||||
);
|
||||
Ok(
|
||||
run_komodo_shell_command(
|
||||
"Search Swarm Service Log",
|
||||
None,
|
||||
command,
|
||||
)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ======
|
||||
// Task
|
||||
// ======
|
||||
|
||||
impl Resolve<super::Args> for InspectSwarmTask {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &super::Args,
|
||||
) -> anyhow::Result<SwarmTask> {
|
||||
let client = docker_client().load();
|
||||
let client = client
|
||||
.iter()
|
||||
.next()
|
||||
.context("Could not connect to docker client")?;
|
||||
client.inspect_swarm_task(&self.task).await
|
||||
}
|
||||
}
|
||||
|
||||
// ========
|
||||
// Secret
|
||||
// ========
|
||||
|
||||
impl Resolve<super::Args> for InspectSwarmSecret {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &super::Args,
|
||||
) -> anyhow::Result<SwarmSecret> {
|
||||
let client = docker_client().load();
|
||||
let client = client
|
||||
.iter()
|
||||
.next()
|
||||
.context("Could not connect to docker client")?;
|
||||
client.inspect_swarm_secret(&self.secret).await
|
||||
}
|
||||
}
|
||||
|
||||
// ========
|
||||
// Config
|
||||
// ========
|
||||
|
||||
impl Resolve<super::Args> for InspectSwarmConfig {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &super::Args,
|
||||
) -> anyhow::Result<Vec<SwarmConfig>> {
|
||||
inspect_swarm_config(&self.config).await
|
||||
}
|
||||
}
|
||||
|
||||
// =======
|
||||
// Stack
|
||||
// =======
|
||||
|
||||
impl Resolve<super::Args> for InspectSwarmStack {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &super::Args,
|
||||
) -> anyhow::Result<SwarmStackLists> {
|
||||
inspect_swarm_stack(self.stack).await
|
||||
}
|
||||
}
|
||||
271
bin/periphery/src/api/swarm/mod.rs
Normal file
271
bin/periphery/src/api/swarm/mod.rs
Normal file
@@ -0,0 +1,271 @@
|
||||
use anyhow::Context as _;
|
||||
use command::run_komodo_standard_command;
|
||||
use komodo_client::entities::{
|
||||
docker::{
|
||||
SwarmLists, config::SwarmConfig, node::SwarmNode,
|
||||
secret::SwarmSecret, task::SwarmTask,
|
||||
},
|
||||
update::Log,
|
||||
};
|
||||
use periphery_client::api::swarm::*;
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
docker::{
|
||||
config::{inspect_swarm_config, list_swarm_configs},
|
||||
stack::list_swarm_stacks,
|
||||
},
|
||||
state::docker_client,
|
||||
};
|
||||
|
||||
mod service;
|
||||
mod stack;
|
||||
|
||||
impl Resolve<crate::api::Args> for PollSwarmStatus {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &crate::api::Args,
|
||||
) -> anyhow::Result<PollSwarmStatusResponse> {
|
||||
let client = docker_client().load();
|
||||
let client = client
|
||||
.iter()
|
||||
.next()
|
||||
.context("Could not connect to docker client")?;
|
||||
let (inspect, nodes, services, tasks, secrets, configs, stacks) = tokio::join!(
|
||||
client.inspect_swarm(),
|
||||
client.list_swarm_nodes(),
|
||||
client.list_swarm_services(),
|
||||
client.list_swarm_tasks(),
|
||||
client.list_swarm_secrets(),
|
||||
list_swarm_configs(),
|
||||
list_swarm_stacks(),
|
||||
);
|
||||
Ok(PollSwarmStatusResponse {
|
||||
inspect: inspect.ok(),
|
||||
lists: SwarmLists {
|
||||
nodes: nodes.unwrap_or_default(),
|
||||
services: services.unwrap_or_default(),
|
||||
tasks: tasks.unwrap_or_default(),
|
||||
secrets: secrets.unwrap_or_default(),
|
||||
configs: configs.unwrap_or_default(),
|
||||
stacks: stacks.unwrap_or_default(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ======
|
||||
// Node
|
||||
// ======
|
||||
|
||||
impl Resolve<crate::api::Args> for InspectSwarmNode {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &crate::api::Args,
|
||||
) -> anyhow::Result<SwarmNode> {
|
||||
let client = docker_client().load();
|
||||
let client = client
|
||||
.iter()
|
||||
.next()
|
||||
.context("Could not connect to docker client")?;
|
||||
client.inspect_swarm_node(&self.node).await
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<crate::api::Args> for UpdateSwarmNode {
|
||||
#[instrument(
|
||||
"UpdateSwarmNode",
|
||||
skip_all,
|
||||
fields(
|
||||
id = args.id.to_string(),
|
||||
core = args.core,
|
||||
node = self.node,
|
||||
update = serde_json::to_string(&self).unwrap_or_else(|e| e.to_string())
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
let mut command = String::from("docker node update");
|
||||
|
||||
if let Some(role) = self.role {
|
||||
command += " --role=";
|
||||
command += role.as_ref();
|
||||
}
|
||||
|
||||
if let Some(availability) = self.availability {
|
||||
command += " --availability=";
|
||||
command += availability.as_ref();
|
||||
}
|
||||
|
||||
if let Some(label_add) = self.label_add {
|
||||
for (key, value) in label_add {
|
||||
command += " --label-add ";
|
||||
command += &key;
|
||||
if let Some(value) = value {
|
||||
command += "=";
|
||||
command += &value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(label_rm) = self.label_rm {
|
||||
for key in label_rm {
|
||||
command += " --label-rm ";
|
||||
command += &key;
|
||||
}
|
||||
}
|
||||
|
||||
command += " ";
|
||||
command += &self.node;
|
||||
|
||||
Ok(
|
||||
run_komodo_standard_command("Update Swarm Node", None, command)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<crate::api::Args> for RemoveSwarmNodes {
|
||||
#[instrument(
|
||||
"RemoveSwarmNodes",
|
||||
skip_all,
|
||||
fields(
|
||||
id = args.id.to_string(),
|
||||
core = args.core,
|
||||
nodes = serde_json::to_string(&self.nodes).unwrap_or_else(|e| e.to_string()),
|
||||
force = self.force,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
let mut command = String::from("docker node rm");
|
||||
if self.force {
|
||||
command += " --force"
|
||||
}
|
||||
for node in self.nodes {
|
||||
command += " ";
|
||||
command += &node;
|
||||
}
|
||||
Ok(
|
||||
run_komodo_standard_command(
|
||||
"Remove Swarm Nodes",
|
||||
None,
|
||||
command,
|
||||
)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ======
|
||||
// Task
|
||||
// ======
|
||||
|
||||
impl Resolve<crate::api::Args> for InspectSwarmTask {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &crate::api::Args,
|
||||
) -> anyhow::Result<SwarmTask> {
|
||||
let client = docker_client().load();
|
||||
let client = client
|
||||
.iter()
|
||||
.next()
|
||||
.context("Could not connect to docker client")?;
|
||||
client.inspect_swarm_task(&self.task).await
|
||||
}
|
||||
}
|
||||
|
||||
// ========
|
||||
// Config
|
||||
// ========
|
||||
|
||||
impl Resolve<crate::api::Args> for InspectSwarmConfig {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &crate::api::Args,
|
||||
) -> anyhow::Result<Vec<SwarmConfig>> {
|
||||
inspect_swarm_config(&self.config).await
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<crate::api::Args> for RemoveSwarmConfigs {
|
||||
#[instrument(
|
||||
"RemoveSwarmConfigs",
|
||||
skip_all,
|
||||
fields(
|
||||
id = args.id.to_string(),
|
||||
core = args.core,
|
||||
configs = serde_json::to_string(&self.configs).unwrap_or_else(|e| e.to_string()),
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
let mut command = String::from("docker config rm");
|
||||
for config in self.configs {
|
||||
command += " ";
|
||||
command += &config;
|
||||
}
|
||||
Ok(
|
||||
run_komodo_standard_command(
|
||||
"Remove Swarm Configs",
|
||||
None,
|
||||
command,
|
||||
)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ========
|
||||
// Secret
|
||||
// ========
|
||||
|
||||
impl Resolve<crate::api::Args> for InspectSwarmSecret {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &crate::api::Args,
|
||||
) -> anyhow::Result<SwarmSecret> {
|
||||
let client = docker_client().load();
|
||||
let client = client
|
||||
.iter()
|
||||
.next()
|
||||
.context("Could not connect to docker client")?;
|
||||
client.inspect_swarm_secret(&self.secret).await
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<crate::api::Args> for RemoveSwarmSecrets {
|
||||
#[instrument(
|
||||
"RemoveSwarmSecrets",
|
||||
skip_all,
|
||||
fields(
|
||||
id = args.id.to_string(),
|
||||
core = args.core,
|
||||
secrets = serde_json::to_string(&self.secrets).unwrap_or_else(|e| e.to_string()),
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
let mut command = String::from("docker secret rm");
|
||||
for secret in self.secrets {
|
||||
command += " ";
|
||||
command += &secret;
|
||||
}
|
||||
Ok(
|
||||
run_komodo_standard_command(
|
||||
"Remove Swarm Secrets",
|
||||
None,
|
||||
command,
|
||||
)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
}
|
||||
327
bin/periphery/src/api/swarm/service.rs
Normal file
327
bin/periphery/src/api/swarm/service.rs
Normal file
@@ -0,0 +1,327 @@
|
||||
use std::fmt::Write;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use command::{
|
||||
KomodoCommandMode, run_komodo_command_with_sanitization,
|
||||
run_komodo_shell_command, run_komodo_standard_command,
|
||||
};
|
||||
use formatting::format_serror;
|
||||
use interpolate::Interpolator;
|
||||
use komodo_client::entities::{
|
||||
deployment::{
|
||||
Deployment, DeploymentConfig, DeploymentImage,
|
||||
conversions_from_str, extract_registry_domain,
|
||||
},
|
||||
docker::service::SwarmService,
|
||||
environment_vars_from_str,
|
||||
update::Log,
|
||||
};
|
||||
use periphery_client::api::swarm::{
|
||||
CreateSwarmService, GetSwarmServiceLog, GetSwarmServiceLogSearch,
|
||||
InspectSwarmService, RemoveSwarmServices,
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use tracing::Instrument;
|
||||
|
||||
use crate::{
|
||||
config::periphery_config,
|
||||
docker::docker_login,
|
||||
helpers::{
|
||||
format_log_grep, push_conversions, push_environment,
|
||||
push_extra_args, push_labels,
|
||||
},
|
||||
state::docker_client,
|
||||
};
|
||||
|
||||
impl Resolve<crate::api::Args> for InspectSwarmService {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &crate::api::Args,
|
||||
) -> anyhow::Result<SwarmService> {
|
||||
let client = docker_client().load();
|
||||
let client = client
|
||||
.iter()
|
||||
.next()
|
||||
.context("Could not connect to docker client")?;
|
||||
client.inspect_swarm_service(&self.service).await
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<crate::api::Args> for GetSwarmServiceLog {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
let GetSwarmServiceLog {
|
||||
service,
|
||||
tail,
|
||||
timestamps,
|
||||
no_task_ids,
|
||||
no_resolve,
|
||||
details,
|
||||
} = self;
|
||||
let timestamps = if timestamps {
|
||||
" --timestamps"
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let no_task_ids = if no_task_ids {
|
||||
" --no-task-ids"
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let no_resolve = if no_resolve {
|
||||
" --no-resolve"
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let details = if details {
|
||||
" --details"
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let command = format!(
|
||||
"docker service logs --tail {tail}{timestamps}{no_task_ids}{no_resolve}{details} {service}",
|
||||
);
|
||||
Ok(
|
||||
run_komodo_standard_command(
|
||||
"Get Swarm Service Log",
|
||||
None,
|
||||
command,
|
||||
)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<crate::api::Args> for GetSwarmServiceLogSearch {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
let GetSwarmServiceLogSearch {
|
||||
service,
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
no_task_ids,
|
||||
no_resolve,
|
||||
details,
|
||||
} = self;
|
||||
let timestamps = if timestamps {
|
||||
" --timestamps"
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let no_task_ids = if no_task_ids {
|
||||
" --no-task-ids"
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let no_resolve = if no_resolve {
|
||||
" --no-resolve"
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let details = if details {
|
||||
" --details"
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let grep = format_log_grep(&terms, combinator, invert);
|
||||
let command = format!(
|
||||
"docker service logs --tail 5000{timestamps}{no_task_ids}{no_resolve}{details} {service} 2>&1 | {grep}",
|
||||
);
|
||||
Ok(
|
||||
run_komodo_shell_command(
|
||||
"Search Swarm Service Log",
|
||||
None,
|
||||
command,
|
||||
)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<crate::api::Args> for RemoveSwarmServices {
|
||||
#[instrument(
|
||||
"RemoveSwarmServices",
|
||||
skip_all,
|
||||
fields(
|
||||
id = args.id.to_string(),
|
||||
core = args.core,
|
||||
services = serde_json::to_string(&self.services).unwrap_or_else(|e| e.to_string()),
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
let mut command = String::from("docker service rm");
|
||||
for service in self.services {
|
||||
command += " ";
|
||||
command += &service;
|
||||
}
|
||||
Ok(
|
||||
run_komodo_standard_command(
|
||||
"Remove Swarm Services",
|
||||
None,
|
||||
command,
|
||||
)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<crate::api::Args> for CreateSwarmService {
|
||||
#[instrument(
|
||||
"CreateSwarmService",
|
||||
skip_all,
|
||||
fields(
|
||||
id = args.id.to_string(),
|
||||
core = args.core,
|
||||
deployment = &self.deployment.name,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let CreateSwarmService {
|
||||
mut deployment,
|
||||
registry_token,
|
||||
mut replacers,
|
||||
} = self;
|
||||
|
||||
let mut interpolator =
|
||||
Interpolator::new(None, &periphery_config().secrets);
|
||||
interpolator.interpolate_deployment(&mut deployment)?;
|
||||
replacers.extend(interpolator.secret_replacers);
|
||||
|
||||
let image = if let DeploymentImage::Image { image } =
|
||||
&deployment.config.image
|
||||
{
|
||||
if image.is_empty() {
|
||||
return Ok(Log::error(
|
||||
"Get Image",
|
||||
String::from("Deployment does not have image attached"),
|
||||
));
|
||||
}
|
||||
image
|
||||
} else {
|
||||
return Ok(Log::error(
|
||||
"Get Image",
|
||||
String::from(
|
||||
"Deployment does not have build replaced by Core",
|
||||
),
|
||||
));
|
||||
};
|
||||
|
||||
let use_with_registry_auth = match docker_login(
|
||||
&extract_registry_domain(image)?,
|
||||
&deployment.config.image_registry_account,
|
||||
registry_token.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
return Ok(Log::error(
|
||||
"Docker Login",
|
||||
format_serror(
|
||||
&e.context("Failed to login to docker registry").into(),
|
||||
),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let command = docker_service_create_command(
|
||||
&deployment,
|
||||
image,
|
||||
use_with_registry_auth,
|
||||
)
|
||||
.context(
|
||||
"Unable to generate valid docker service create command",
|
||||
)?;
|
||||
|
||||
let span = info_span!("ExecuteDockerServiceCreate");
|
||||
let Some(log) = run_komodo_command_with_sanitization(
|
||||
"Docker Service Create",
|
||||
None,
|
||||
command,
|
||||
KomodoCommandMode::Shell,
|
||||
&replacers,
|
||||
)
|
||||
.instrument(span)
|
||||
.await
|
||||
else {
|
||||
// The none case is only for empty command,
|
||||
// this won't be the case given it is populated above.
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
Ok(log)
|
||||
}
|
||||
}
|
||||
|
||||
fn docker_service_create_command(
|
||||
Deployment {
|
||||
name,
|
||||
config:
|
||||
DeploymentConfig {
|
||||
volumes,
|
||||
ports,
|
||||
network,
|
||||
command,
|
||||
environment,
|
||||
labels,
|
||||
extra_args,
|
||||
..
|
||||
},
|
||||
..
|
||||
}: &Deployment,
|
||||
image: &str,
|
||||
use_with_registry_auth: bool,
|
||||
) -> anyhow::Result<String> {
|
||||
let mut res = format!(
|
||||
"docker service create --name {name} --network {network}"
|
||||
);
|
||||
|
||||
push_conversions(
|
||||
&mut res,
|
||||
&conversions_from_str(ports).context("Invalid ports")?,
|
||||
"-p",
|
||||
)?;
|
||||
|
||||
push_conversions(
|
||||
&mut res,
|
||||
&conversions_from_str(volumes).context("Invalid volumes")?,
|
||||
"--mount",
|
||||
)?;
|
||||
|
||||
push_environment(
|
||||
&mut res,
|
||||
&environment_vars_from_str(environment)
|
||||
.context("Invalid environment")?,
|
||||
)?;
|
||||
|
||||
push_labels(
|
||||
&mut res,
|
||||
&environment_vars_from_str(labels).context("Invalid labels")?,
|
||||
)?;
|
||||
|
||||
if use_with_registry_auth {
|
||||
res += " --with-registry-auth";
|
||||
}
|
||||
|
||||
push_extra_args(&mut res, extra_args)?;
|
||||
|
||||
write!(&mut res, " {image}")?;
|
||||
|
||||
if !command.is_empty() {
|
||||
write!(&mut res, " {command}")?;
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
306
bin/periphery/src/api/swarm/stack.rs
Normal file
306
bin/periphery/src/api/swarm/stack.rs
Normal file
@@ -0,0 +1,306 @@
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use command::{
|
||||
KomodoCommandMode, run_komodo_command_with_sanitization,
|
||||
run_komodo_standard_command,
|
||||
};
|
||||
use formatting::format_serror;
|
||||
use interpolate::Interpolator;
|
||||
use komodo_client::{
|
||||
entities::{
|
||||
all_logs_success,
|
||||
docker::stack::SwarmStack,
|
||||
stack::{ComposeFile, ComposeService, StackServiceNames},
|
||||
update::Log,
|
||||
},
|
||||
parsers::parse_multiline_command,
|
||||
};
|
||||
use periphery_client::api::{
|
||||
DeployStackResponse,
|
||||
swarm::{DeploySwarmStack, InspectSwarmStack, RemoveSwarmStacks},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use tracing::Instrument as _;
|
||||
|
||||
use crate::{
|
||||
config::periphery_config,
|
||||
docker::stack::inspect_swarm_stack,
|
||||
helpers::push_extra_args,
|
||||
stack::{maybe_login_registry, validate_files, write::write_stack},
|
||||
};
|
||||
|
||||
impl Resolve<crate::api::Args> for InspectSwarmStack {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &crate::api::Args,
|
||||
) -> anyhow::Result<SwarmStack> {
|
||||
inspect_swarm_stack(self.stack).await
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<crate::api::Args> for RemoveSwarmStacks {
|
||||
#[instrument(
|
||||
"RemoveSwarmStacks",
|
||||
skip_all,
|
||||
fields(
|
||||
id = args.id.to_string(),
|
||||
core = args.core,
|
||||
stacks = serde_json::to_string(&self.stacks).unwrap_or_else(|e| e.to_string()),
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Log> {
|
||||
let mut command = String::from("docker stack rm");
|
||||
// This defaults to true, only need when false
|
||||
if !self.detach {
|
||||
command += " --detach=false"
|
||||
}
|
||||
for stack in self.stacks {
|
||||
command += " ";
|
||||
command += &stack;
|
||||
}
|
||||
Ok(
|
||||
run_komodo_standard_command(
|
||||
"Remove Swarm Stacks",
|
||||
None,
|
||||
command,
|
||||
)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<crate::api::Args> for DeploySwarmStack {
|
||||
#[instrument(
|
||||
"DeploySwarmStack",
|
||||
skip_all,
|
||||
fields(
|
||||
id = args.id.to_string(),
|
||||
core = args.core,
|
||||
stack = self.stack.name,
|
||||
repo = self.repo.as_ref().map(|repo| &repo.name),
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let DeploySwarmStack {
|
||||
mut stack,
|
||||
repo,
|
||||
git_token,
|
||||
registry_token,
|
||||
mut replacers,
|
||||
} = self;
|
||||
|
||||
let mut res = DeployStackResponse::default();
|
||||
|
||||
let mut interpolator =
|
||||
Interpolator::new(None, &periphery_config().secrets);
|
||||
// Only interpolate Stack. Repo interpolation will be handled
|
||||
// by the CloneRepo / PullOrCloneRepo call.
|
||||
interpolator
|
||||
.interpolate_stack(&mut stack)?
|
||||
.push_logs(&mut res.logs);
|
||||
replacers.extend(interpolator.secret_replacers);
|
||||
|
||||
// Env files are not supported by docker stack deploy so are ignored.
|
||||
let (run_directory, _) = match write_stack(
|
||||
&stack,
|
||||
repo.as_ref(),
|
||||
git_token,
|
||||
replacers.clone(),
|
||||
&mut res,
|
||||
args,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
res
|
||||
.logs
|
||||
.push(Log::error("Write Stack", format_serror(&e.into())));
|
||||
return Ok(res);
|
||||
}
|
||||
};
|
||||
|
||||
// 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)",
|
||||
)?;
|
||||
|
||||
validate_files(&stack, &run_directory, &mut res).await;
|
||||
if !all_logs_success(&res.logs) {
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
let use_with_registry_auth =
|
||||
maybe_login_registry(&stack, registry_token, &mut res.logs)
|
||||
.await;
|
||||
if !all_logs_success(&res.logs) {
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
// Pre deploy
|
||||
if !stack.config.pre_deploy.is_none() {
|
||||
let pre_deploy_path =
|
||||
run_directory.join(&stack.config.pre_deploy.path);
|
||||
let span = info_span!("ExecutePreDeploy");
|
||||
if let Some(log) = run_komodo_command_with_sanitization(
|
||||
"Pre Deploy",
|
||||
pre_deploy_path.as_path(),
|
||||
&stack.config.pre_deploy.command,
|
||||
KomodoCommandMode::Multiline,
|
||||
&replacers,
|
||||
)
|
||||
.instrument(span)
|
||||
.await
|
||||
{
|
||||
res.logs.push(log);
|
||||
if !all_logs_success(&res.logs) {
|
||||
return Ok(res);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let file_args = stack.compose_file_paths().join(" -c ");
|
||||
|
||||
// This will be the last project name, which is the one that needs to be destroyed.
|
||||
// Might be different from the current project name, if user renames stack / changes to custom project name.
|
||||
let last_project_name = stack.project_name(false);
|
||||
let project_name = stack.project_name(true);
|
||||
|
||||
// Uses 'docker stack config' command to extract services (including image)
|
||||
// after performing interpolation
|
||||
{
|
||||
let command = format!("docker stack config -c {file_args}",);
|
||||
let span = info_span!("GetStackConfig", command);
|
||||
let Some(config_log) = run_komodo_command_with_sanitization(
|
||||
"Stack Config",
|
||||
run_directory.as_path(),
|
||||
command,
|
||||
KomodoCommandMode::Standard,
|
||||
&replacers,
|
||||
)
|
||||
.instrument(span)
|
||||
.await
|
||||
else {
|
||||
// Only reachable if command is empty,
|
||||
// not the case since it is provided above.
|
||||
unreachable!()
|
||||
};
|
||||
if !config_log.success {
|
||||
res.logs.push(config_log);
|
||||
return Ok(res);
|
||||
}
|
||||
let compose =
|
||||
serde_yaml_ng::from_str::<ComposeFile>(&config_log.stdout)
|
||||
.context("Failed to parse compose contents")?;
|
||||
// Store sanitized stack config output
|
||||
res.merged_config = Some(config_log.stdout);
|
||||
for (service_name, ComposeService { image, .. }) in
|
||||
compose.services
|
||||
{
|
||||
let image = image.unwrap_or_default();
|
||||
res.services.push(StackServiceNames {
|
||||
container_name: format!("{project_name}-{service_name}"),
|
||||
service_name,
|
||||
image,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if stack.config.destroy_before_deploy
|
||||
// Also check if project name changed, which also requires taking down.
|
||||
|| last_project_name != project_name
|
||||
{
|
||||
// Take down the existing stack.
|
||||
// This one tries to use the previously deployed project name, to ensure the right stack is taken down.
|
||||
remove_stack(&last_project_name, &mut res)
|
||||
.await
|
||||
.context("Failed to destroy existing stack")?;
|
||||
}
|
||||
|
||||
// Run stack deploy
|
||||
let mut command =
|
||||
format!("docker stack deploy --detach=false -c {file_args}");
|
||||
if use_with_registry_auth {
|
||||
command += " --with-registry-auth";
|
||||
}
|
||||
push_extra_args(&mut command, &stack.config.extra_args)?;
|
||||
|
||||
// Apply compose cmd wrapper if configured
|
||||
let compose_cmd_wrapper =
|
||||
parse_multiline_command(&stack.config.compose_cmd_wrapper);
|
||||
if !compose_cmd_wrapper.is_empty() {
|
||||
if !compose_cmd_wrapper.contains("[[COMPOSE_COMMAND]]") {
|
||||
res.logs.push(Log::error(
|
||||
"Compose Command Wrapper",
|
||||
"compose_cmd_wrapper is configured but does not contain [[COMPOSE_COMMAND]] placeholder. The placeholder is required to inject the compose command.".to_string(),
|
||||
));
|
||||
return Ok(res);
|
||||
}
|
||||
command =
|
||||
compose_cmd_wrapper.replace("[[COMPOSE_COMMAND]]", &command);
|
||||
}
|
||||
|
||||
let span = info_span!("ExecuteStackDeploy");
|
||||
let Some(log) = run_komodo_command_with_sanitization(
|
||||
"Stack Deploy",
|
||||
run_directory.as_path(),
|
||||
command,
|
||||
KomodoCommandMode::Shell,
|
||||
&replacers,
|
||||
)
|
||||
.instrument(span)
|
||||
.await
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
res.deployed = log.success;
|
||||
res.logs.push(log);
|
||||
|
||||
if res.deployed && !stack.config.post_deploy.is_none() {
|
||||
let post_deploy_path =
|
||||
run_directory.join(&stack.config.post_deploy.path);
|
||||
let span = info_span!("ExecutePostDeploy");
|
||||
if let Some(log) = run_komodo_command_with_sanitization(
|
||||
"Post Deploy",
|
||||
post_deploy_path.as_path(),
|
||||
&stack.config.post_deploy.command,
|
||||
KomodoCommandMode::Multiline,
|
||||
&replacers,
|
||||
)
|
||||
.instrument(span)
|
||||
.await
|
||||
{
|
||||
res.logs.push(log);
|
||||
};
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument("RemoveStack", skip(res))]
|
||||
async fn remove_stack(
|
||||
stack: &str,
|
||||
res: &mut DeployStackResponse,
|
||||
) -> anyhow::Result<()> {
|
||||
let log = run_komodo_standard_command(
|
||||
"Remove Stack",
|
||||
None,
|
||||
format!("docker stack rm --detach=false {stack}"),
|
||||
)
|
||||
.await;
|
||||
let success = log.success;
|
||||
res.logs.push(log);
|
||||
if !success {
|
||||
return Err(anyhow!(
|
||||
"Failed to remove existing stack with docker stack rm. Stopping run."
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -25,10 +25,10 @@ use crate::{
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for ListTerminals {
|
||||
impl Resolve<crate::api::Args> for ListTerminals {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &super::Args,
|
||||
_: &crate::api::Args,
|
||||
) -> anyhow::Result<Vec<Terminal>> {
|
||||
clean_up_terminals().await;
|
||||
Ok(list_terminals(self.target.as_ref()).await)
|
||||
@@ -37,7 +37,7 @@ impl Resolve<super::Args> for ListTerminals {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for CreateServerTerminal {
|
||||
impl Resolve<crate::api::Args> for CreateServerTerminal {
|
||||
#[instrument(
|
||||
"CreateServerTerminal",
|
||||
skip_all,
|
||||
@@ -51,7 +51,7 @@ impl Resolve<super::Args> for CreateServerTerminal {
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &super::Args,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<NoData> {
|
||||
if periphery_config().disable_terminals {
|
||||
return Err(anyhow!(
|
||||
@@ -71,7 +71,7 @@ impl Resolve<super::Args> for CreateServerTerminal {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for CreateContainerExecTerminal {
|
||||
impl Resolve<crate::api::Args> for CreateContainerExecTerminal {
|
||||
#[instrument(
|
||||
"CreateContainerExecTerminal",
|
||||
skip_all,
|
||||
@@ -86,7 +86,7 @@ impl Resolve<super::Args> for CreateContainerExecTerminal {
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &super::Args,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<NoData> {
|
||||
if periphery_config().disable_container_terminals {
|
||||
return Err(anyhow!(
|
||||
@@ -119,7 +119,7 @@ impl Resolve<super::Args> for CreateContainerExecTerminal {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for CreateContainerAttachTerminal {
|
||||
impl Resolve<crate::api::Args> for CreateContainerAttachTerminal {
|
||||
#[instrument(
|
||||
"CreateContainerAttachTerminal",
|
||||
skip_all,
|
||||
@@ -133,7 +133,7 @@ impl Resolve<super::Args> for CreateContainerAttachTerminal {
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &super::Args,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<NoData> {
|
||||
if periphery_config().disable_container_terminals {
|
||||
return Err(anyhow!(
|
||||
@@ -164,7 +164,7 @@ impl Resolve<super::Args> for CreateContainerAttachTerminal {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for DeleteTerminal {
|
||||
impl Resolve<crate::api::Args> for DeleteTerminal {
|
||||
#[instrument(
|
||||
"DeleteTerminal",
|
||||
skip_all,
|
||||
@@ -176,7 +176,7 @@ impl Resolve<super::Args> for DeleteTerminal {
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &super::Args,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<NoData> {
|
||||
delete_terminal(&self.target, &self.terminal).await;
|
||||
Ok(NoData {})
|
||||
@@ -185,7 +185,7 @@ impl Resolve<super::Args> for DeleteTerminal {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for DeleteAllTerminals {
|
||||
impl Resolve<crate::api::Args> for DeleteAllTerminals {
|
||||
#[instrument(
|
||||
"DeleteAllTerminals",
|
||||
skip_all,
|
||||
@@ -196,7 +196,7 @@ impl Resolve<super::Args> for DeleteAllTerminals {
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &super::Args,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<NoData> {
|
||||
delete_all_terminals().await;
|
||||
Ok(NoData {})
|
||||
@@ -205,7 +205,7 @@ impl Resolve<super::Args> for DeleteAllTerminals {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for ConnectTerminal {
|
||||
impl Resolve<crate::api::Args> for ConnectTerminal {
|
||||
#[instrument(
|
||||
"ConnectTerminal",
|
||||
skip_all,
|
||||
@@ -215,7 +215,10 @@ impl Resolve<super::Args> for ConnectTerminal {
|
||||
terminal = self.terminal,
|
||||
)
|
||||
)]
|
||||
async fn resolve(self, args: &super::Args) -> anyhow::Result<Uuid> {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Uuid> {
|
||||
let connection =
|
||||
core_connections().get(&args.core).await.with_context(
|
||||
|| format!("Failed to find channel for {}", args.core),
|
||||
@@ -234,7 +237,7 @@ impl Resolve<super::Args> for ConnectTerminal {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for DisconnectTerminal {
|
||||
impl Resolve<crate::api::Args> for DisconnectTerminal {
|
||||
#[instrument(
|
||||
"DisconnectTerminal",
|
||||
skip_all,
|
||||
@@ -246,7 +249,7 @@ impl Resolve<super::Args> for DisconnectTerminal {
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &super::Args,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<NoData> {
|
||||
terminal_channels().remove(&self.channel).await;
|
||||
Ok(NoData {})
|
||||
@@ -255,7 +258,7 @@ impl Resolve<super::Args> for DisconnectTerminal {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<super::Args> for ExecuteTerminal {
|
||||
impl Resolve<crate::api::Args> for ExecuteTerminal {
|
||||
#[instrument(
|
||||
"ExecuteTerminal",
|
||||
skip_all,
|
||||
@@ -266,7 +269,10 @@ impl Resolve<super::Args> for ExecuteTerminal {
|
||||
command = self.command,
|
||||
)
|
||||
)]
|
||||
async fn resolve(self, args: &super::Args) -> anyhow::Result<Uuid> {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> anyhow::Result<Uuid> {
|
||||
let channel =
|
||||
core_connections().get(&args.core).await.with_context(
|
||||
|| format!("Failed to find channel for {}", args.core),
|
||||
|
||||
@@ -29,7 +29,7 @@ pub async fn list_compose_projects()
|
||||
)));
|
||||
}
|
||||
|
||||
let res =
|
||||
let mut res =
|
||||
serde_json::from_str::<Vec<DockerComposeLsItem>>(&res.stdout)
|
||||
.with_context(|| res.stdout.clone())
|
||||
.with_context(|| {
|
||||
@@ -48,7 +48,11 @@ pub async fn list_compose_projects()
|
||||
.map(str::to_string)
|
||||
.collect(),
|
||||
})
|
||||
.collect();
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
res.sort_by(|a, b| {
|
||||
a.status.cmp(&b.status).then_with(|| a.name.cmp(&b.name))
|
||||
});
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
@@ -20,11 +20,18 @@ pub async fn list_swarm_configs()
|
||||
}
|
||||
|
||||
// The output is in JSONL, need to convert to standard JSON vec.
|
||||
serde_json::from_str(&format!(
|
||||
"[{}]",
|
||||
res.stdout.trim().replace('\n', ",")
|
||||
))
|
||||
.context("Failed to parse 'docker config ls' response from json")
|
||||
let mut res = serde_json::from_str::<Vec<SwarmConfigListItem>>(
|
||||
&format!("[{}]", res.stdout.trim().replace('\n', ",")),
|
||||
)
|
||||
.context("Failed to parse 'docker config ls' response from json")?;
|
||||
|
||||
res.sort_by(|a, b| {
|
||||
a.name
|
||||
.cmp(&b.name)
|
||||
.then_with(|| b.updated_at.cmp(&a.updated_at))
|
||||
});
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn inspect_swarm_config(
|
||||
|
||||
@@ -100,6 +100,9 @@ impl DockerClient {
|
||||
container.network_mode =
|
||||
container_id_to_network.get(container_id).cloned();
|
||||
});
|
||||
containers.sort_by(|a, b| {
|
||||
a.state.cmp(&b.state).then_with(|| a.name.cmp(&b.name))
|
||||
});
|
||||
Ok(containers)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ impl DockerClient {
|
||||
&self,
|
||||
containers: &[ContainerListItem],
|
||||
) -> anyhow::Result<Vec<ImageListItem>> {
|
||||
let images = self
|
||||
let mut images = self
|
||||
.docker
|
||||
.list_images(Option::<ListImagesOptions>::None)
|
||||
.await?
|
||||
@@ -37,7 +37,12 @@ impl DockerClient {
|
||||
in_use,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
images.sort_by(|a, b| {
|
||||
a.in_use.cmp(&b.in_use).then_with(|| a.name.cmp(&b.name))
|
||||
});
|
||||
|
||||
Ok(images)
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ impl DockerClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether build result should be pushed after build
|
||||
/// Returns whether login was actually performed.
|
||||
#[instrument("DockerLogin", skip(registry_token))]
|
||||
pub async fn docker_login(
|
||||
domain: &str,
|
||||
@@ -45,30 +45,33 @@ pub async fn docker_login(
|
||||
if domain.is_empty() || account.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let registry_token = match registry_token {
|
||||
Some(token) => token,
|
||||
None => crate::helpers::registry_token(domain, account)?,
|
||||
};
|
||||
|
||||
let log = run_shell_command(&format!(
|
||||
"echo {registry_token} | docker login {domain} --username '{account}' --password-stdin",
|
||||
), None)
|
||||
.await;
|
||||
|
||||
if log.success() {
|
||||
Ok(true)
|
||||
} else {
|
||||
let mut e = anyhow!("End of trace");
|
||||
for line in
|
||||
log.stderr.split('\n').filter(|line| !line.is_empty()).rev()
|
||||
{
|
||||
e = e.context(line.to_string());
|
||||
}
|
||||
for line in
|
||||
log.stdout.split('\n').filter(|line| !line.is_empty()).rev()
|
||||
{
|
||||
e = e.context(line.to_string());
|
||||
}
|
||||
Err(e.context(format!("Registry {domain} login error")))
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let mut e = anyhow!("End of trace");
|
||||
for line in
|
||||
log.stderr.split('\n').filter(|line| !line.is_empty()).rev()
|
||||
{
|
||||
e = e.context(line.to_string());
|
||||
}
|
||||
for line in
|
||||
log.stdout.split('\n').filter(|line| !line.is_empty()).rev()
|
||||
{
|
||||
e = e.context(line.to_string());
|
||||
}
|
||||
Err(e.context(format!("Registry {domain} login error")))
|
||||
}
|
||||
|
||||
#[instrument("PullImage")]
|
||||
|
||||
@@ -12,7 +12,7 @@ impl DockerClient {
|
||||
&self,
|
||||
containers: &[ContainerListItem],
|
||||
) -> anyhow::Result<Vec<NetworkListItem>> {
|
||||
let networks = self
|
||||
let mut networks = self
|
||||
.docker
|
||||
.list_networks(Option::<ListNetworksOptions>::None)
|
||||
.await?
|
||||
@@ -54,7 +54,12 @@ impl DockerClient {
|
||||
in_use,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
networks.sort_by(|a, b| {
|
||||
a.in_use.cmp(&b.in_use).then_with(|| a.name.cmp(&b.name))
|
||||
});
|
||||
|
||||
Ok(networks)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,14 +11,22 @@ impl DockerClient {
|
||||
pub async fn list_swarm_nodes(
|
||||
&self,
|
||||
) -> anyhow::Result<Vec<SwarmNodeListItem>> {
|
||||
let nodes = self
|
||||
let mut nodes = self
|
||||
.docker
|
||||
.list_nodes(Option::<ListNodesOptions>::None)
|
||||
.await
|
||||
.context("Failed to query for swarm node list")?
|
||||
.into_iter()
|
||||
.map(convert_node_list_item)
|
||||
.collect();
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
nodes.sort_by(|a, b| {
|
||||
a.state
|
||||
.cmp(&b.state)
|
||||
.then_with(|| a.name.cmp(&b.name))
|
||||
.then_with(|| a.hostname.cmp(&b.hostname))
|
||||
});
|
||||
|
||||
Ok(nodes)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,14 +10,21 @@ impl DockerClient {
|
||||
pub async fn list_swarm_secrets(
|
||||
&self,
|
||||
) -> anyhow::Result<Vec<SwarmSecretListItem>> {
|
||||
let secrets = self
|
||||
let mut secrets = self
|
||||
.docker
|
||||
.list_secrets(Option::<ListSecretsOptions>::None)
|
||||
.await
|
||||
.context("Failed to query for swarm secret list")?
|
||||
.into_iter()
|
||||
.map(convert_secret_list_item)
|
||||
.collect();
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
secrets.sort_by(|a, b| {
|
||||
a.name
|
||||
.cmp(&b.name)
|
||||
.then_with(|| b.updated_at.cmp(&a.updated_at))
|
||||
});
|
||||
|
||||
Ok(secrets)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,14 +13,21 @@ impl DockerClient {
|
||||
pub async fn list_swarm_services(
|
||||
&self,
|
||||
) -> anyhow::Result<Vec<SwarmServiceListItem>> {
|
||||
let services = self
|
||||
let mut services = self
|
||||
.docker
|
||||
.list_services(Option::<ListServicesOptions>::None)
|
||||
.await
|
||||
.context("Failed to query for swarm service list")?
|
||||
.into_iter()
|
||||
.map(convert_service_list_item)
|
||||
.collect();
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
services.sort_by(|a, b| {
|
||||
a.name
|
||||
.cmp(&b.name)
|
||||
.then_with(|| b.updated_at.cmp(&a.updated_at))
|
||||
});
|
||||
|
||||
Ok(services)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
use anyhow::{Context, anyhow};
|
||||
use command::run_komodo_standard_command;
|
||||
use komodo_client::entities::docker::stack::{
|
||||
SwarmStackListItem, SwarmStackLists, SwarmStackServiceListItem,
|
||||
SwarmStackTaskListItem,
|
||||
use futures_util::{StreamExt, stream::FuturesOrdered};
|
||||
use komodo_client::entities::{
|
||||
docker::stack::{
|
||||
SwarmStack, SwarmStackListItem, SwarmStackServiceListItem,
|
||||
SwarmStackTaskListItem,
|
||||
},
|
||||
swarm::SwarmState,
|
||||
};
|
||||
|
||||
pub async fn inspect_swarm_stack(
|
||||
name: String,
|
||||
) -> anyhow::Result<SwarmStackLists> {
|
||||
let (services, tasks) = tokio::try_join!(
|
||||
list_swarm_stack_services(&name),
|
||||
) -> anyhow::Result<SwarmStack> {
|
||||
let (tasks, services) = tokio::try_join!(
|
||||
list_swarm_stack_tasks(&name),
|
||||
list_swarm_stack_services(&name)
|
||||
)?;
|
||||
Ok(SwarmStackLists {
|
||||
let state = state_from_tasks(&tasks);
|
||||
Ok(SwarmStack {
|
||||
name,
|
||||
state,
|
||||
services,
|
||||
tasks,
|
||||
})
|
||||
@@ -35,11 +41,35 @@ pub async fn list_swarm_stacks()
|
||||
}
|
||||
|
||||
// The output is in JSONL, need to convert to standard JSON vec.
|
||||
serde_json::from_str(&format!(
|
||||
"[{}]",
|
||||
res.stdout.trim().replace('\n', ",")
|
||||
))
|
||||
.context("Failed to parse 'docker stack ls' response from json")
|
||||
let mut stacks = serde_json::from_str::<Vec<SwarmStackListItem>>(
|
||||
&format!("[{}]", res.stdout.trim().replace('\n', ",")),
|
||||
)
|
||||
.context("Failed to parse 'docker stack ls' response from json")?
|
||||
// Attach state concurrently from tasks. Still include stack
|
||||
// if it fails, just with None state.
|
||||
.into_iter()
|
||||
.map(|mut stack| async move {
|
||||
let res = async {
|
||||
let tasks =
|
||||
list_swarm_stack_tasks(stack.name.as_ref()?).await.ok()?;
|
||||
Some(state_from_tasks(&tasks))
|
||||
}
|
||||
.await;
|
||||
if let Some(state) = res {
|
||||
stack.state = Some(state);
|
||||
}
|
||||
stack
|
||||
})
|
||||
.collect::<FuturesOrdered<_>>()
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
|
||||
stacks.sort_by(|a, b| {
|
||||
cmp_option(a.state, b.state)
|
||||
.then_with(|| cmp_option(a.name.as_ref(), b.name.as_ref()))
|
||||
});
|
||||
|
||||
Ok(stacks)
|
||||
}
|
||||
|
||||
pub async fn list_swarm_stack_services(
|
||||
@@ -59,13 +89,18 @@ pub async fn list_swarm_stack_services(
|
||||
}
|
||||
|
||||
// The output is in JSONL, need to convert to standard JSON vec.
|
||||
serde_json::from_str(&format!(
|
||||
"[{}]",
|
||||
res.stdout.trim().replace('\n', ",")
|
||||
))
|
||||
.context(
|
||||
"Failed to parse 'docker stack services' response from json",
|
||||
)
|
||||
let mut services =
|
||||
serde_json::from_str::<Vec<SwarmStackServiceListItem>>(&format!(
|
||||
"[{}]",
|
||||
res.stdout.trim().replace('\n', ",")
|
||||
))
|
||||
.context(
|
||||
"Failed to parse 'docker stack services' response from json",
|
||||
)?;
|
||||
|
||||
services.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
|
||||
Ok(services)
|
||||
}
|
||||
|
||||
pub async fn list_swarm_stack_tasks(
|
||||
@@ -74,7 +109,7 @@ pub async fn list_swarm_stack_tasks(
|
||||
let res = run_komodo_standard_command(
|
||||
"List Swarm Stack Tasks",
|
||||
None,
|
||||
format!("docker stack ps --format json {stack}"),
|
||||
format!("docker stack ps --format json --no-trunc {stack}"),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -85,9 +120,53 @@ pub async fn list_swarm_stack_tasks(
|
||||
}
|
||||
|
||||
// The output is in JSONL, need to convert to standard JSON vec.
|
||||
serde_json::from_str(&format!(
|
||||
"[{}]",
|
||||
res.stdout.trim().replace('\n', ",")
|
||||
))
|
||||
.context("Failed to parse 'docker stack ps' response from json")
|
||||
let mut tasks =
|
||||
serde_json::from_str::<Vec<SwarmStackTaskListItem>>(&format!(
|
||||
"[{}]",
|
||||
res.stdout.trim().replace('\n', ",")
|
||||
))
|
||||
.context(
|
||||
"Failed to parse 'docker stack ps' response from json",
|
||||
)?;
|
||||
|
||||
tasks.sort_by(|a, b| {
|
||||
a.desired_state
|
||||
.cmp(&b.desired_state)
|
||||
.then_with(|| a.name.cmp(&b.name))
|
||||
});
|
||||
|
||||
Ok(tasks)
|
||||
}
|
||||
|
||||
pub fn state_from_tasks<'a>(
|
||||
tasks: impl IntoIterator<Item = &'a SwarmStackTaskListItem>,
|
||||
) -> SwarmState {
|
||||
for task in tasks {
|
||||
let (Some(current), Some(desired)) =
|
||||
(&task.current_state, &task.desired_state)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
// CurrentState example: 'Running 44 minutes ago'.
|
||||
// Only want first "word"
|
||||
let Some(current) = current.split(" ").next() else {
|
||||
continue;
|
||||
};
|
||||
if current != desired {
|
||||
return SwarmState::Unhealthy;
|
||||
}
|
||||
}
|
||||
SwarmState::Healthy
|
||||
}
|
||||
|
||||
fn cmp_option<T: Ord>(
|
||||
a: Option<T>,
|
||||
b: Option<T>,
|
||||
) -> std::cmp::Ordering {
|
||||
match (a, b) {
|
||||
(Some(a), Some(b)) => a.cmp(&b),
|
||||
(Some(_), None) => std::cmp::Ordering::Less,
|
||||
(None, Some(_)) => std::cmp::Ordering::Greater,
|
||||
(None, None) => std::cmp::Ordering::Equal,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,14 +8,19 @@ impl DockerClient {
|
||||
pub async fn list_swarm_tasks(
|
||||
&self,
|
||||
) -> anyhow::Result<Vec<SwarmTaskListItem>> {
|
||||
let tasks = self
|
||||
let mut tasks = self
|
||||
.docker
|
||||
.list_tasks(Option::<ListTasksOptions>::None)
|
||||
.await
|
||||
.context("Failed to query for swarm tasks list")?
|
||||
.into_iter()
|
||||
.map(convert_task_list_item)
|
||||
.collect();
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
tasks.sort_by(|a, b| {
|
||||
a.state.cmp(&b.state).then_with(|| a.name.cmp(&b.name))
|
||||
});
|
||||
|
||||
Ok(tasks)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ impl DockerClient {
|
||||
&self,
|
||||
containers: &[ContainerListItem],
|
||||
) -> anyhow::Result<Vec<VolumeListItem>> {
|
||||
let volumes = self
|
||||
let mut volumes = self
|
||||
.docker
|
||||
.list_volumes(Option::<ListVolumesOptions>::None)
|
||||
.await?
|
||||
@@ -45,7 +45,12 @@ impl DockerClient {
|
||||
in_use,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
volumes.sort_by(|a, b| {
|
||||
a.in_use.cmp(&b.in_use).then_with(|| a.name.cmp(&b.name))
|
||||
});
|
||||
|
||||
Ok(volumes)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{
|
||||
net::IpAddr, path::PathBuf, str::FromStr as _, sync::OnceLock,
|
||||
time::Duration,
|
||||
fmt::Write, net::IpAddr, path::PathBuf, str::FromStr as _,
|
||||
sync::OnceLock, time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
@@ -14,6 +14,7 @@ use komodo_client::{
|
||||
entities::{
|
||||
EnvironmentVar, RepoExecutionArgs, RepoExecutionResponse,
|
||||
SearchCombinator, SystemCommand, all_logs_success,
|
||||
deployment::Conversion,
|
||||
},
|
||||
parsers::QUOTE_PATTERN,
|
||||
};
|
||||
@@ -34,6 +35,17 @@ pub fn format_extra_args(extra_args: &[String]) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_extra_args(
|
||||
command: &mut String,
|
||||
extra_args: &[String],
|
||||
) -> anyhow::Result<()> {
|
||||
for arg in extra_args {
|
||||
write!(command, " {arg}")
|
||||
.context("Failed to write extra args to command")?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn format_labels(labels: &[EnvironmentVar]) -> String {
|
||||
labels
|
||||
.iter()
|
||||
@@ -51,6 +63,56 @@ pub fn format_labels(labels: &[EnvironmentVar]) -> String {
|
||||
.join("")
|
||||
}
|
||||
|
||||
pub fn push_labels(
|
||||
command: &mut String,
|
||||
labels: &[EnvironmentVar],
|
||||
) -> anyhow::Result<()> {
|
||||
for label in labels {
|
||||
if label.value.starts_with(QUOTE_PATTERN)
|
||||
&& label.value.ends_with(QUOTE_PATTERN)
|
||||
{
|
||||
write!(command, " --label {}={}", label.variable, label.value)
|
||||
} else {
|
||||
write!(
|
||||
command,
|
||||
" --label {}=\"{}\"",
|
||||
label.variable, label.value
|
||||
)
|
||||
}
|
||||
.context("Failed to write labels to command")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn push_conversions(
|
||||
command: &mut String,
|
||||
conversions: &[Conversion],
|
||||
flag: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
for Conversion { local, container } in conversions {
|
||||
write!(command, " {flag} {local}:{container}")
|
||||
.context("Failed to format conversions")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn push_environment(
|
||||
command: &mut String,
|
||||
environment: &[EnvironmentVar],
|
||||
) -> anyhow::Result<()> {
|
||||
for EnvironmentVar { variable, value } in environment {
|
||||
if value.starts_with(QUOTE_PATTERN)
|
||||
&& value.ends_with(QUOTE_PATTERN)
|
||||
{
|
||||
write!(command, " --env {variable}={value}")
|
||||
} else {
|
||||
write!(command, " --env {variable}=\"{value}\"")
|
||||
}
|
||||
.context("Failed to format environment")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn format_log_grep(
|
||||
terms: &[String],
|
||||
combinator: SearchCombinator,
|
||||
|
||||
@@ -15,6 +15,7 @@ mod config;
|
||||
mod connection;
|
||||
mod docker;
|
||||
mod helpers;
|
||||
mod stack;
|
||||
mod state;
|
||||
mod stats;
|
||||
mod terminal;
|
||||
|
||||
@@ -1,29 +1,26 @@
|
||||
use std::{
|
||||
fmt::Write,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
//! Module to handle common parts of deploying Compose and Swarm Stacks.
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use command::run_komodo_standard_command;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use formatting::format_serror;
|
||||
use komodo_client::entities::{
|
||||
FileContents, RepoExecutionArgs,
|
||||
repo::Repo,
|
||||
stack::{AdditionalEnvFile, Stack, StackRemoteFileContents},
|
||||
stack::{Stack, StackRemoteFileContents},
|
||||
to_path_compatible_name,
|
||||
update::Log,
|
||||
};
|
||||
use periphery_client::api::{
|
||||
compose::ComposeUpResponse, git::PullOrCloneRepo,
|
||||
DeployStackResponse, git::PullOrCloneRepo,
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use tokio::fs;
|
||||
use resolver_api::Resolve as _;
|
||||
|
||||
use crate::{
|
||||
api::Args, config::periphery_config, docker::docker_login,
|
||||
};
|
||||
|
||||
use super::docker_compose;
|
||||
pub mod write;
|
||||
|
||||
#[instrument(
|
||||
"MaybeLoginRegistry",
|
||||
@@ -34,10 +31,11 @@ pub async fn maybe_login_registry(
|
||||
stack: &Stack,
|
||||
registry_token: Option<String>,
|
||||
logs: &mut Vec<Log>,
|
||||
) {
|
||||
) -> bool {
|
||||
if !stack.config.registry_provider.is_empty()
|
||||
&& !stack.config.registry_account.is_empty()
|
||||
&& let Err(e) = docker_login(
|
||||
{
|
||||
if let Err(e) = docker_login(
|
||||
&stack.config.registry_provider,
|
||||
&stack.config.registry_account,
|
||||
registry_token.as_deref(),
|
||||
@@ -50,68 +48,16 @@ pub async fn maybe_login_registry(
|
||||
)
|
||||
})
|
||||
.context("Failed to login to image registry")
|
||||
{
|
||||
logs.push(Log::error(
|
||||
"Login to Registry",
|
||||
format_serror(&e.into()),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn env_file_args(
|
||||
env_file_path: Option<&str>,
|
||||
additional_env_files: &[AdditionalEnvFile],
|
||||
) -> anyhow::Result<String> {
|
||||
let mut res = String::new();
|
||||
|
||||
// Add additional env files (except komodo's own, which comes last)
|
||||
for file in additional_env_files
|
||||
.iter()
|
||||
.filter(|f| env_file_path != Some(f.path.as_str()))
|
||||
{
|
||||
let path = &file.path;
|
||||
write!(res, " --env-file {path}").with_context(|| {
|
||||
format!("Failed to write --env-file arg for {path}")
|
||||
})?;
|
||||
}
|
||||
|
||||
// Add komodo's env file last for highest priority
|
||||
if let Some(file) = env_file_path {
|
||||
write!(res, " --env-file {file}").with_context(|| {
|
||||
format!("Failed to write --env-file arg for {file}")
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[instrument("ComposeDown", skip(res))]
|
||||
pub async fn compose_down(
|
||||
project: &str,
|
||||
services: &[String],
|
||||
res: &mut ComposeUpResponse,
|
||||
) -> anyhow::Result<()> {
|
||||
let docker_compose = docker_compose();
|
||||
let service_args = if services.is_empty() {
|
||||
String::new()
|
||||
{
|
||||
logs.push(Log::error(
|
||||
"Login to Registry",
|
||||
format_serror(&e.into()),
|
||||
));
|
||||
}
|
||||
true
|
||||
} else {
|
||||
format!(" {}", services.join(" "))
|
||||
};
|
||||
let log = run_komodo_standard_command(
|
||||
"Compose Down",
|
||||
None,
|
||||
format!("{docker_compose} -p {project} down{service_args}"),
|
||||
)
|
||||
.await;
|
||||
let success = log.success;
|
||||
res.logs.push(log);
|
||||
if !success {
|
||||
return Err(anyhow!(
|
||||
"Failed to bring down existing container(s) with docker compose down. Stopping run."
|
||||
));
|
||||
false
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Only for git repo based Stacks.
|
||||
@@ -191,7 +137,7 @@ pub async fn pull_or_clone_stack(
|
||||
pub async fn validate_files(
|
||||
stack: &Stack,
|
||||
run_directory: &Path,
|
||||
res: &mut ComposeUpResponse,
|
||||
res: &mut DeployStackResponse,
|
||||
) {
|
||||
let file_paths = stack
|
||||
.all_file_dependencies()
|
||||
@@ -231,9 +177,9 @@ pub async fn validate_files(
|
||||
|
||||
for (full_path, file) in file_paths {
|
||||
let file_contents =
|
||||
match fs::read_to_string(&full_path).await.with_context(|| {
|
||||
format!("Failed to read file contents at {full_path:?}")
|
||||
}) {
|
||||
match tokio::fs::read_to_string(&full_path).await.with_context(
|
||||
|| format!("Failed to read file contents at {full_path:?}"),
|
||||
) {
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
let error = format_serror(&e.into());
|
||||
@@ -7,9 +7,8 @@ use komodo_client::entities::{
|
||||
stack::Stack, to_path_compatible_name, update::Log,
|
||||
};
|
||||
use periphery_client::api::{
|
||||
compose::{
|
||||
ComposePullResponse, ComposeRunResponse, ComposeUpResponse,
|
||||
},
|
||||
DeployStackResponse,
|
||||
compose::{ComposePullResponse, ComposeRunResponse},
|
||||
git::{CloneRepo, PullOrCloneRepo},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
@@ -24,7 +23,7 @@ pub trait WriteStackRes {
|
||||
fn set_commit_message(&mut self, _message: Option<String>) {}
|
||||
}
|
||||
|
||||
impl WriteStackRes for &mut ComposeUpResponse {
|
||||
impl WriteStackRes for &mut DeployStackResponse {
|
||||
fn logs(&mut self) -> &mut Vec<Log> {
|
||||
&mut self.logs
|
||||
}
|
||||
@@ -62,7 +61,7 @@ impl WriteStackRes for &mut ComposeRunResponse {
|
||||
repo = repo.as_ref().map(|repo| &repo.name),
|
||||
)
|
||||
)]
|
||||
pub async fn stack<'a>(
|
||||
pub async fn write_stack<'a>(
|
||||
stack: &'a Stack,
|
||||
repo: Option<&Repo>,
|
||||
git_token: Option<String>,
|
||||
@@ -14,6 +14,7 @@ mod procedure;
|
||||
mod repo;
|
||||
mod server;
|
||||
mod stack;
|
||||
mod swarm;
|
||||
mod sync;
|
||||
|
||||
pub use action::*;
|
||||
@@ -25,6 +26,7 @@ pub use procedure::*;
|
||||
pub use repo::*;
|
||||
pub use server::*;
|
||||
pub use stack::*;
|
||||
pub use swarm::*;
|
||||
pub use sync::*;
|
||||
|
||||
use crate::{
|
||||
@@ -59,83 +61,6 @@ pub enum Execution {
|
||||
/// The "null" execution. Does nothing.
|
||||
None(NoData),
|
||||
|
||||
// ACTION
|
||||
/// Run the target action. (alias: `action`, `ac`)
|
||||
#[clap(alias = "action", alias = "ac")]
|
||||
RunAction(RunAction),
|
||||
BatchRunAction(BatchRunAction),
|
||||
|
||||
// PROCEDURE
|
||||
/// Run the target procedure. (alias: `procedure`, `pr`)
|
||||
#[clap(alias = "procedure", alias = "pr")]
|
||||
RunProcedure(RunProcedure),
|
||||
BatchRunProcedure(BatchRunProcedure),
|
||||
|
||||
// BUILD
|
||||
/// Run the target build. (alias: `build`, `bd`)
|
||||
#[clap(alias = "build", alias = "bd")]
|
||||
RunBuild(RunBuild),
|
||||
BatchRunBuild(BatchRunBuild),
|
||||
CancelBuild(CancelBuild),
|
||||
|
||||
// DEPLOYMENT
|
||||
/// Deploy the target deployment. (alias: `dp`)
|
||||
#[clap(alias = "dp")]
|
||||
Deploy(Deploy),
|
||||
BatchDeploy(BatchDeploy),
|
||||
PullDeployment(PullDeployment),
|
||||
StartDeployment(StartDeployment),
|
||||
RestartDeployment(RestartDeployment),
|
||||
PauseDeployment(PauseDeployment),
|
||||
UnpauseDeployment(UnpauseDeployment),
|
||||
StopDeployment(StopDeployment),
|
||||
DestroyDeployment(DestroyDeployment),
|
||||
BatchDestroyDeployment(BatchDestroyDeployment),
|
||||
|
||||
// REPO
|
||||
/// Clone the target repo
|
||||
#[clap(alias = "clone")]
|
||||
CloneRepo(CloneRepo),
|
||||
BatchCloneRepo(BatchCloneRepo),
|
||||
PullRepo(PullRepo),
|
||||
BatchPullRepo(BatchPullRepo),
|
||||
BuildRepo(BuildRepo),
|
||||
BatchBuildRepo(BatchBuildRepo),
|
||||
CancelRepoBuild(CancelRepoBuild),
|
||||
|
||||
// SERVER (Container)
|
||||
StartContainer(StartContainer),
|
||||
RestartContainer(RestartContainer),
|
||||
PauseContainer(PauseContainer),
|
||||
UnpauseContainer(UnpauseContainer),
|
||||
StopContainer(StopContainer),
|
||||
DestroyContainer(DestroyContainer),
|
||||
StartAllContainers(StartAllContainers),
|
||||
RestartAllContainers(RestartAllContainers),
|
||||
PauseAllContainers(PauseAllContainers),
|
||||
UnpauseAllContainers(UnpauseAllContainers),
|
||||
StopAllContainers(StopAllContainers),
|
||||
PruneContainers(PruneContainers),
|
||||
|
||||
// SERVER (Prune)
|
||||
DeleteNetwork(DeleteNetwork),
|
||||
PruneNetworks(PruneNetworks),
|
||||
DeleteImage(DeleteImage),
|
||||
PruneImages(PruneImages),
|
||||
DeleteVolume(DeleteVolume),
|
||||
PruneVolumes(PruneVolumes),
|
||||
PruneDockerBuilders(PruneDockerBuilders),
|
||||
PruneBuildx(PruneBuildx),
|
||||
PruneSystem(PruneSystem),
|
||||
|
||||
// SYNC
|
||||
/// Execute a Resource Sync. (alias: `sync`)
|
||||
#[clap(alias = "sync")]
|
||||
RunSync(RunSync),
|
||||
/// Commit a Resource Sync. (alias: `commit`)
|
||||
#[clap(alias = "commit")]
|
||||
CommitSync(CommitSync), // This is a special case, its actually a write operation.
|
||||
|
||||
// STACK
|
||||
/// Deploy the target stack. (alias: `stack`, `st`)
|
||||
#[clap(alias = "stack", alias = "st")]
|
||||
@@ -154,11 +79,93 @@ pub enum Execution {
|
||||
BatchDestroyStack(BatchDestroyStack),
|
||||
RunStackService(RunStackService),
|
||||
|
||||
// DEPLOYMENT
|
||||
/// Deploy the target deployment. (alias: `dp`)
|
||||
#[clap(alias = "dp")]
|
||||
Deploy(Deploy),
|
||||
BatchDeploy(BatchDeploy),
|
||||
PullDeployment(PullDeployment),
|
||||
StartDeployment(StartDeployment),
|
||||
RestartDeployment(RestartDeployment),
|
||||
PauseDeployment(PauseDeployment),
|
||||
UnpauseDeployment(UnpauseDeployment),
|
||||
StopDeployment(StopDeployment),
|
||||
DestroyDeployment(DestroyDeployment),
|
||||
BatchDestroyDeployment(BatchDestroyDeployment),
|
||||
|
||||
// BUILD
|
||||
/// Run the target build. (alias: `build`, `bd`)
|
||||
#[clap(alias = "build", alias = "bd")]
|
||||
RunBuild(RunBuild),
|
||||
BatchRunBuild(BatchRunBuild),
|
||||
CancelBuild(CancelBuild),
|
||||
|
||||
// REPO
|
||||
/// Clone the target repo
|
||||
#[clap(alias = "clone")]
|
||||
CloneRepo(CloneRepo),
|
||||
BatchCloneRepo(BatchCloneRepo),
|
||||
PullRepo(PullRepo),
|
||||
BatchPullRepo(BatchPullRepo),
|
||||
BuildRepo(BuildRepo),
|
||||
BatchBuildRepo(BatchBuildRepo),
|
||||
CancelRepoBuild(CancelRepoBuild),
|
||||
|
||||
// PROCEDURE
|
||||
/// Run the target procedure. (alias: `procedure`, `pr`)
|
||||
#[clap(alias = "procedure", alias = "pr")]
|
||||
RunProcedure(RunProcedure),
|
||||
BatchRunProcedure(BatchRunProcedure),
|
||||
|
||||
// ACTION
|
||||
/// Run the target action. (alias: `action`, `ac`)
|
||||
#[clap(alias = "action", alias = "ac")]
|
||||
RunAction(RunAction),
|
||||
BatchRunAction(BatchRunAction),
|
||||
|
||||
// SYNC
|
||||
/// Execute a Resource Sync. (alias: `sync`)
|
||||
#[clap(alias = "sync")]
|
||||
RunSync(RunSync),
|
||||
/// Commit a Resource Sync. (alias: `commit`)
|
||||
#[clap(alias = "commit")]
|
||||
CommitSync(CommitSync), // This is a special case, its actually a write operation.
|
||||
|
||||
// ALERTER
|
||||
TestAlerter(TestAlerter),
|
||||
#[clap(alias = "alert")]
|
||||
SendAlert(SendAlert),
|
||||
|
||||
// SERVER
|
||||
StartContainer(StartContainer),
|
||||
RestartContainer(RestartContainer),
|
||||
PauseContainer(PauseContainer),
|
||||
UnpauseContainer(UnpauseContainer),
|
||||
StopContainer(StopContainer),
|
||||
DestroyContainer(DestroyContainer),
|
||||
StartAllContainers(StartAllContainers),
|
||||
RestartAllContainers(RestartAllContainers),
|
||||
PauseAllContainers(PauseAllContainers),
|
||||
UnpauseAllContainers(UnpauseAllContainers),
|
||||
StopAllContainers(StopAllContainers),
|
||||
PruneContainers(PruneContainers),
|
||||
DeleteNetwork(DeleteNetwork),
|
||||
PruneNetworks(PruneNetworks),
|
||||
DeleteImage(DeleteImage),
|
||||
PruneImages(PruneImages),
|
||||
DeleteVolume(DeleteVolume),
|
||||
PruneVolumes(PruneVolumes),
|
||||
PruneDockerBuilders(PruneDockerBuilders),
|
||||
PruneBuildx(PruneBuildx),
|
||||
PruneSystem(PruneSystem),
|
||||
|
||||
// SWARM
|
||||
RemoveSwarmNodes(RemoveSwarmNodes),
|
||||
RemoveSwarmStacks(RemoveSwarmStacks),
|
||||
RemoveSwarmServices(RemoveSwarmServices),
|
||||
RemoveSwarmConfigs(RemoveSwarmConfigs),
|
||||
RemoveSwarmSecrets(RemoveSwarmSecrets),
|
||||
|
||||
// MAINTENANCE
|
||||
ClearRepoCache(ClearRepoCache),
|
||||
#[clap(
|
||||
|
||||
@@ -30,6 +30,8 @@ pub struct DeployStack {
|
||||
pub stack: String,
|
||||
/// Filter to only deploy specific services.
|
||||
/// If empty, will deploy all services.
|
||||
///
|
||||
/// Note. For Swarm mode Stacks, this field is not supported and will be ignored.
|
||||
#[serde(default)]
|
||||
pub services: Vec<String>,
|
||||
/// Override the default termination max time.
|
||||
|
||||
161
client/core/rs/src/api/execute/swarm.rs
Normal file
161
client/core/rs/src/api/execute/swarm.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
use clap::Parser;
|
||||
use derive_empty_traits::EmptyTraits;
|
||||
use resolver_api::Resolve;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typeshare::typeshare;
|
||||
|
||||
use crate::{
|
||||
api::execute::KomodoExecuteRequest, entities::update::Update,
|
||||
};
|
||||
|
||||
// ========
|
||||
// = Node =
|
||||
// ========
|
||||
|
||||
/// `docker node rm [OPTIONS] NODE [NODE...]`
|
||||
///
|
||||
/// https://docs.docker.com/reference/cli/docker/node/rm/
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Debug,
|
||||
Clone,
|
||||
PartialEq,
|
||||
Resolve,
|
||||
EmptyTraits,
|
||||
Parser,
|
||||
)]
|
||||
#[empty_traits(KomodoExecuteRequest)]
|
||||
#[response(Update)]
|
||||
#[error(serror::Error)]
|
||||
pub struct RemoveSwarmNodes {
|
||||
/// Name or id
|
||||
pub swarm: String,
|
||||
/// Node names or ids to remove
|
||||
pub nodes: Vec<String>,
|
||||
/// Force remove a node from the swarm
|
||||
#[serde(default)]
|
||||
#[arg(long, short, default_value_t = false)]
|
||||
pub force: bool,
|
||||
}
|
||||
|
||||
// =========
|
||||
// = Stack =
|
||||
// =========
|
||||
|
||||
/// `docker stack rm [OPTIONS] STACK [STACK...]`
|
||||
///
|
||||
/// https://docs.docker.com/reference/cli/docker/stack/rm/
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Debug,
|
||||
Clone,
|
||||
PartialEq,
|
||||
Resolve,
|
||||
EmptyTraits,
|
||||
Parser,
|
||||
)]
|
||||
#[empty_traits(KomodoExecuteRequest)]
|
||||
#[response(Update)]
|
||||
#[error(serror::Error)]
|
||||
pub struct RemoveSwarmStacks {
|
||||
/// Name or id
|
||||
pub swarm: String,
|
||||
/// Node names to remove
|
||||
pub stacks: Vec<String>,
|
||||
/// Do not wait for stack removal
|
||||
#[serde(default = "default_detach")]
|
||||
#[arg(long, short, default_value_t = default_detach())]
|
||||
pub detach: bool,
|
||||
}
|
||||
|
||||
fn default_detach() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
// ===========
|
||||
// = Service =
|
||||
// ===========
|
||||
|
||||
/// `docker service rm SERVICE [SERVICE...]`
|
||||
///
|
||||
/// https://docs.docker.com/reference/cli/docker/service/rm/
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Debug,
|
||||
Clone,
|
||||
PartialEq,
|
||||
Resolve,
|
||||
EmptyTraits,
|
||||
Parser,
|
||||
)]
|
||||
#[empty_traits(KomodoExecuteRequest)]
|
||||
#[response(Update)]
|
||||
#[error(serror::Error)]
|
||||
pub struct RemoveSwarmServices {
|
||||
/// Name or id
|
||||
pub swarm: String,
|
||||
/// Service names or ids
|
||||
pub services: Vec<String>,
|
||||
}
|
||||
|
||||
// ==========
|
||||
// = Config =
|
||||
// ==========
|
||||
|
||||
/// `docker config rm CONFIG [CONFIG...]`
|
||||
///
|
||||
/// https://docs.docker.com/reference/cli/docker/config/rm/
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Debug,
|
||||
Clone,
|
||||
PartialEq,
|
||||
Resolve,
|
||||
EmptyTraits,
|
||||
Parser,
|
||||
)]
|
||||
#[empty_traits(KomodoExecuteRequest)]
|
||||
#[response(Update)]
|
||||
#[error(serror::Error)]
|
||||
pub struct RemoveSwarmConfigs {
|
||||
/// Name or id
|
||||
pub swarm: String,
|
||||
/// Config names or ids
|
||||
pub configs: Vec<String>,
|
||||
}
|
||||
|
||||
// ==========
|
||||
// = Secret =
|
||||
// ==========
|
||||
|
||||
/// `docker secret rm SECRET [SECRET...]`
|
||||
///
|
||||
/// https://docs.docker.com/reference/cli/docker/secret/rm/
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Debug,
|
||||
Clone,
|
||||
PartialEq,
|
||||
Resolve,
|
||||
EmptyTraits,
|
||||
Parser,
|
||||
)]
|
||||
#[empty_traits(KomodoExecuteRequest)]
|
||||
#[response(Update)]
|
||||
#[error(serror::Error)]
|
||||
pub struct RemoveSwarmSecrets {
|
||||
/// Name or id
|
||||
pub swarm: String,
|
||||
/// Secret names or ids
|
||||
pub secrets: Vec<String>,
|
||||
}
|
||||
@@ -10,7 +10,7 @@ use crate::entities::{
|
||||
node::{SwarmNode, SwarmNodeListItem},
|
||||
secret::{SwarmSecret, SwarmSecretListItem},
|
||||
service::{SwarmService, SwarmServiceListItem},
|
||||
stack::{SwarmStackListItem, SwarmStackLists},
|
||||
stack::{SwarmStack, SwarmStackListItem},
|
||||
swarm::SwarmInspectInfo,
|
||||
task::{SwarmTask, SwarmTaskListItem},
|
||||
},
|
||||
@@ -484,4 +484,4 @@ pub struct InspectSwarmStack {
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
pub type InspectSwarmStackResponse = SwarmStackLists;
|
||||
pub type InspectSwarmStackResponse = SwarmStack;
|
||||
|
||||
@@ -5,7 +5,7 @@ use derive_default_builder::DefaultBuilder;
|
||||
use derive_variants::EnumVariants;
|
||||
use partial_derive2::Partial;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{Display, EnumString};
|
||||
use strum::{AsRefStr, Display, EnumString};
|
||||
use typeshare::typeshare;
|
||||
|
||||
use crate::{
|
||||
@@ -58,7 +58,19 @@ pub type _PartialDeploymentConfig = PartialDeploymentConfig;
|
||||
#[partial_derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
#[partial(skip_serializing_none, from, diff)]
|
||||
pub struct DeploymentConfig {
|
||||
/// The id of server the deployment is deployed on.
|
||||
/// The Swarm to deploy the Deployment on (as a Swarm Service), setting the Deployment into Swarm mode.
|
||||
///
|
||||
/// Note. If both swarm_id and server_id are set,
|
||||
/// swarm_id overrides server_id and the Deployment will be in Swarm mode.
|
||||
#[serde(default, alias = "swarm")]
|
||||
#[partial_attr(serde(alias = "swarm"))]
|
||||
#[builder(default)]
|
||||
pub swarm_id: String,
|
||||
|
||||
/// The Server to deploy the Deployment on, setting the Deployment into Container mode.
|
||||
///
|
||||
/// Note. If both swarm_id and server_id are set,
|
||||
/// swarm_id overrides server_id and the Deployment will be in Swarm mode.
|
||||
#[serde(default, alias = "server")]
|
||||
#[partial_attr(serde(alias = "server"))]
|
||||
#[builder(default)]
|
||||
@@ -134,6 +146,14 @@ pub struct DeploymentConfig {
|
||||
#[builder(default)]
|
||||
pub command: String,
|
||||
|
||||
/// The number of replicas for the Service.
|
||||
///
|
||||
/// Note. Only used in Swarm mode.
|
||||
#[serde(default = "default_replicas")]
|
||||
#[builder(default = "default_replicas()")]
|
||||
#[partial_default(default_replicas())]
|
||||
pub replicas: i32,
|
||||
|
||||
/// The default termination signal to use to stop the deployment. Defaults to SigTerm (default docker signal).
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
@@ -145,8 +165,12 @@ pub struct DeploymentConfig {
|
||||
#[partial_default(default_termination_timeout())]
|
||||
pub termination_timeout: i32,
|
||||
|
||||
/// Extra args which are interpolated into the `docker run` command,
|
||||
/// Extra args which are interpolated into the
|
||||
/// `docker run` / `docker service create` command,
|
||||
/// and affect the container configuration.
|
||||
///
|
||||
/// - Container ref: https://docs.docker.com/reference/cli/docker/container/run/#options
|
||||
/// - Swarm Service ref: https://docs.docker.com/reference/cli/docker/service/create/#options
|
||||
#[serde(default, deserialize_with = "string_list_deserializer")]
|
||||
#[partial_attr(serde(
|
||||
default,
|
||||
@@ -156,7 +180,8 @@ pub struct DeploymentConfig {
|
||||
pub extra_args: Vec<String>,
|
||||
|
||||
/// Labels attached to various termination signal options.
|
||||
/// Used to specify different shutdown functionality depending on the termination signal.
|
||||
/// Used to specify different shutdown functionality depending
|
||||
/// on the termination signal.
|
||||
#[serde(default, deserialize_with = "term_labels_deserializer")]
|
||||
#[partial_attr(serde(
|
||||
default,
|
||||
@@ -186,7 +211,7 @@ pub struct DeploymentConfig {
|
||||
#[builder(default)]
|
||||
pub volumes: String,
|
||||
|
||||
/// The environment variables passed to the container.
|
||||
/// The environment variables passed to the container / service.
|
||||
#[serde(default, deserialize_with = "env_vars_deserializer")]
|
||||
#[partial_attr(serde(
|
||||
default,
|
||||
@@ -216,6 +241,10 @@ impl DeploymentConfig {
|
||||
}
|
||||
}
|
||||
|
||||
fn default_replicas() -> i32 {
|
||||
1
|
||||
}
|
||||
|
||||
fn default_send_alerts() -> bool {
|
||||
true
|
||||
}
|
||||
@@ -231,26 +260,28 @@ fn default_network() -> String {
|
||||
impl Default for DeploymentConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
swarm_id: Default::default(),
|
||||
server_id: Default::default(),
|
||||
send_alerts: default_send_alerts(),
|
||||
links: Default::default(),
|
||||
image: Default::default(),
|
||||
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(),
|
||||
send_alerts: default_send_alerts(),
|
||||
links: Default::default(),
|
||||
network: default_network(),
|
||||
restart: Default::default(),
|
||||
command: Default::default(),
|
||||
replicas: default_replicas(),
|
||||
termination_signal: Default::default(),
|
||||
termination_timeout: default_termination_timeout(),
|
||||
extra_args: Default::default(),
|
||||
term_signal_labels: Default::default(),
|
||||
ports: Default::default(),
|
||||
volumes: Default::default(),
|
||||
environment: Default::default(),
|
||||
labels: Default::default(),
|
||||
network: default_network(),
|
||||
restart: Default::default(),
|
||||
command: Default::default(),
|
||||
extra_args: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -399,6 +430,7 @@ impl From<ContainerStateStatusEnum> for DeploymentState {
|
||||
Default,
|
||||
Display,
|
||||
EnumString,
|
||||
AsRefStr,
|
||||
)]
|
||||
pub enum RestartMode {
|
||||
#[default]
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::AsRefStr;
|
||||
use typeshare::typeshare;
|
||||
|
||||
use super::*;
|
||||
@@ -111,6 +112,7 @@ pub struct NodeSpec {
|
||||
Default,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
AsRefStr,
|
||||
)]
|
||||
pub enum NodeSpecRoleEnum {
|
||||
#[default]
|
||||
@@ -134,6 +136,7 @@ pub enum NodeSpecRoleEnum {
|
||||
Default,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
AsRefStr,
|
||||
)]
|
||||
pub enum NodeSpecAvailabilityEnum {
|
||||
#[default]
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typeshare::typeshare;
|
||||
|
||||
use crate::entities::swarm::SwarmState;
|
||||
|
||||
/// Swarm stack list item.
|
||||
/// Returned by `docker stack ls --format json`
|
||||
///
|
||||
@@ -14,6 +16,14 @@ pub struct SwarmStackListItem {
|
||||
#[serde(rename = "Name")]
|
||||
pub name: Option<String>,
|
||||
|
||||
/// Swarm stack state.
|
||||
/// - Healthy if all associated tasks match their desired state
|
||||
/// - Unhealthy otherwise
|
||||
///
|
||||
/// Not included in docker cli return, computed by Komodo
|
||||
#[serde(rename = "State")]
|
||||
pub state: Option<SwarmState>,
|
||||
|
||||
/// Number of services which are part of the stack
|
||||
#[serde(rename = "Services")]
|
||||
pub services: Option<String>,
|
||||
@@ -37,11 +47,19 @@ pub struct SwarmStackListItem {
|
||||
#[derive(
|
||||
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
|
||||
)]
|
||||
pub struct SwarmStackLists {
|
||||
pub struct SwarmStack {
|
||||
/// Swarm stack name.
|
||||
#[serde(rename = "Name")]
|
||||
pub name: String,
|
||||
|
||||
/// Swarm stack state.
|
||||
/// - Healthy if all associated tasks match their desired state (or report no desired state)
|
||||
/// - Unhealthy otherwise
|
||||
///
|
||||
/// Not included in docker cli return, computed by Komodo
|
||||
#[serde(rename = "State")]
|
||||
pub state: SwarmState,
|
||||
|
||||
/// Services part of the stack
|
||||
#[serde(rename = "Services")]
|
||||
pub services: Vec<SwarmStackServiceListItem>,
|
||||
@@ -60,10 +78,11 @@ pub struct SwarmStackLists {
|
||||
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
|
||||
)]
|
||||
pub struct SwarmStackServiceListItem {
|
||||
/// The *short* swarm service ID
|
||||
#[serde(rename = "ID")]
|
||||
pub id: Option<String>,
|
||||
|
||||
/// Swarm stack task name.
|
||||
/// The service name.
|
||||
#[serde(rename = "Name")]
|
||||
pub name: Option<String>,
|
||||
|
||||
@@ -93,6 +112,7 @@ pub struct SwarmStackServiceListItem {
|
||||
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
|
||||
)]
|
||||
pub struct SwarmStackTaskListItem {
|
||||
/// The task ID
|
||||
#[serde(rename = "ID")]
|
||||
pub id: Option<String>,
|
||||
|
||||
@@ -108,9 +128,11 @@ pub struct SwarmStackTaskListItem {
|
||||
#[serde(rename = "Node")]
|
||||
pub node: Option<String>,
|
||||
|
||||
/// The task desired state. Matches 'CurrentState' when healthy.
|
||||
#[serde(rename = "DesiredState")]
|
||||
pub desired_state: Option<String>,
|
||||
|
||||
/// The task current state. Matches 'DesiredState' when healthy.
|
||||
#[serde(rename = "CurrentState")]
|
||||
pub current_state: Option<String>,
|
||||
|
||||
|
||||
@@ -1053,6 +1053,11 @@ pub enum Operation {
|
||||
UpdateSwarm,
|
||||
RenameSwarm,
|
||||
DeleteSwarm,
|
||||
RemoveSwarmNodes,
|
||||
RemoveSwarmStacks,
|
||||
RemoveSwarmServices,
|
||||
RemoveSwarmConfigs,
|
||||
RemoveSwarmSecrets,
|
||||
|
||||
// Server
|
||||
CreateServer,
|
||||
@@ -1488,3 +1493,8 @@ pub fn resource_link(
|
||||
};
|
||||
format!("{host}{path}")
|
||||
}
|
||||
|
||||
pub enum SwarmOrServer {
|
||||
Swarm(swarm::Swarm),
|
||||
Server(server::Server),
|
||||
}
|
||||
|
||||
@@ -278,7 +278,19 @@ pub type _PartialStackConfig = PartialStackConfig;
|
||||
#[partial_derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[partial(skip_serializing_none, from, diff)]
|
||||
pub struct StackConfig {
|
||||
/// The server to deploy the stack on.
|
||||
/// The Swarm to deploy the Stack on, setting the Stack into Swarm mode.
|
||||
///
|
||||
/// Note. If both swarm_id and server_id are set,
|
||||
/// swarm_id overrides server_id and the Stack will be in Swarm mode.
|
||||
#[serde(default, alias = "swarm")]
|
||||
#[partial_attr(serde(alias = "swarm"))]
|
||||
#[builder(default)]
|
||||
pub swarm_id: String,
|
||||
|
||||
/// The Server to deploy the Stack on, setting the Stack into Compose mode.
|
||||
///
|
||||
/// Note. If both swarm_id and server_id are set,
|
||||
/// swarm_id overrides server_id and the Stack will be in Swarm mode.
|
||||
#[serde(default, alias = "server")]
|
||||
#[partial_attr(serde(alias = "server"))]
|
||||
#[builder(default)]
|
||||
@@ -295,9 +307,9 @@ pub struct StackConfig {
|
||||
|
||||
/// Optionally specify a custom project name for the stack.
|
||||
/// If this is empty string, it will default to the stack name.
|
||||
/// Used with `docker compose -p {project_name}`.
|
||||
/// Used with `docker compose -p {project_name}` / `docker stack deploy {project_name}`.
|
||||
///
|
||||
/// Note. Can be used to import pre-existing stacks.
|
||||
/// Note. Can be used to import pre-existing stacks with names that do not match Stack name.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub project_name: String,
|
||||
@@ -305,6 +317,8 @@ pub struct StackConfig {
|
||||
/// Whether to automatically `compose pull` before redeploying stack.
|
||||
/// Ensured latest images are deployed.
|
||||
/// Will fail if the compose file specifies a locally build image.
|
||||
///
|
||||
/// Note. Not used in Swarm mode.
|
||||
#[serde(default = "default_auto_pull")]
|
||||
#[builder(default = "default_auto_pull()")]
|
||||
#[partial_default(default_auto_pull())]
|
||||
@@ -312,6 +326,8 @@ pub struct StackConfig {
|
||||
|
||||
/// Whether to `docker compose build` before `compose down` / `compose up`.
|
||||
/// Combine with build_extra_args for custom behaviors.
|
||||
///
|
||||
/// Note. Not used in Swarm mode.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub run_build: bool,
|
||||
@@ -447,6 +463,8 @@ pub struct StackConfig {
|
||||
/// The name of the written environment file before `docker compose up`.
|
||||
/// Relative to the run directory root.
|
||||
/// Default: .env
|
||||
///
|
||||
/// Note. Not used in Swarm mode.
|
||||
#[serde(default = "default_env_file_path")]
|
||||
#[builder(default = "default_env_file_path()")]
|
||||
#[partial_default(default_env_file_path())]
|
||||
@@ -500,7 +518,11 @@ pub struct StackConfig {
|
||||
#[builder(default)]
|
||||
pub post_deploy: SystemCommand,
|
||||
|
||||
/// The extra arguments to pass after `docker compose up -d`.
|
||||
/// The extra arguments to pass to the deploy command.
|
||||
///
|
||||
/// - For Compose stack, uses `docker compose up -d [EXTRA_ARGS]`.
|
||||
/// - For Swarm mode. `docker stack deploy [EXTRA_ARGS] STACK_NAME`
|
||||
///
|
||||
/// If empty, no extra arguments will be passed.
|
||||
#[serde(default, deserialize_with = "string_list_deserializer")]
|
||||
#[partial_attr(serde(
|
||||
@@ -513,6 +535,8 @@ pub struct StackConfig {
|
||||
/// The extra arguments to pass after `docker compose build`.
|
||||
/// If empty, no extra build arguments will be passed.
|
||||
/// Only used if `run_build: true`
|
||||
///
|
||||
/// Note. Not used in Swarm mode.
|
||||
#[serde(default, deserialize_with = "string_list_deserializer")]
|
||||
#[partial_attr(serde(
|
||||
default,
|
||||
@@ -560,6 +584,8 @@ pub struct StackConfig {
|
||||
/// which is given relative to the run directory.
|
||||
///
|
||||
/// If it is empty, no file will be written.
|
||||
///
|
||||
/// Note. Not used in Swarm mode.
|
||||
#[serde(default, deserialize_with = "env_vars_deserializer")]
|
||||
#[partial_attr(serde(
|
||||
default,
|
||||
@@ -611,6 +637,7 @@ fn default_send_alerts() -> bool {
|
||||
impl Default for StackConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
swarm_id: Default::default(),
|
||||
server_id: Default::default(),
|
||||
project_name: Default::default(),
|
||||
run_directory: Default::default(),
|
||||
@@ -688,6 +715,9 @@ pub struct StackServiceNames {
|
||||
///
|
||||
/// This stores only 1. and 2., ie stacko-mongo.
|
||||
/// Containers will be matched via regex like `^container_name-?[0-9]*$``
|
||||
///
|
||||
/// Note. Setting container_name is not supported by Swarm,
|
||||
/// so will always be 1. and 2. in Swarm mode.
|
||||
pub container_name: String,
|
||||
/// The services image.
|
||||
#[serde(default)]
|
||||
|
||||
@@ -29,16 +29,26 @@ pub struct SwarmListItemInfo {
|
||||
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Debug, Clone, Copy, Default, Serialize, Deserialize, Display,
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Default,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Display,
|
||||
)]
|
||||
pub enum SwarmState {
|
||||
/// Unknown case
|
||||
#[default]
|
||||
Unknown,
|
||||
/// The Swarm is healthy, all nodes OK
|
||||
Healthy,
|
||||
/// The Swarm is unhealthy
|
||||
Unhealthy,
|
||||
/// Unknown case
|
||||
#[default]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
|
||||
@@ -359,29 +359,6 @@ export type WriteResponses = {
|
||||
};
|
||||
|
||||
export type ExecuteResponses = {
|
||||
// ==== SERVER ====
|
||||
StartContainer: Types.Update;
|
||||
RestartContainer: Types.Update;
|
||||
PauseContainer: Types.Update;
|
||||
UnpauseContainer: Types.Update;
|
||||
StopContainer: Types.Update;
|
||||
DestroyContainer: Types.Update;
|
||||
StartAllContainers: Types.Update;
|
||||
RestartAllContainers: Types.Update;
|
||||
PauseAllContainers: Types.Update;
|
||||
UnpauseAllContainers: Types.Update;
|
||||
StopAllContainers: Types.Update;
|
||||
PruneContainers: Types.Update;
|
||||
DeleteNetwork: Types.Update;
|
||||
PruneNetworks: Types.Update;
|
||||
DeleteImage: Types.Update;
|
||||
PruneImages: Types.Update;
|
||||
DeleteVolume: Types.Update;
|
||||
PruneVolumes: Types.Update;
|
||||
PruneDockerBuilders: Types.Update;
|
||||
PruneBuildx: Types.Update;
|
||||
PruneSystem: Types.Update;
|
||||
|
||||
// ==== STACK ====
|
||||
DeployStack: Types.Update;
|
||||
BatchDeployStack: Types.BatchExecutionResponse;
|
||||
@@ -439,6 +416,36 @@ export type ExecuteResponses = {
|
||||
TestAlerter: Types.Update;
|
||||
SendAlert: Types.Update;
|
||||
|
||||
// ==== SERVER ====
|
||||
StartContainer: Types.Update;
|
||||
RestartContainer: Types.Update;
|
||||
PauseContainer: Types.Update;
|
||||
UnpauseContainer: Types.Update;
|
||||
StopContainer: Types.Update;
|
||||
DestroyContainer: Types.Update;
|
||||
StartAllContainers: Types.Update;
|
||||
RestartAllContainers: Types.Update;
|
||||
PauseAllContainers: Types.Update;
|
||||
UnpauseAllContainers: Types.Update;
|
||||
StopAllContainers: Types.Update;
|
||||
PruneContainers: Types.Update;
|
||||
DeleteNetwork: Types.Update;
|
||||
PruneNetworks: Types.Update;
|
||||
DeleteImage: Types.Update;
|
||||
PruneImages: Types.Update;
|
||||
DeleteVolume: Types.Update;
|
||||
PruneVolumes: Types.Update;
|
||||
PruneDockerBuilders: Types.Update;
|
||||
PruneBuildx: Types.Update;
|
||||
PruneSystem: Types.Update;
|
||||
|
||||
// ==== SWARM ====
|
||||
RemoveSwarmNodes: Types.Update;
|
||||
RemoveSwarmStacks: Types.Update;
|
||||
RemoveSwarmServices: Types.Update;
|
||||
RemoveSwarmConfigs: Types.Update;
|
||||
RemoveSwarmSecrets: Types.Update;
|
||||
|
||||
// ==== MAINTENANCE ====
|
||||
ClearRepoCache: Types.Update;
|
||||
BackupCoreDatabase: Types.Update;
|
||||
|
||||
@@ -351,6 +351,11 @@ export enum Operation {
|
||||
UpdateSwarm = "UpdateSwarm",
|
||||
RenameSwarm = "RenameSwarm",
|
||||
DeleteSwarm = "DeleteSwarm",
|
||||
RemoveSwarmNodes = "RemoveSwarmNodes",
|
||||
RemoveSwarmStacks = "RemoveSwarmStacks",
|
||||
RemoveSwarmServices = "RemoveSwarmServices",
|
||||
RemoveSwarmConfigs = "RemoveSwarmConfigs",
|
||||
RemoveSwarmSecrets = "RemoveSwarmSecrets",
|
||||
CreateServer = "CreateServer",
|
||||
UpdateServer = "UpdateServer",
|
||||
UpdateServerKey = "UpdateServerKey",
|
||||
@@ -818,16 +823,21 @@ export type BuilderQuery = ResourceQuery<BuilderQuerySpecifics>;
|
||||
export type Execution =
|
||||
/** The "null" execution. Does nothing. */
|
||||
| { type: "None", params: NoData }
|
||||
/** Run the target action. (alias: `action`, `ac`) */
|
||||
| { type: "RunAction", params: RunAction }
|
||||
| { type: "BatchRunAction", params: BatchRunAction }
|
||||
/** Run the target procedure. (alias: `procedure`, `pr`) */
|
||||
| { type: "RunProcedure", params: RunProcedure }
|
||||
| { type: "BatchRunProcedure", params: BatchRunProcedure }
|
||||
/** Run the target build. (alias: `build`, `bd`) */
|
||||
| { type: "RunBuild", params: RunBuild }
|
||||
| { type: "BatchRunBuild", params: BatchRunBuild }
|
||||
| { type: "CancelBuild", params: CancelBuild }
|
||||
/** Deploy the target stack. (alias: `stack`, `st`) */
|
||||
| { type: "DeployStack", params: DeployStack }
|
||||
| { type: "BatchDeployStack", params: BatchDeployStack }
|
||||
| { type: "DeployStackIfChanged", params: DeployStackIfChanged }
|
||||
| { type: "BatchDeployStackIfChanged", params: BatchDeployStackIfChanged }
|
||||
| { type: "PullStack", params: PullStack }
|
||||
| { type: "BatchPullStack", params: BatchPullStack }
|
||||
| { type: "StartStack", params: StartStack }
|
||||
| { type: "RestartStack", params: RestartStack }
|
||||
| { type: "PauseStack", params: PauseStack }
|
||||
| { type: "UnpauseStack", params: UnpauseStack }
|
||||
| { type: "StopStack", params: StopStack }
|
||||
| { type: "DestroyStack", params: DestroyStack }
|
||||
| { type: "BatchDestroyStack", params: BatchDestroyStack }
|
||||
| { type: "RunStackService", params: RunStackService }
|
||||
/** Deploy the target deployment. (alias: `dp`) */
|
||||
| { type: "Deploy", params: Deploy }
|
||||
| { type: "BatchDeploy", params: BatchDeploy }
|
||||
@@ -839,6 +849,10 @@ export type Execution =
|
||||
| { type: "StopDeployment", params: StopDeployment }
|
||||
| { type: "DestroyDeployment", params: DestroyDeployment }
|
||||
| { type: "BatchDestroyDeployment", params: BatchDestroyDeployment }
|
||||
/** Run the target build. (alias: `build`, `bd`) */
|
||||
| { type: "RunBuild", params: RunBuild }
|
||||
| { type: "BatchRunBuild", params: BatchRunBuild }
|
||||
| { type: "CancelBuild", params: CancelBuild }
|
||||
/** Clone the target repo */
|
||||
| { type: "CloneRepo", params: CloneRepo }
|
||||
| { type: "BatchCloneRepo", params: BatchCloneRepo }
|
||||
@@ -847,6 +861,18 @@ export type Execution =
|
||||
| { type: "BuildRepo", params: BuildRepo }
|
||||
| { type: "BatchBuildRepo", params: BatchBuildRepo }
|
||||
| { type: "CancelRepoBuild", params: CancelRepoBuild }
|
||||
/** Run the target procedure. (alias: `procedure`, `pr`) */
|
||||
| { type: "RunProcedure", params: RunProcedure }
|
||||
| { type: "BatchRunProcedure", params: BatchRunProcedure }
|
||||
/** Run the target action. (alias: `action`, `ac`) */
|
||||
| { type: "RunAction", params: RunAction }
|
||||
| { type: "BatchRunAction", params: BatchRunAction }
|
||||
/** Execute a Resource Sync. (alias: `sync`) */
|
||||
| { type: "RunSync", params: RunSync }
|
||||
/** Commit a Resource Sync. (alias: `commit`) */
|
||||
| { type: "CommitSync", params: CommitSync }
|
||||
| { type: "TestAlerter", params: TestAlerter }
|
||||
| { type: "SendAlert", params: SendAlert }
|
||||
| { type: "StartContainer", params: StartContainer }
|
||||
| { type: "RestartContainer", params: RestartContainer }
|
||||
| { type: "PauseContainer", params: PauseContainer }
|
||||
@@ -868,27 +894,11 @@ export type Execution =
|
||||
| { type: "PruneDockerBuilders", params: PruneDockerBuilders }
|
||||
| { type: "PruneBuildx", params: PruneBuildx }
|
||||
| { type: "PruneSystem", params: PruneSystem }
|
||||
/** Execute a Resource Sync. (alias: `sync`) */
|
||||
| { type: "RunSync", params: RunSync }
|
||||
/** Commit a Resource Sync. (alias: `commit`) */
|
||||
| { type: "CommitSync", params: CommitSync }
|
||||
/** Deploy the target stack. (alias: `stack`, `st`) */
|
||||
| { type: "DeployStack", params: DeployStack }
|
||||
| { type: "BatchDeployStack", params: BatchDeployStack }
|
||||
| { type: "DeployStackIfChanged", params: DeployStackIfChanged }
|
||||
| { type: "BatchDeployStackIfChanged", params: BatchDeployStackIfChanged }
|
||||
| { type: "PullStack", params: PullStack }
|
||||
| { type: "BatchPullStack", params: BatchPullStack }
|
||||
| { type: "StartStack", params: StartStack }
|
||||
| { type: "RestartStack", params: RestartStack }
|
||||
| { type: "PauseStack", params: PauseStack }
|
||||
| { type: "UnpauseStack", params: UnpauseStack }
|
||||
| { type: "StopStack", params: StopStack }
|
||||
| { type: "DestroyStack", params: DestroyStack }
|
||||
| { type: "BatchDestroyStack", params: BatchDestroyStack }
|
||||
| { type: "RunStackService", params: RunStackService }
|
||||
| { type: "TestAlerter", params: TestAlerter }
|
||||
| { type: "SendAlert", params: SendAlert }
|
||||
| { type: "RemoveSwarmNodes", params: RemoveSwarmNodes }
|
||||
| { type: "RemoveSwarmStacks", params: RemoveSwarmStacks }
|
||||
| { type: "RemoveSwarmServices", params: RemoveSwarmServices }
|
||||
| { type: "RemoveSwarmConfigs", params: RemoveSwarmConfigs }
|
||||
| { type: "RemoveSwarmSecrets", params: RemoveSwarmSecrets }
|
||||
| { type: "ClearRepoCache", params: ClearRepoCache }
|
||||
| { type: "BackupCoreDatabase", params: BackupCoreDatabase }
|
||||
| { type: "GlobalAutoUpdate", params: GlobalAutoUpdate }
|
||||
@@ -1211,7 +1221,19 @@ export enum TerminationSignal {
|
||||
}
|
||||
|
||||
export interface DeploymentConfig {
|
||||
/** The id of server the deployment is deployed on. */
|
||||
/**
|
||||
* The Swarm to deploy the Deployment on (as a Swarm Service), setting the Deployment into Swarm mode.
|
||||
*
|
||||
* Note. If both swarm_id and server_id are set,
|
||||
* swarm_id overrides server_id and the Deployment will be in Swarm mode.
|
||||
*/
|
||||
swarm_id?: string;
|
||||
/**
|
||||
* The Server to deploy the Deployment on, setting the Deployment into Container mode.
|
||||
*
|
||||
* Note. If both swarm_id and server_id are set,
|
||||
* swarm_id overrides server_id and the Deployment will be in Swarm mode.
|
||||
*/
|
||||
server_id?: string;
|
||||
/**
|
||||
* The image which the deployment deploys.
|
||||
@@ -1258,18 +1280,29 @@ export interface DeploymentConfig {
|
||||
* Empty is no command.
|
||||
*/
|
||||
command?: string;
|
||||
/**
|
||||
* The number of replicas for the Service.
|
||||
*
|
||||
* Note. Only used in Swarm mode.
|
||||
*/
|
||||
replicas: number;
|
||||
/** The default termination signal to use to stop the deployment. Defaults to SigTerm (default docker signal). */
|
||||
termination_signal?: TerminationSignal;
|
||||
/** The termination timeout. */
|
||||
termination_timeout: number;
|
||||
/**
|
||||
* Extra args which are interpolated into the `docker run` command,
|
||||
* Extra args which are interpolated into the
|
||||
* `docker run` / `docker service create` command,
|
||||
* and affect the container configuration.
|
||||
*
|
||||
* - Container ref: https://docs.docker.com/reference/cli/docker/container/run/#options
|
||||
* - Swarm Service ref: https://docs.docker.com/reference/cli/docker/service/create/#options
|
||||
*/
|
||||
extra_args?: string[];
|
||||
/**
|
||||
* Labels attached to various termination signal options.
|
||||
* Used to specify different shutdown functionality depending on the termination signal.
|
||||
* Used to specify different shutdown functionality depending
|
||||
* on the termination signal.
|
||||
*/
|
||||
term_signal_labels?: string;
|
||||
/**
|
||||
@@ -1283,7 +1316,7 @@ export interface DeploymentConfig {
|
||||
* Maps files / folders on host to files / folders in container.
|
||||
*/
|
||||
volumes?: string;
|
||||
/** The environment variables passed to the container. */
|
||||
/** The environment variables passed to the container / service. */
|
||||
environment?: string;
|
||||
/** The docker labels given to the container. */
|
||||
labels?: string;
|
||||
@@ -2265,27 +2298,43 @@ export interface StackFileDependency {
|
||||
|
||||
/** The compose file configuration. */
|
||||
export interface StackConfig {
|
||||
/** The server to deploy the stack on. */
|
||||
/**
|
||||
* The Swarm to deploy the Stack on, setting the Stack into Swarm mode.
|
||||
*
|
||||
* Note. If both swarm_id and server_id are set,
|
||||
* swarm_id overrides server_id and the Stack will be in Swarm mode.
|
||||
*/
|
||||
swarm_id?: string;
|
||||
/**
|
||||
* The Server to deploy the Stack on, setting the Stack into Compose mode.
|
||||
*
|
||||
* Note. If both swarm_id and server_id are set,
|
||||
* swarm_id overrides server_id and the Stack will be in Swarm mode.
|
||||
*/
|
||||
server_id?: string;
|
||||
/** Configure quick links that are displayed in the resource header */
|
||||
links?: string[];
|
||||
/**
|
||||
* Optionally specify a custom project name for the stack.
|
||||
* If this is empty string, it will default to the stack name.
|
||||
* Used with `docker compose -p {project_name}`.
|
||||
* Used with `docker compose -p {project_name}` / `docker stack deploy {project_name}`.
|
||||
*
|
||||
* Note. Can be used to import pre-existing stacks.
|
||||
* Note. Can be used to import pre-existing stacks with names that do not match Stack name.
|
||||
*/
|
||||
project_name?: string;
|
||||
/**
|
||||
* Whether to automatically `compose pull` before redeploying stack.
|
||||
* Ensured latest images are deployed.
|
||||
* Will fail if the compose file specifies a locally build image.
|
||||
*
|
||||
* Note. Not used in Swarm mode.
|
||||
*/
|
||||
auto_pull: boolean;
|
||||
/**
|
||||
* Whether to `docker compose build` before `compose down` / `compose up`.
|
||||
* Combine with build_extra_args for custom behaviors.
|
||||
*
|
||||
* Note. Not used in Swarm mode.
|
||||
*/
|
||||
run_build?: boolean;
|
||||
/** Whether to poll for any updates to the images. */
|
||||
@@ -2372,6 +2421,8 @@ export interface StackConfig {
|
||||
* The name of the written environment file before `docker compose up`.
|
||||
* Relative to the run directory root.
|
||||
* Default: .env
|
||||
*
|
||||
* Note. Not used in Swarm mode.
|
||||
*/
|
||||
env_file_path: string;
|
||||
/**
|
||||
@@ -2403,7 +2454,11 @@ export interface StackConfig {
|
||||
/** The optional command to run after the Stack is deployed. */
|
||||
post_deploy?: SystemCommand;
|
||||
/**
|
||||
* The extra arguments to pass after `docker compose up -d`.
|
||||
* The extra arguments to pass to the deploy command.
|
||||
*
|
||||
* - For Compose stack, uses `docker compose up -d [EXTRA_ARGS]`.
|
||||
* - For Swarm mode. `docker stack deploy [EXTRA_ARGS] STACK_NAME`
|
||||
*
|
||||
* If empty, no extra arguments will be passed.
|
||||
*/
|
||||
extra_args?: string[];
|
||||
@@ -2411,6 +2466,8 @@ export interface StackConfig {
|
||||
* The extra arguments to pass after `docker compose build`.
|
||||
* If empty, no extra build arguments will be passed.
|
||||
* Only used if `run_build: true`
|
||||
*
|
||||
* Note. Not used in Swarm mode.
|
||||
*/
|
||||
build_extra_args?: string[];
|
||||
/**
|
||||
@@ -2442,6 +2499,8 @@ export interface StackConfig {
|
||||
* which is given relative to the run directory.
|
||||
*
|
||||
* If it is empty, no file will be written.
|
||||
*
|
||||
* Note. Not used in Swarm mode.
|
||||
*/
|
||||
environment?: string;
|
||||
}
|
||||
@@ -2473,6 +2532,9 @@ export interface StackServiceNames {
|
||||
*
|
||||
* This stores only 1. and 2., ie stacko-mongo.
|
||||
* Containers will be matched via regex like `^container_name-?[0-9]*$``
|
||||
*
|
||||
* Note. Setting container_name is not supported by Swarm,
|
||||
* so will always be 1. and 2. in Swarm mode.
|
||||
*/
|
||||
container_name: string;
|
||||
/** The services image. */
|
||||
@@ -4334,6 +4396,15 @@ export interface SwarmService {
|
||||
|
||||
export type InspectSwarmServiceResponse = SwarmService;
|
||||
|
||||
export enum SwarmState {
|
||||
/** The Swarm is healthy, all nodes OK */
|
||||
Healthy = "Healthy",
|
||||
/** The Swarm is unhealthy */
|
||||
Unhealthy = "Unhealthy",
|
||||
/** Unknown case */
|
||||
Unknown = "Unknown",
|
||||
}
|
||||
|
||||
/**
|
||||
* Swarm stack service list item.
|
||||
* Returned by `docker stack services --format json <NAME>`
|
||||
@@ -4341,8 +4412,9 @@ export type InspectSwarmServiceResponse = SwarmService;
|
||||
* https://docs.docker.com/reference/cli/docker/stack/services/#format
|
||||
*/
|
||||
export interface SwarmStackServiceListItem {
|
||||
/** The *short* swarm service ID */
|
||||
ID?: string;
|
||||
/** Swarm stack task name. */
|
||||
/** The service name. */
|
||||
Name?: string;
|
||||
/** The service mode. */
|
||||
Mode?: string;
|
||||
@@ -4361,6 +4433,7 @@ export interface SwarmStackServiceListItem {
|
||||
* https://docs.docker.com/reference/cli/docker/stack/ps/#format
|
||||
*/
|
||||
export interface SwarmStackTaskListItem {
|
||||
/** The task ID */
|
||||
ID?: string;
|
||||
/** Swarm stack task name. */
|
||||
Name?: string;
|
||||
@@ -4368,7 +4441,9 @@ export interface SwarmStackTaskListItem {
|
||||
Image?: string;
|
||||
/** The node the task is running on */
|
||||
Node?: string;
|
||||
/** The task desired state. Matches 'CurrentState' when healthy. */
|
||||
DesiredState?: string;
|
||||
/** The task current state. Matches 'DesiredState' when healthy. */
|
||||
CurrentState?: string;
|
||||
/** An error message, if one exists */
|
||||
Error?: string;
|
||||
@@ -4384,16 +4459,24 @@ export interface SwarmStackTaskListItem {
|
||||
* docker stack ps --format json <STACK>
|
||||
* ```
|
||||
*/
|
||||
export interface SwarmStackLists {
|
||||
export interface SwarmStack {
|
||||
/** Swarm stack name. */
|
||||
Name: string;
|
||||
/**
|
||||
* Swarm stack state.
|
||||
* - Healthy if all associated tasks match their desired state (or report no desired state)
|
||||
* - Unhealthy otherwise
|
||||
*
|
||||
* Not included in docker cli return, computed by Komodo
|
||||
*/
|
||||
State: SwarmState;
|
||||
/** Services part of the stack */
|
||||
Services: SwarmStackServiceListItem[];
|
||||
/** Tasks part of the stack */
|
||||
Tasks: SwarmStackTaskListItem[];
|
||||
}
|
||||
|
||||
export type InspectSwarmStackResponse = SwarmStackLists;
|
||||
export type InspectSwarmStackResponse = SwarmStack;
|
||||
|
||||
export enum TaskState {
|
||||
NEW = "new",
|
||||
@@ -5121,6 +5204,14 @@ export type ListSwarmServicesResponse = SwarmServiceListItem[];
|
||||
export interface SwarmStackListItem {
|
||||
/** Swarm stack name. */
|
||||
Name?: string;
|
||||
/**
|
||||
* Swarm stack state.
|
||||
* - Healthy if all associated tasks match their desired state
|
||||
* - Unhealthy otherwise
|
||||
*
|
||||
* Not included in docker cli return, computed by Komodo
|
||||
*/
|
||||
State?: SwarmState;
|
||||
/** Number of services which are part of the stack */
|
||||
Services?: string;
|
||||
/** The stack orchestrator */
|
||||
@@ -5151,15 +5242,6 @@ export interface SwarmTaskListItem {
|
||||
|
||||
export type ListSwarmTasksResponse = SwarmTaskListItem[];
|
||||
|
||||
export enum SwarmState {
|
||||
/** Unknown case */
|
||||
Unknown = "Unknown",
|
||||
/** The Swarm is healthy, all nodes OK */
|
||||
Healthy = "Healthy",
|
||||
/** The Swarm is unhealthy */
|
||||
Unhealthy = "Unhealthy",
|
||||
}
|
||||
|
||||
export interface SwarmListItemInfo {
|
||||
/** Servers part of the swarm */
|
||||
server_ids: string[];
|
||||
@@ -6750,6 +6832,8 @@ export interface DeployStack {
|
||||
/**
|
||||
* Filter to only deploy specific services.
|
||||
* If empty, will deploy all services.
|
||||
*
|
||||
* Note. For Swarm mode Stacks, this field is not supported and will be ignored.
|
||||
*/
|
||||
services?: string[];
|
||||
/**
|
||||
@@ -8711,6 +8795,70 @@ export interface RefreshStackCache {
|
||||
stack: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* `docker config rm CONFIG [CONFIG...]`
|
||||
*
|
||||
* https://docs.docker.com/reference/cli/docker/config/rm/
|
||||
*/
|
||||
export interface RemoveSwarmConfigs {
|
||||
/** Name or id */
|
||||
swarm: string;
|
||||
/** Config names or ids */
|
||||
configs: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* `docker node rm [OPTIONS] NODE [NODE...]`
|
||||
*
|
||||
* https://docs.docker.com/reference/cli/docker/node/rm/
|
||||
*/
|
||||
export interface RemoveSwarmNodes {
|
||||
/** Name or id */
|
||||
swarm: string;
|
||||
/** Node names or ids to remove */
|
||||
nodes: string[];
|
||||
/** Force remove a node from the swarm */
|
||||
force?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* `docker secret rm SECRET [SECRET...]`
|
||||
*
|
||||
* https://docs.docker.com/reference/cli/docker/secret/rm/
|
||||
*/
|
||||
export interface RemoveSwarmSecrets {
|
||||
/** Name or id */
|
||||
swarm: string;
|
||||
/** Secret names or ids */
|
||||
secrets: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* `docker service rm SERVICE [SERVICE...]`
|
||||
*
|
||||
* https://docs.docker.com/reference/cli/docker/service/rm/
|
||||
*/
|
||||
export interface RemoveSwarmServices {
|
||||
/** Name or id */
|
||||
swarm: string;
|
||||
/** Service names or ids */
|
||||
services: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* `docker stack rm [OPTIONS] STACK [STACK...]`
|
||||
*
|
||||
* https://docs.docker.com/reference/cli/docker/stack/rm/
|
||||
*/
|
||||
export interface RemoveSwarmStacks {
|
||||
/** Name or id */
|
||||
swarm: string;
|
||||
/** Node names to remove */
|
||||
stacks: string[];
|
||||
/** Do not wait for stack removal */
|
||||
detach: boolean;
|
||||
}
|
||||
|
||||
/** **Admin only.** Remove a user from a user group. Response: [UserGroup] */
|
||||
export interface RemoveUserFromUserGroup {
|
||||
/** The name or id of UserGroup that user should be removed from. */
|
||||
@@ -9979,27 +10127,6 @@ export enum DayOfWeek {
|
||||
}
|
||||
|
||||
export type ExecuteRequest =
|
||||
| { type: "StartContainer", params: StartContainer }
|
||||
| { type: "RestartContainer", params: RestartContainer }
|
||||
| { type: "PauseContainer", params: PauseContainer }
|
||||
| { type: "UnpauseContainer", params: UnpauseContainer }
|
||||
| { type: "StopContainer", params: StopContainer }
|
||||
| { type: "DestroyContainer", params: DestroyContainer }
|
||||
| { type: "StartAllContainers", params: StartAllContainers }
|
||||
| { type: "RestartAllContainers", params: RestartAllContainers }
|
||||
| { type: "PauseAllContainers", params: PauseAllContainers }
|
||||
| { type: "UnpauseAllContainers", params: UnpauseAllContainers }
|
||||
| { type: "StopAllContainers", params: StopAllContainers }
|
||||
| { type: "PruneContainers", params: PruneContainers }
|
||||
| { type: "DeleteNetwork", params: DeleteNetwork }
|
||||
| { type: "PruneNetworks", params: PruneNetworks }
|
||||
| { type: "DeleteImage", params: DeleteImage }
|
||||
| { type: "PruneImages", params: PruneImages }
|
||||
| { type: "DeleteVolume", params: DeleteVolume }
|
||||
| { type: "PruneVolumes", params: PruneVolumes }
|
||||
| { type: "PruneDockerBuilders", params: PruneDockerBuilders }
|
||||
| { type: "PruneBuildx", params: PruneBuildx }
|
||||
| { type: "PruneSystem", params: PruneSystem }
|
||||
| { type: "DeployStack", params: DeployStack }
|
||||
| { type: "BatchDeployStack", params: BatchDeployStack }
|
||||
| { type: "DeployStackIfChanged", params: DeployStackIfChanged }
|
||||
@@ -10041,6 +10168,32 @@ export type ExecuteRequest =
|
||||
| { type: "RunSync", params: RunSync }
|
||||
| { type: "TestAlerter", params: TestAlerter }
|
||||
| { type: "SendAlert", params: SendAlert }
|
||||
| { type: "StartContainer", params: StartContainer }
|
||||
| { type: "RestartContainer", params: RestartContainer }
|
||||
| { type: "PauseContainer", params: PauseContainer }
|
||||
| { type: "UnpauseContainer", params: UnpauseContainer }
|
||||
| { type: "StopContainer", params: StopContainer }
|
||||
| { type: "DestroyContainer", params: DestroyContainer }
|
||||
| { type: "StartAllContainers", params: StartAllContainers }
|
||||
| { type: "RestartAllContainers", params: RestartAllContainers }
|
||||
| { type: "PauseAllContainers", params: PauseAllContainers }
|
||||
| { type: "UnpauseAllContainers", params: UnpauseAllContainers }
|
||||
| { type: "StopAllContainers", params: StopAllContainers }
|
||||
| { type: "PruneContainers", params: PruneContainers }
|
||||
| { type: "DeleteNetwork", params: DeleteNetwork }
|
||||
| { type: "PruneNetworks", params: PruneNetworks }
|
||||
| { type: "DeleteImage", params: DeleteImage }
|
||||
| { type: "PruneImages", params: PruneImages }
|
||||
| { type: "DeleteVolume", params: DeleteVolume }
|
||||
| { type: "PruneVolumes", params: PruneVolumes }
|
||||
| { type: "PruneDockerBuilders", params: PruneDockerBuilders }
|
||||
| { type: "PruneBuildx", params: PruneBuildx }
|
||||
| { type: "PruneSystem", params: PruneSystem }
|
||||
| { type: "RemoveSwarmNodes", params: RemoveSwarmNodes }
|
||||
| { type: "RemoveSwarmStacks", params: RemoveSwarmStacks }
|
||||
| { type: "RemoveSwarmServices", params: RemoveSwarmServices }
|
||||
| { type: "RemoveSwarmConfigs", params: RemoveSwarmConfigs }
|
||||
| { type: "RemoveSwarmSecrets", params: RemoveSwarmSecrets }
|
||||
| { type: "ClearRepoCache", params: ClearRepoCache }
|
||||
| { type: "BackupCoreDatabase", params: BackupCoreDatabase }
|
||||
| { type: "GlobalAutoUpdate", params: GlobalAutoUpdate }
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
use komodo_client::entities::{
|
||||
FileContents, RepoExecutionResponse, SearchCombinator,
|
||||
repo::Repo,
|
||||
stack::{
|
||||
Stack, StackFileDependency, StackRemoteFileContents,
|
||||
StackServiceNames,
|
||||
},
|
||||
stack::{Stack, StackFileDependency, StackRemoteFileContents},
|
||||
update::Log,
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::api::DeployStackResponse;
|
||||
|
||||
//
|
||||
|
||||
/// Get the compose contents on the host, for stacks using
|
||||
@@ -165,7 +164,7 @@ pub struct ComposePullResponse {
|
||||
|
||||
/// docker compose up.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]
|
||||
#[response(ComposeUpResponse)]
|
||||
#[response(DeployStackResponse)]
|
||||
#[error(anyhow::Error)]
|
||||
pub struct ComposeUp {
|
||||
/// The stack to deploy
|
||||
@@ -185,31 +184,6 @@ pub struct ComposeUp {
|
||||
pub replacers: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct ComposeUpResponse {
|
||||
/// If any of the required files are missing, they will be here.
|
||||
pub missing_files: Vec<String>,
|
||||
/// The logs produced by the deploy
|
||||
pub logs: Vec<Log>,
|
||||
/// Whether stack was successfully deployed
|
||||
pub deployed: bool,
|
||||
/// The stack services.
|
||||
///
|
||||
/// Note. The "image" is after interpolation.
|
||||
#[serde(default)]
|
||||
pub services: Vec<StackServiceNames>,
|
||||
/// The deploy compose file contents if they could be acquired, or empty vec.
|
||||
pub file_contents: Vec<StackRemoteFileContents>,
|
||||
/// The error in getting remote file contents at the path, or null
|
||||
pub remote_errors: Vec<FileContents>,
|
||||
/// The output of `docker compose config` at deploy time
|
||||
pub compose_config: Option<String>,
|
||||
/// If its a repo based stack, will include the latest commit hash
|
||||
pub commit_hash: Option<String>,
|
||||
/// If its a repo based stack, will include the latest commit message
|
||||
pub commit_message: Option<String>,
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
|
||||
@@ -87,10 +87,12 @@ pub struct GetFullContainerStats {
|
||||
// ACTIONS
|
||||
// =======
|
||||
|
||||
/// Executes `docker run` to create a container
|
||||
/// using info given by the Deployment
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]
|
||||
#[response(Log)]
|
||||
#[error(anyhow::Error)]
|
||||
pub struct Deploy {
|
||||
pub struct RunContainer {
|
||||
pub deployment: Deployment,
|
||||
pub stop_signal: Option<TerminationSignal>,
|
||||
pub stop_time: Option<i32>,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use komodo_client::entities::{
|
||||
FileContents,
|
||||
config::{DockerRegistry, GitProvider},
|
||||
stack::{StackRemoteFileContents, StackServiceNames},
|
||||
update::Log,
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
@@ -83,3 +85,27 @@ pub struct ListSecrets {}
|
||||
#[response(Log)]
|
||||
#[error(anyhow::Error)]
|
||||
pub struct PruneSystem {}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct DeployStackResponse {
|
||||
/// If any of the required files are missing, they will be here.
|
||||
pub missing_files: Vec<String>,
|
||||
/// The logs produced by the deploy
|
||||
pub logs: Vec<Log>,
|
||||
/// Whether stack was successfully deployed
|
||||
pub deployed: bool,
|
||||
/// The stack services.
|
||||
///
|
||||
/// Note. The "image" is after interpolation.
|
||||
pub services: Vec<StackServiceNames>,
|
||||
/// The deploy compose file contents if they could be acquired, or empty vec.
|
||||
pub file_contents: Vec<StackRemoteFileContents>,
|
||||
/// The error in getting remote file contents at the path, or null
|
||||
pub remote_errors: Vec<FileContents>,
|
||||
/// The output of `docker compose config` / `docker stack config` at deploy time
|
||||
pub merged_config: Option<String>,
|
||||
/// If its a repo based stack, will include the latest commit hash
|
||||
pub commit_hash: Option<String>,
|
||||
/// If its a repo based stack, will include the latest commit message
|
||||
pub commit_message: Option<String>,
|
||||
}
|
||||
|
||||
@@ -2,21 +2,26 @@ use std::collections::HashMap;
|
||||
|
||||
use komodo_client::entities::{
|
||||
SearchCombinator,
|
||||
deployment::Deployment,
|
||||
docker::{
|
||||
SwarmLists,
|
||||
config::SwarmConfig,
|
||||
node::{NodeSpecAvailabilityEnum, NodeSpecRoleEnum, SwarmNode},
|
||||
secret::SwarmSecret,
|
||||
service::SwarmService,
|
||||
stack::SwarmStackLists,
|
||||
stack::SwarmStack,
|
||||
swarm::SwarmInspectInfo,
|
||||
task::SwarmTask,
|
||||
},
|
||||
repo::Repo,
|
||||
stack::Stack,
|
||||
update::Log,
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::api::DeployStackResponse;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]
|
||||
#[response(PollSwarmStatusResponse)]
|
||||
#[error(anyhow::Error)]
|
||||
@@ -46,7 +51,7 @@ pub struct InspectSwarmNode {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]
|
||||
#[response(Log)]
|
||||
#[error(anyhow::Error)]
|
||||
pub struct RmSwarmNodes {
|
||||
pub struct RemoveSwarmNodes {
|
||||
pub nodes: Vec<String>,
|
||||
pub force: bool,
|
||||
}
|
||||
@@ -69,6 +74,50 @@ pub struct UpdateSwarmNode {
|
||||
pub role: Option<NodeSpecRoleEnum>,
|
||||
}
|
||||
|
||||
// =======
|
||||
// Stack
|
||||
// =======
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]
|
||||
#[response(SwarmStack)]
|
||||
#[error(anyhow::Error)]
|
||||
pub struct InspectSwarmStack {
|
||||
/// The swarm stack name
|
||||
pub stack: String,
|
||||
}
|
||||
|
||||
/// `docker stack deploy [OPTIONS] STACK`
|
||||
///
|
||||
/// https://docs.docker.com/reference/cli/docker/stack/deploy/
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]
|
||||
#[response(DeployStackResponse)]
|
||||
#[error(anyhow::Error)]
|
||||
pub struct DeploySwarmStack {
|
||||
/// The stack to deploy
|
||||
pub stack: Stack,
|
||||
/// The linked repo, if it exists.
|
||||
pub repo: Option<Repo>,
|
||||
/// 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>,
|
||||
/// Propogate any secret replacers from core interpolation.
|
||||
#[serde(default)]
|
||||
pub replacers: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
/// `docker stack rm [OPTIONS] STACK [STACK...]`
|
||||
///
|
||||
/// https://docs.docker.com/reference/cli/docker/stack/rm/
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]
|
||||
#[response(Log)]
|
||||
#[error(anyhow::Error)]
|
||||
pub struct RemoveSwarmStacks {
|
||||
pub stacks: Vec<String>,
|
||||
/// Do not wait for stack removal
|
||||
pub detach: bool,
|
||||
}
|
||||
|
||||
// =========
|
||||
// Service
|
||||
// =========
|
||||
@@ -146,13 +195,28 @@ pub struct GetSwarmServiceLogSearch {
|
||||
pub details: bool,
|
||||
}
|
||||
|
||||
/// `docker service create [OPTIONS] IMAGE [COMMAND] [ARG...]`
|
||||
///
|
||||
/// https://docs.docker.com/reference/cli/docker/service/create/
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]
|
||||
#[response(Log)]
|
||||
#[error(anyhow::Error)]
|
||||
pub struct CreateSwarmService {
|
||||
pub deployment: Deployment,
|
||||
/// Override registry token with one sent from core.
|
||||
pub registry_token: Option<String>,
|
||||
/// Propogate any secret replacers from core interpolation.
|
||||
#[serde(default)]
|
||||
pub replacers: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
/// `docker service rm SERVICE [SERVICE...]`
|
||||
///
|
||||
/// https://docs.docker.com/reference/cli/docker/service/rm/
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]
|
||||
#[response(Log)]
|
||||
#[error(anyhow::Error)]
|
||||
pub struct RmSwarmServices {
|
||||
pub struct RemoveSwarmServices {
|
||||
pub services: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -167,17 +231,6 @@ pub struct InspectSwarmTask {
|
||||
pub task: String,
|
||||
}
|
||||
|
||||
// ========
|
||||
// Secret
|
||||
// ========
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]
|
||||
#[response(SwarmSecret)]
|
||||
#[error(anyhow::Error)]
|
||||
pub struct InspectSwarmSecret {
|
||||
pub secret: String,
|
||||
}
|
||||
|
||||
// ========
|
||||
// Config
|
||||
// ========
|
||||
@@ -189,26 +242,33 @@ pub struct InspectSwarmConfig {
|
||||
pub config: String,
|
||||
}
|
||||
|
||||
// =======
|
||||
// Stack
|
||||
// =======
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]
|
||||
#[response(SwarmStackLists)]
|
||||
#[error(anyhow::Error)]
|
||||
pub struct InspectSwarmStack {
|
||||
/// The swarm stack name
|
||||
pub stack: String,
|
||||
}
|
||||
|
||||
/// `docker stack rm [OPTIONS] STACK [STACK...]`
|
||||
/// `docker config rm CONFIG [CONFIG...]`
|
||||
///
|
||||
/// https://docs.docker.com/reference/cli/docker/stack/rm/
|
||||
/// https://docs.docker.com/reference/cli/docker/config/rm/
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]
|
||||
#[response(Log)]
|
||||
#[error(anyhow::Error)]
|
||||
pub struct RmSwarmStacks {
|
||||
pub stacks: Vec<String>,
|
||||
/// Do not wait for stack removal
|
||||
pub detach: bool,
|
||||
pub struct RemoveSwarmConfigs {
|
||||
pub configs: Vec<String>,
|
||||
}
|
||||
|
||||
// ========
|
||||
// Secret
|
||||
// ========
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]
|
||||
#[response(SwarmSecret)]
|
||||
#[error(anyhow::Error)]
|
||||
pub struct InspectSwarmSecret {
|
||||
pub secret: String,
|
||||
}
|
||||
|
||||
/// `docker secret rm SECRET [SECRET...]`
|
||||
///
|
||||
/// https://docs.docker.com/reference/cli/docker/secret/rm/
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]
|
||||
#[response(Log)]
|
||||
#[error(anyhow::Error)]
|
||||
pub struct RemoveSwarmSecrets {
|
||||
pub secrets: Vec<String>,
|
||||
}
|
||||
|
||||
47
frontend/public/client/responses.d.ts
vendored
47
frontend/public/client/responses.d.ts
vendored
@@ -263,27 +263,6 @@ export type WriteResponses = {
|
||||
CloseAlert: Types.NoData;
|
||||
};
|
||||
export type ExecuteResponses = {
|
||||
StartContainer: Types.Update;
|
||||
RestartContainer: Types.Update;
|
||||
PauseContainer: Types.Update;
|
||||
UnpauseContainer: Types.Update;
|
||||
StopContainer: Types.Update;
|
||||
DestroyContainer: Types.Update;
|
||||
StartAllContainers: Types.Update;
|
||||
RestartAllContainers: Types.Update;
|
||||
PauseAllContainers: Types.Update;
|
||||
UnpauseAllContainers: Types.Update;
|
||||
StopAllContainers: Types.Update;
|
||||
PruneContainers: Types.Update;
|
||||
DeleteNetwork: Types.Update;
|
||||
PruneNetworks: Types.Update;
|
||||
DeleteImage: Types.Update;
|
||||
PruneImages: Types.Update;
|
||||
DeleteVolume: Types.Update;
|
||||
PruneVolumes: Types.Update;
|
||||
PruneDockerBuilders: Types.Update;
|
||||
PruneBuildx: Types.Update;
|
||||
PruneSystem: Types.Update;
|
||||
DeployStack: Types.Update;
|
||||
BatchDeployStack: Types.BatchExecutionResponse;
|
||||
DeployStackIfChanged: Types.Update;
|
||||
@@ -325,6 +304,32 @@ export type ExecuteResponses = {
|
||||
RunSync: Types.Update;
|
||||
TestAlerter: Types.Update;
|
||||
SendAlert: Types.Update;
|
||||
StartContainer: Types.Update;
|
||||
RestartContainer: Types.Update;
|
||||
PauseContainer: Types.Update;
|
||||
UnpauseContainer: Types.Update;
|
||||
StopContainer: Types.Update;
|
||||
DestroyContainer: Types.Update;
|
||||
StartAllContainers: Types.Update;
|
||||
RestartAllContainers: Types.Update;
|
||||
PauseAllContainers: Types.Update;
|
||||
UnpauseAllContainers: Types.Update;
|
||||
StopAllContainers: Types.Update;
|
||||
PruneContainers: Types.Update;
|
||||
DeleteNetwork: Types.Update;
|
||||
PruneNetworks: Types.Update;
|
||||
DeleteImage: Types.Update;
|
||||
PruneImages: Types.Update;
|
||||
DeleteVolume: Types.Update;
|
||||
PruneVolumes: Types.Update;
|
||||
PruneDockerBuilders: Types.Update;
|
||||
PruneBuildx: Types.Update;
|
||||
PruneSystem: Types.Update;
|
||||
RemoveSwarmNodes: Types.Update;
|
||||
RemoveSwarmStacks: Types.Update;
|
||||
RemoveSwarmServices: Types.Update;
|
||||
RemoveSwarmConfigs: Types.Update;
|
||||
RemoveSwarmSecrets: Types.Update;
|
||||
ClearRepoCache: Types.Update;
|
||||
BackupCoreDatabase: Types.Update;
|
||||
GlobalAutoUpdate: Types.Update;
|
||||
|
||||
484
frontend/public/client/types.d.ts
vendored
484
frontend/public/client/types.d.ts
vendored
@@ -358,6 +358,11 @@ export declare enum Operation {
|
||||
UpdateSwarm = "UpdateSwarm",
|
||||
RenameSwarm = "RenameSwarm",
|
||||
DeleteSwarm = "DeleteSwarm",
|
||||
RemoveSwarmNodes = "RemoveSwarmNodes",
|
||||
RemoveSwarmStacks = "RemoveSwarmStacks",
|
||||
RemoveSwarmServices = "RemoveSwarmServices",
|
||||
RemoveSwarmConfigs = "RemoveSwarmConfigs",
|
||||
RemoveSwarmSecrets = "RemoveSwarmSecrets",
|
||||
CreateServer = "CreateServer",
|
||||
UpdateServer = "UpdateServer",
|
||||
UpdateServerKey = "UpdateServerKey",
|
||||
@@ -815,32 +820,49 @@ export type Execution =
|
||||
type: "None";
|
||||
params: NoData;
|
||||
}
|
||||
/** Run the target action. (alias: `action`, `ac`) */
|
||||
/** Deploy the target stack. (alias: `stack`, `st`) */
|
||||
| {
|
||||
type: "RunAction";
|
||||
params: RunAction;
|
||||
type: "DeployStack";
|
||||
params: DeployStack;
|
||||
} | {
|
||||
type: "BatchRunAction";
|
||||
params: BatchRunAction;
|
||||
}
|
||||
/** Run the target procedure. (alias: `procedure`, `pr`) */
|
||||
| {
|
||||
type: "RunProcedure";
|
||||
params: RunProcedure;
|
||||
type: "BatchDeployStack";
|
||||
params: BatchDeployStack;
|
||||
} | {
|
||||
type: "BatchRunProcedure";
|
||||
params: BatchRunProcedure;
|
||||
}
|
||||
/** Run the target build. (alias: `build`, `bd`) */
|
||||
| {
|
||||
type: "RunBuild";
|
||||
params: RunBuild;
|
||||
type: "DeployStackIfChanged";
|
||||
params: DeployStackIfChanged;
|
||||
} | {
|
||||
type: "BatchRunBuild";
|
||||
params: BatchRunBuild;
|
||||
type: "BatchDeployStackIfChanged";
|
||||
params: BatchDeployStackIfChanged;
|
||||
} | {
|
||||
type: "CancelBuild";
|
||||
params: CancelBuild;
|
||||
type: "PullStack";
|
||||
params: PullStack;
|
||||
} | {
|
||||
type: "BatchPullStack";
|
||||
params: BatchPullStack;
|
||||
} | {
|
||||
type: "StartStack";
|
||||
params: StartStack;
|
||||
} | {
|
||||
type: "RestartStack";
|
||||
params: RestartStack;
|
||||
} | {
|
||||
type: "PauseStack";
|
||||
params: PauseStack;
|
||||
} | {
|
||||
type: "UnpauseStack";
|
||||
params: UnpauseStack;
|
||||
} | {
|
||||
type: "StopStack";
|
||||
params: StopStack;
|
||||
} | {
|
||||
type: "DestroyStack";
|
||||
params: DestroyStack;
|
||||
} | {
|
||||
type: "BatchDestroyStack";
|
||||
params: BatchDestroyStack;
|
||||
} | {
|
||||
type: "RunStackService";
|
||||
params: RunStackService;
|
||||
}
|
||||
/** Deploy the target deployment. (alias: `dp`) */
|
||||
| {
|
||||
@@ -874,6 +896,17 @@ export type Execution =
|
||||
type: "BatchDestroyDeployment";
|
||||
params: BatchDestroyDeployment;
|
||||
}
|
||||
/** Run the target build. (alias: `build`, `bd`) */
|
||||
| {
|
||||
type: "RunBuild";
|
||||
params: RunBuild;
|
||||
} | {
|
||||
type: "BatchRunBuild";
|
||||
params: BatchRunBuild;
|
||||
} | {
|
||||
type: "CancelBuild";
|
||||
params: CancelBuild;
|
||||
}
|
||||
/** Clone the target repo */
|
||||
| {
|
||||
type: "CloneRepo";
|
||||
@@ -896,6 +929,38 @@ export type Execution =
|
||||
} | {
|
||||
type: "CancelRepoBuild";
|
||||
params: CancelRepoBuild;
|
||||
}
|
||||
/** Run the target procedure. (alias: `procedure`, `pr`) */
|
||||
| {
|
||||
type: "RunProcedure";
|
||||
params: RunProcedure;
|
||||
} | {
|
||||
type: "BatchRunProcedure";
|
||||
params: BatchRunProcedure;
|
||||
}
|
||||
/** Run the target action. (alias: `action`, `ac`) */
|
||||
| {
|
||||
type: "RunAction";
|
||||
params: RunAction;
|
||||
} | {
|
||||
type: "BatchRunAction";
|
||||
params: BatchRunAction;
|
||||
}
|
||||
/** Execute a Resource Sync. (alias: `sync`) */
|
||||
| {
|
||||
type: "RunSync";
|
||||
params: RunSync;
|
||||
}
|
||||
/** Commit a Resource Sync. (alias: `commit`) */
|
||||
| {
|
||||
type: "CommitSync";
|
||||
params: CommitSync;
|
||||
} | {
|
||||
type: "TestAlerter";
|
||||
params: TestAlerter;
|
||||
} | {
|
||||
type: "SendAlert";
|
||||
params: SendAlert;
|
||||
} | {
|
||||
type: "StartContainer";
|
||||
params: StartContainer;
|
||||
@@ -959,66 +1024,21 @@ export type Execution =
|
||||
} | {
|
||||
type: "PruneSystem";
|
||||
params: PruneSystem;
|
||||
}
|
||||
/** Execute a Resource Sync. (alias: `sync`) */
|
||||
| {
|
||||
type: "RunSync";
|
||||
params: RunSync;
|
||||
}
|
||||
/** Commit a Resource Sync. (alias: `commit`) */
|
||||
| {
|
||||
type: "CommitSync";
|
||||
params: CommitSync;
|
||||
}
|
||||
/** Deploy the target stack. (alias: `stack`, `st`) */
|
||||
| {
|
||||
type: "DeployStack";
|
||||
params: DeployStack;
|
||||
} | {
|
||||
type: "BatchDeployStack";
|
||||
params: BatchDeployStack;
|
||||
type: "RemoveSwarmNodes";
|
||||
params: RemoveSwarmNodes;
|
||||
} | {
|
||||
type: "DeployStackIfChanged";
|
||||
params: DeployStackIfChanged;
|
||||
type: "RemoveSwarmStacks";
|
||||
params: RemoveSwarmStacks;
|
||||
} | {
|
||||
type: "BatchDeployStackIfChanged";
|
||||
params: BatchDeployStackIfChanged;
|
||||
type: "RemoveSwarmServices";
|
||||
params: RemoveSwarmServices;
|
||||
} | {
|
||||
type: "PullStack";
|
||||
params: PullStack;
|
||||
type: "RemoveSwarmConfigs";
|
||||
params: RemoveSwarmConfigs;
|
||||
} | {
|
||||
type: "BatchPullStack";
|
||||
params: BatchPullStack;
|
||||
} | {
|
||||
type: "StartStack";
|
||||
params: StartStack;
|
||||
} | {
|
||||
type: "RestartStack";
|
||||
params: RestartStack;
|
||||
} | {
|
||||
type: "PauseStack";
|
||||
params: PauseStack;
|
||||
} | {
|
||||
type: "UnpauseStack";
|
||||
params: UnpauseStack;
|
||||
} | {
|
||||
type: "StopStack";
|
||||
params: StopStack;
|
||||
} | {
|
||||
type: "DestroyStack";
|
||||
params: DestroyStack;
|
||||
} | {
|
||||
type: "BatchDestroyStack";
|
||||
params: BatchDestroyStack;
|
||||
} | {
|
||||
type: "RunStackService";
|
||||
params: RunStackService;
|
||||
} | {
|
||||
type: "TestAlerter";
|
||||
params: TestAlerter;
|
||||
} | {
|
||||
type: "SendAlert";
|
||||
params: SendAlert;
|
||||
type: "RemoveSwarmSecrets";
|
||||
params: RemoveSwarmSecrets;
|
||||
} | {
|
||||
type: "ClearRepoCache";
|
||||
params: ClearRepoCache;
|
||||
@@ -1344,7 +1364,19 @@ export declare enum TerminationSignal {
|
||||
SigTerm = "SIGTERM"
|
||||
}
|
||||
export interface DeploymentConfig {
|
||||
/** The id of server the deployment is deployed on. */
|
||||
/**
|
||||
* The Swarm to deploy the Deployment on (as a Swarm Service), setting the Deployment into Swarm mode.
|
||||
*
|
||||
* Note. If both swarm_id and server_id are set,
|
||||
* swarm_id overrides server_id and the Deployment will be in Swarm mode.
|
||||
*/
|
||||
swarm_id?: string;
|
||||
/**
|
||||
* The Server to deploy the Deployment on, setting the Deployment into Container mode.
|
||||
*
|
||||
* Note. If both swarm_id and server_id are set,
|
||||
* swarm_id overrides server_id and the Deployment will be in Swarm mode.
|
||||
*/
|
||||
server_id?: string;
|
||||
/**
|
||||
* The image which the deployment deploys.
|
||||
@@ -1391,18 +1423,29 @@ export interface DeploymentConfig {
|
||||
* Empty is no command.
|
||||
*/
|
||||
command?: string;
|
||||
/**
|
||||
* The number of replicas for the Service.
|
||||
*
|
||||
* Note. Only used in Swarm mode.
|
||||
*/
|
||||
replicas: number;
|
||||
/** The default termination signal to use to stop the deployment. Defaults to SigTerm (default docker signal). */
|
||||
termination_signal?: TerminationSignal;
|
||||
/** The termination timeout. */
|
||||
termination_timeout: number;
|
||||
/**
|
||||
* Extra args which are interpolated into the `docker run` command,
|
||||
* Extra args which are interpolated into the
|
||||
* `docker run` / `docker service create` command,
|
||||
* and affect the container configuration.
|
||||
*
|
||||
* - Container ref: https://docs.docker.com/reference/cli/docker/container/run/#options
|
||||
* - Swarm Service ref: https://docs.docker.com/reference/cli/docker/service/create/#options
|
||||
*/
|
||||
extra_args?: string[];
|
||||
/**
|
||||
* Labels attached to various termination signal options.
|
||||
* Used to specify different shutdown functionality depending on the termination signal.
|
||||
* Used to specify different shutdown functionality depending
|
||||
* on the termination signal.
|
||||
*/
|
||||
term_signal_labels?: string;
|
||||
/**
|
||||
@@ -1416,7 +1459,7 @@ export interface DeploymentConfig {
|
||||
* Maps files / folders on host to files / folders in container.
|
||||
*/
|
||||
volumes?: string;
|
||||
/** The environment variables passed to the container. */
|
||||
/** The environment variables passed to the container / service. */
|
||||
environment?: string;
|
||||
/** The docker labels given to the container. */
|
||||
labels?: string;
|
||||
@@ -2394,27 +2437,43 @@ export interface StackFileDependency {
|
||||
}
|
||||
/** The compose file configuration. */
|
||||
export interface StackConfig {
|
||||
/** The server to deploy the stack on. */
|
||||
/**
|
||||
* The Swarm to deploy the Stack on, setting the Stack into Swarm mode.
|
||||
*
|
||||
* Note. If both swarm_id and server_id are set,
|
||||
* swarm_id overrides server_id and the Stack will be in Swarm mode.
|
||||
*/
|
||||
swarm_id?: string;
|
||||
/**
|
||||
* The Server to deploy the Stack on, setting the Stack into Compose mode.
|
||||
*
|
||||
* Note. If both swarm_id and server_id are set,
|
||||
* swarm_id overrides server_id and the Stack will be in Swarm mode.
|
||||
*/
|
||||
server_id?: string;
|
||||
/** Configure quick links that are displayed in the resource header */
|
||||
links?: string[];
|
||||
/**
|
||||
* Optionally specify a custom project name for the stack.
|
||||
* If this is empty string, it will default to the stack name.
|
||||
* Used with `docker compose -p {project_name}`.
|
||||
* Used with `docker compose -p {project_name}` / `docker stack deploy {project_name}`.
|
||||
*
|
||||
* Note. Can be used to import pre-existing stacks.
|
||||
* Note. Can be used to import pre-existing stacks with names that do not match Stack name.
|
||||
*/
|
||||
project_name?: string;
|
||||
/**
|
||||
* Whether to automatically `compose pull` before redeploying stack.
|
||||
* Ensured latest images are deployed.
|
||||
* Will fail if the compose file specifies a locally build image.
|
||||
*
|
||||
* Note. Not used in Swarm mode.
|
||||
*/
|
||||
auto_pull: boolean;
|
||||
/**
|
||||
* Whether to `docker compose build` before `compose down` / `compose up`.
|
||||
* Combine with build_extra_args for custom behaviors.
|
||||
*
|
||||
* Note. Not used in Swarm mode.
|
||||
*/
|
||||
run_build?: boolean;
|
||||
/** Whether to poll for any updates to the images. */
|
||||
@@ -2501,6 +2560,8 @@ export interface StackConfig {
|
||||
* The name of the written environment file before `docker compose up`.
|
||||
* Relative to the run directory root.
|
||||
* Default: .env
|
||||
*
|
||||
* Note. Not used in Swarm mode.
|
||||
*/
|
||||
env_file_path: string;
|
||||
/**
|
||||
@@ -2532,7 +2593,11 @@ export interface StackConfig {
|
||||
/** The optional command to run after the Stack is deployed. */
|
||||
post_deploy?: SystemCommand;
|
||||
/**
|
||||
* The extra arguments to pass after `docker compose up -d`.
|
||||
* The extra arguments to pass to the deploy command.
|
||||
*
|
||||
* - For Compose stack, uses `docker compose up -d [EXTRA_ARGS]`.
|
||||
* - For Swarm mode. `docker stack deploy [EXTRA_ARGS] STACK_NAME`
|
||||
*
|
||||
* If empty, no extra arguments will be passed.
|
||||
*/
|
||||
extra_args?: string[];
|
||||
@@ -2540,6 +2605,8 @@ export interface StackConfig {
|
||||
* The extra arguments to pass after `docker compose build`.
|
||||
* If empty, no extra build arguments will be passed.
|
||||
* Only used if `run_build: true`
|
||||
*
|
||||
* Note. Not used in Swarm mode.
|
||||
*/
|
||||
build_extra_args?: string[];
|
||||
/**
|
||||
@@ -2571,6 +2638,8 @@ export interface StackConfig {
|
||||
* which is given relative to the run directory.
|
||||
*
|
||||
* If it is empty, no file will be written.
|
||||
*
|
||||
* Note. Not used in Swarm mode.
|
||||
*/
|
||||
environment?: string;
|
||||
}
|
||||
@@ -2600,6 +2669,9 @@ export interface StackServiceNames {
|
||||
*
|
||||
* This stores only 1. and 2., ie stacko-mongo.
|
||||
* Containers will be matched via regex like `^container_name-?[0-9]*$``
|
||||
*
|
||||
* Note. Setting container_name is not supported by Swarm,
|
||||
* so will always be 1. and 2. in Swarm mode.
|
||||
*/
|
||||
container_name: string;
|
||||
/** The services image. */
|
||||
@@ -4289,6 +4361,14 @@ export interface SwarmService {
|
||||
JobStatus?: ServiceJobStatus;
|
||||
}
|
||||
export type InspectSwarmServiceResponse = SwarmService;
|
||||
export declare enum SwarmState {
|
||||
/** The Swarm is healthy, all nodes OK */
|
||||
Healthy = "Healthy",
|
||||
/** The Swarm is unhealthy */
|
||||
Unhealthy = "Unhealthy",
|
||||
/** Unknown case */
|
||||
Unknown = "Unknown"
|
||||
}
|
||||
/**
|
||||
* Swarm stack service list item.
|
||||
* Returned by `docker stack services --format json <NAME>`
|
||||
@@ -4296,8 +4376,9 @@ export type InspectSwarmServiceResponse = SwarmService;
|
||||
* https://docs.docker.com/reference/cli/docker/stack/services/#format
|
||||
*/
|
||||
export interface SwarmStackServiceListItem {
|
||||
/** The *short* swarm service ID */
|
||||
ID?: string;
|
||||
/** Swarm stack task name. */
|
||||
/** The service name. */
|
||||
Name?: string;
|
||||
/** The service mode. */
|
||||
Mode?: string;
|
||||
@@ -4315,6 +4396,7 @@ export interface SwarmStackServiceListItem {
|
||||
* https://docs.docker.com/reference/cli/docker/stack/ps/#format
|
||||
*/
|
||||
export interface SwarmStackTaskListItem {
|
||||
/** The task ID */
|
||||
ID?: string;
|
||||
/** Swarm stack task name. */
|
||||
Name?: string;
|
||||
@@ -4322,7 +4404,9 @@ export interface SwarmStackTaskListItem {
|
||||
Image?: string;
|
||||
/** The node the task is running on */
|
||||
Node?: string;
|
||||
/** The task desired state. Matches 'CurrentState' when healthy. */
|
||||
DesiredState?: string;
|
||||
/** The task current state. Matches 'DesiredState' when healthy. */
|
||||
CurrentState?: string;
|
||||
/** An error message, if one exists */
|
||||
Error?: string;
|
||||
@@ -4337,15 +4421,23 @@ export interface SwarmStackTaskListItem {
|
||||
* docker stack ps --format json <STACK>
|
||||
* ```
|
||||
*/
|
||||
export interface SwarmStackLists {
|
||||
export interface SwarmStack {
|
||||
/** Swarm stack name. */
|
||||
Name: string;
|
||||
/**
|
||||
* Swarm stack state.
|
||||
* - Healthy if all associated tasks match their desired state (or report no desired state)
|
||||
* - Unhealthy otherwise
|
||||
*
|
||||
* Not included in docker cli return, computed by Komodo
|
||||
*/
|
||||
State: SwarmState;
|
||||
/** Services part of the stack */
|
||||
Services: SwarmStackServiceListItem[];
|
||||
/** Tasks part of the stack */
|
||||
Tasks: SwarmStackTaskListItem[];
|
||||
}
|
||||
export type InspectSwarmStackResponse = SwarmStackLists;
|
||||
export type InspectSwarmStackResponse = SwarmStack;
|
||||
export declare enum TaskState {
|
||||
NEW = "new",
|
||||
ALLOCATED = "allocated",
|
||||
@@ -4985,6 +5077,14 @@ export type ListSwarmServicesResponse = SwarmServiceListItem[];
|
||||
export interface SwarmStackListItem {
|
||||
/** Swarm stack name. */
|
||||
Name?: string;
|
||||
/**
|
||||
* Swarm stack state.
|
||||
* - Healthy if all associated tasks match their desired state
|
||||
* - Unhealthy otherwise
|
||||
*
|
||||
* Not included in docker cli return, computed by Komodo
|
||||
*/
|
||||
State?: SwarmState;
|
||||
/** Number of services which are part of the stack */
|
||||
Services?: string;
|
||||
/** The stack orchestrator */
|
||||
@@ -5011,14 +5111,6 @@ export interface SwarmTaskListItem {
|
||||
UpdatedAt?: string;
|
||||
}
|
||||
export type ListSwarmTasksResponse = SwarmTaskListItem[];
|
||||
export declare enum SwarmState {
|
||||
/** Unknown case */
|
||||
Unknown = "Unknown",
|
||||
/** The Swarm is healthy, all nodes OK */
|
||||
Healthy = "Healthy",
|
||||
/** The Swarm is unhealthy */
|
||||
Unhealthy = "Unhealthy"
|
||||
}
|
||||
export interface SwarmListItemInfo {
|
||||
/** Servers part of the swarm */
|
||||
server_ids: string[];
|
||||
@@ -6451,6 +6543,8 @@ export interface DeployStack {
|
||||
/**
|
||||
* Filter to only deploy specific services.
|
||||
* If empty, will deploy all services.
|
||||
*
|
||||
* Note. For Swarm mode Stacks, this field is not supported and will be ignored.
|
||||
*/
|
||||
services?: string[];
|
||||
/**
|
||||
@@ -8216,6 +8310,65 @@ export interface RefreshStackCache {
|
||||
/** Id or name */
|
||||
stack: string;
|
||||
}
|
||||
/**
|
||||
* `docker config rm CONFIG [CONFIG...]`
|
||||
*
|
||||
* https://docs.docker.com/reference/cli/docker/config/rm/
|
||||
*/
|
||||
export interface RemoveSwarmConfigs {
|
||||
/** Name or id */
|
||||
swarm: string;
|
||||
/** Config names or ids */
|
||||
configs: string[];
|
||||
}
|
||||
/**
|
||||
* `docker node rm [OPTIONS] NODE [NODE...]`
|
||||
*
|
||||
* https://docs.docker.com/reference/cli/docker/node/rm/
|
||||
*/
|
||||
export interface RemoveSwarmNodes {
|
||||
/** Name or id */
|
||||
swarm: string;
|
||||
/** Node names or ids to remove */
|
||||
nodes: string[];
|
||||
/** Force remove a node from the swarm */
|
||||
force?: boolean;
|
||||
}
|
||||
/**
|
||||
* `docker secret rm SECRET [SECRET...]`
|
||||
*
|
||||
* https://docs.docker.com/reference/cli/docker/secret/rm/
|
||||
*/
|
||||
export interface RemoveSwarmSecrets {
|
||||
/** Name or id */
|
||||
swarm: string;
|
||||
/** Secret names or ids */
|
||||
secrets: string[];
|
||||
}
|
||||
/**
|
||||
* `docker service rm SERVICE [SERVICE...]`
|
||||
*
|
||||
* https://docs.docker.com/reference/cli/docker/service/rm/
|
||||
*/
|
||||
export interface RemoveSwarmServices {
|
||||
/** Name or id */
|
||||
swarm: string;
|
||||
/** Service names or ids */
|
||||
services: string[];
|
||||
}
|
||||
/**
|
||||
* `docker stack rm [OPTIONS] STACK [STACK...]`
|
||||
*
|
||||
* https://docs.docker.com/reference/cli/docker/stack/rm/
|
||||
*/
|
||||
export interface RemoveSwarmStacks {
|
||||
/** Name or id */
|
||||
swarm: string;
|
||||
/** Node names to remove */
|
||||
stacks: string[];
|
||||
/** Do not wait for stack removal */
|
||||
detach: boolean;
|
||||
}
|
||||
/** **Admin only.** Remove a user from a user group. Response: [UserGroup] */
|
||||
export interface RemoveUserFromUserGroup {
|
||||
/** The name or id of UserGroup that user should be removed from. */
|
||||
@@ -9396,69 +9549,6 @@ export declare enum DayOfWeek {
|
||||
Sunday = "Sunday"
|
||||
}
|
||||
export type ExecuteRequest = {
|
||||
type: "StartContainer";
|
||||
params: StartContainer;
|
||||
} | {
|
||||
type: "RestartContainer";
|
||||
params: RestartContainer;
|
||||
} | {
|
||||
type: "PauseContainer";
|
||||
params: PauseContainer;
|
||||
} | {
|
||||
type: "UnpauseContainer";
|
||||
params: UnpauseContainer;
|
||||
} | {
|
||||
type: "StopContainer";
|
||||
params: StopContainer;
|
||||
} | {
|
||||
type: "DestroyContainer";
|
||||
params: DestroyContainer;
|
||||
} | {
|
||||
type: "StartAllContainers";
|
||||
params: StartAllContainers;
|
||||
} | {
|
||||
type: "RestartAllContainers";
|
||||
params: RestartAllContainers;
|
||||
} | {
|
||||
type: "PauseAllContainers";
|
||||
params: PauseAllContainers;
|
||||
} | {
|
||||
type: "UnpauseAllContainers";
|
||||
params: UnpauseAllContainers;
|
||||
} | {
|
||||
type: "StopAllContainers";
|
||||
params: StopAllContainers;
|
||||
} | {
|
||||
type: "PruneContainers";
|
||||
params: PruneContainers;
|
||||
} | {
|
||||
type: "DeleteNetwork";
|
||||
params: DeleteNetwork;
|
||||
} | {
|
||||
type: "PruneNetworks";
|
||||
params: PruneNetworks;
|
||||
} | {
|
||||
type: "DeleteImage";
|
||||
params: DeleteImage;
|
||||
} | {
|
||||
type: "PruneImages";
|
||||
params: PruneImages;
|
||||
} | {
|
||||
type: "DeleteVolume";
|
||||
params: DeleteVolume;
|
||||
} | {
|
||||
type: "PruneVolumes";
|
||||
params: PruneVolumes;
|
||||
} | {
|
||||
type: "PruneDockerBuilders";
|
||||
params: PruneDockerBuilders;
|
||||
} | {
|
||||
type: "PruneBuildx";
|
||||
params: PruneBuildx;
|
||||
} | {
|
||||
type: "PruneSystem";
|
||||
params: PruneSystem;
|
||||
} | {
|
||||
type: "DeployStack";
|
||||
params: DeployStack;
|
||||
} | {
|
||||
@@ -9581,6 +9671,84 @@ export type ExecuteRequest = {
|
||||
} | {
|
||||
type: "SendAlert";
|
||||
params: SendAlert;
|
||||
} | {
|
||||
type: "StartContainer";
|
||||
params: StartContainer;
|
||||
} | {
|
||||
type: "RestartContainer";
|
||||
params: RestartContainer;
|
||||
} | {
|
||||
type: "PauseContainer";
|
||||
params: PauseContainer;
|
||||
} | {
|
||||
type: "UnpauseContainer";
|
||||
params: UnpauseContainer;
|
||||
} | {
|
||||
type: "StopContainer";
|
||||
params: StopContainer;
|
||||
} | {
|
||||
type: "DestroyContainer";
|
||||
params: DestroyContainer;
|
||||
} | {
|
||||
type: "StartAllContainers";
|
||||
params: StartAllContainers;
|
||||
} | {
|
||||
type: "RestartAllContainers";
|
||||
params: RestartAllContainers;
|
||||
} | {
|
||||
type: "PauseAllContainers";
|
||||
params: PauseAllContainers;
|
||||
} | {
|
||||
type: "UnpauseAllContainers";
|
||||
params: UnpauseAllContainers;
|
||||
} | {
|
||||
type: "StopAllContainers";
|
||||
params: StopAllContainers;
|
||||
} | {
|
||||
type: "PruneContainers";
|
||||
params: PruneContainers;
|
||||
} | {
|
||||
type: "DeleteNetwork";
|
||||
params: DeleteNetwork;
|
||||
} | {
|
||||
type: "PruneNetworks";
|
||||
params: PruneNetworks;
|
||||
} | {
|
||||
type: "DeleteImage";
|
||||
params: DeleteImage;
|
||||
} | {
|
||||
type: "PruneImages";
|
||||
params: PruneImages;
|
||||
} | {
|
||||
type: "DeleteVolume";
|
||||
params: DeleteVolume;
|
||||
} | {
|
||||
type: "PruneVolumes";
|
||||
params: PruneVolumes;
|
||||
} | {
|
||||
type: "PruneDockerBuilders";
|
||||
params: PruneDockerBuilders;
|
||||
} | {
|
||||
type: "PruneBuildx";
|
||||
params: PruneBuildx;
|
||||
} | {
|
||||
type: "PruneSystem";
|
||||
params: PruneSystem;
|
||||
} | {
|
||||
type: "RemoveSwarmNodes";
|
||||
params: RemoveSwarmNodes;
|
||||
} | {
|
||||
type: "RemoveSwarmStacks";
|
||||
params: RemoveSwarmStacks;
|
||||
} | {
|
||||
type: "RemoveSwarmServices";
|
||||
params: RemoveSwarmServices;
|
||||
} | {
|
||||
type: "RemoveSwarmConfigs";
|
||||
params: RemoveSwarmConfigs;
|
||||
} | {
|
||||
type: "RemoveSwarmSecrets";
|
||||
params: RemoveSwarmSecrets;
|
||||
} | {
|
||||
type: "ClearRepoCache";
|
||||
params: ClearRepoCache;
|
||||
|
||||
@@ -69,6 +69,11 @@ export var Operation;
|
||||
Operation["UpdateSwarm"] = "UpdateSwarm";
|
||||
Operation["RenameSwarm"] = "RenameSwarm";
|
||||
Operation["DeleteSwarm"] = "DeleteSwarm";
|
||||
Operation["RemoveSwarmNodes"] = "RemoveSwarmNodes";
|
||||
Operation["RemoveSwarmStacks"] = "RemoveSwarmStacks";
|
||||
Operation["RemoveSwarmServices"] = "RemoveSwarmServices";
|
||||
Operation["RemoveSwarmConfigs"] = "RemoveSwarmConfigs";
|
||||
Operation["RemoveSwarmSecrets"] = "RemoveSwarmSecrets";
|
||||
Operation["CreateServer"] = "CreateServer";
|
||||
Operation["UpdateServer"] = "UpdateServer";
|
||||
Operation["UpdateServerKey"] = "UpdateServerKey";
|
||||
@@ -606,6 +611,15 @@ export var ServiceUpdateStatusStateEnum;
|
||||
ServiceUpdateStatusStateEnum["ROLLBACK_PAUSED"] = "rollback_paused";
|
||||
ServiceUpdateStatusStateEnum["ROLLBACK_COMPLETED"] = "rollback_completed";
|
||||
})(ServiceUpdateStatusStateEnum || (ServiceUpdateStatusStateEnum = {}));
|
||||
export var SwarmState;
|
||||
(function (SwarmState) {
|
||||
/** The Swarm is healthy, all nodes OK */
|
||||
SwarmState["Healthy"] = "Healthy";
|
||||
/** The Swarm is unhealthy */
|
||||
SwarmState["Unhealthy"] = "Unhealthy";
|
||||
/** Unknown case */
|
||||
SwarmState["Unknown"] = "Unknown";
|
||||
})(SwarmState || (SwarmState = {}));
|
||||
export var TaskState;
|
||||
(function (TaskState) {
|
||||
TaskState["NEW"] = "new";
|
||||
@@ -704,15 +718,6 @@ export var StackState;
|
||||
/** Server not reachable for status */
|
||||
StackState["Unknown"] = "unknown";
|
||||
})(StackState || (StackState = {}));
|
||||
export var SwarmState;
|
||||
(function (SwarmState) {
|
||||
/** Unknown case */
|
||||
SwarmState["Unknown"] = "Unknown";
|
||||
/** The Swarm is healthy, all nodes OK */
|
||||
SwarmState["Healthy"] = "Healthy";
|
||||
/** The Swarm is unhealthy */
|
||||
SwarmState["Unhealthy"] = "Unhealthy";
|
||||
})(SwarmState || (SwarmState = {}));
|
||||
/**
|
||||
* Configures the behavior of [CreateTerminal] if the
|
||||
* specified terminal name already exists.
|
||||
|
||||
@@ -162,36 +162,43 @@ export const Config = <T,>({
|
||||
<div className="flex gap-6">
|
||||
{!disableSidebar && (
|
||||
<div className="hidden xl:block relative pr-6 border-r">
|
||||
<div className="sticky top-24 hidden xl:flex flex-col gap-8 w-[140px] h-fit pb-24">
|
||||
{sections.map((section) => (
|
||||
<div key={section}>
|
||||
{section && (
|
||||
<p className="text-muted-foreground uppercase text-right mb-2">
|
||||
{section}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex flex-col gap-2">
|
||||
{components[section] &&
|
||||
components[section]
|
||||
.filter((item) => !item.hidden)
|
||||
.map((item) => (
|
||||
// uses a tags becasue react-router-dom Links don't reliably hash scroll
|
||||
<a
|
||||
href={"#" + section + item.label}
|
||||
key={section + item.label}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="justify-end w-full"
|
||||
size="sm"
|
||||
<div className="sticky top-24 hidden xl:flex flex-col gap-4 w-[140px] pb-24">
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col gap-8 h-fit overflow-auto max-h-[calc(100vh-130px)]",
|
||||
changesMade && "max-h-[calc(100vh-220px)]"
|
||||
)}
|
||||
>
|
||||
{sections.map((section) => (
|
||||
<div key={section}>
|
||||
{section && (
|
||||
<p className="text-muted-foreground uppercase text-right mb-2">
|
||||
{section}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex flex-col gap-2">
|
||||
{components[section] &&
|
||||
components[section]
|
||||
.filter((item) => !item.hidden)
|
||||
.map((item) => (
|
||||
// uses a tags becasue react-router-dom Links don't reliably hash scroll
|
||||
<a
|
||||
href={"#" + section + item.label}
|
||||
key={section + item.label}
|
||||
>
|
||||
{item.label}
|
||||
</Button>
|
||||
</a>
|
||||
))}
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="justify-end w-full"
|
||||
size="sm"
|
||||
>
|
||||
{item.label}
|
||||
</Button>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
{changesMade && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<ConfirmUpdate
|
||||
|
||||
@@ -94,6 +94,11 @@ type MinExecutionType = Exclude<
|
||||
| "DeleteImage"
|
||||
| "DeleteVolume"
|
||||
| "TestAlerter"
|
||||
| "RemoveSwarmNodes"
|
||||
| "RemoveSwarmStacks"
|
||||
| "RemoveSwarmServices"
|
||||
| "RemoveSwarmConfigs"
|
||||
| "RemoveSwarmSecrets"
|
||||
>;
|
||||
|
||||
type ExecutionConfigParams<T extends MinExecutionType> = Extract<
|
||||
|
||||
@@ -438,7 +438,7 @@ export const StackConfig = ({
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Command Wrapper",
|
||||
label: "Wrapper",
|
||||
description:
|
||||
"Optional wrapper to execute 'docker compose up -d' as a subcommand of tools like secrets management.",
|
||||
components: {
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { Section } from "@components/layouts";
|
||||
import { useRead } from "@lib/hooks";
|
||||
import { DataTable, SortableHeader } from "@ui/data-table";
|
||||
import { Dispatch, ReactNode, SetStateAction } from "react";
|
||||
import { Search } from "lucide-react";
|
||||
import { Input } from "@ui/input";
|
||||
import { filterBySplit } from "@lib/utils";
|
||||
import { SwarmLink } from "..";
|
||||
import { SwarmServicesTable } from "../table";
|
||||
|
||||
export const SwarmServices = ({
|
||||
id,
|
||||
@@ -16,86 +11,16 @@ export const SwarmServices = ({
|
||||
titleOther: ReactNode;
|
||||
_search: [string, Dispatch<SetStateAction<string>>];
|
||||
}) => {
|
||||
const [search, setSearch] = _search;
|
||||
const services =
|
||||
useRead("ListSwarmServices", { swarm: id }, { refetchInterval: 10_000 })
|
||||
.data ?? [];
|
||||
|
||||
const filtered = filterBySplit(
|
||||
services,
|
||||
search,
|
||||
(service) => service.Name ?? service.ID ?? "Unknown"
|
||||
);
|
||||
|
||||
return (
|
||||
<Section
|
||||
<SwarmServicesTable
|
||||
id={id}
|
||||
services={services}
|
||||
titleOther={titleOther}
|
||||
actions={
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<div className="relative">
|
||||
<Search className="w-4 absolute top-[50%] left-3 -translate-y-[50%] text-muted-foreground" />
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="search..."
|
||||
className="pl-8 w-[200px] lg:w-[300px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<DataTable
|
||||
containerClassName="min-h-[60vh]"
|
||||
tableKey="swarm-services"
|
||||
data={filtered}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "Name",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Name" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<SwarmLink
|
||||
type="Service"
|
||||
swarm_id={id}
|
||||
resource_id={row.original.ID}
|
||||
name={row.original.Name}
|
||||
/>
|
||||
),
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "ID",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Id" />
|
||||
),
|
||||
cell: ({ row }) => row.original.ID ?? "Unknown",
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "UpdatedAt",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Updated" />
|
||||
),
|
||||
cell: ({ row }) =>
|
||||
row.original.UpdatedAt
|
||||
? new Date(row.original.UpdatedAt).toLocaleString()
|
||||
: "Unknown",
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "CreatedAt",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Created" />
|
||||
),
|
||||
cell: ({ row }) =>
|
||||
row.original.CreatedAt
|
||||
? new Date(row.original.CreatedAt).toLocaleString()
|
||||
: "Unknown",
|
||||
size: 200,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Section>
|
||||
_search={_search}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -69,6 +69,12 @@ export const SwarmStacks = ({
|
||||
<SortableHeader column={column} title="Services" />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "State",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="State" />
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { Section } from "@components/layouts";
|
||||
import { useRead } from "@lib/hooks";
|
||||
import { DataTable, SortableHeader } from "@ui/data-table";
|
||||
import { Dispatch, ReactNode, SetStateAction } from "react";
|
||||
import { Search } from "lucide-react";
|
||||
import { Input } from "@ui/input";
|
||||
import { filterBySplit } from "@lib/utils";
|
||||
import { SwarmLink } from "..";
|
||||
import { SwarmTasksTable } from "../table";
|
||||
|
||||
export const SwarmTasks = ({
|
||||
id,
|
||||
@@ -16,134 +11,16 @@ export const SwarmTasks = ({
|
||||
titleOther: ReactNode;
|
||||
_search: [string, Dispatch<SetStateAction<string>>];
|
||||
}) => {
|
||||
const [search, setSearch] = _search;
|
||||
const nodes =
|
||||
useRead("ListSwarmNodes", { swarm: id }, { refetchInterval: 10_000 })
|
||||
.data ?? [];
|
||||
const services =
|
||||
useRead("ListSwarmServices", { swarm: id }, { refetchInterval: 10_000 })
|
||||
.data ?? [];
|
||||
const _tasks =
|
||||
const tasks =
|
||||
useRead("ListSwarmTasks", { swarm: id }, { refetchInterval: 10_000 })
|
||||
.data ?? [];
|
||||
|
||||
const tasks = _tasks.map((task) => {
|
||||
return {
|
||||
...task,
|
||||
node: nodes.find((node) => task.NodeID === node.ID),
|
||||
service: services.find((service) => task.ServiceID === service.ID),
|
||||
};
|
||||
});
|
||||
|
||||
const filtered = filterBySplit(
|
||||
tasks,
|
||||
search,
|
||||
(task) => task.Name ?? task.service?.Name ?? "Unknown"
|
||||
);
|
||||
|
||||
return (
|
||||
<Section
|
||||
<SwarmTasksTable
|
||||
id={id}
|
||||
tasks={tasks}
|
||||
titleOther={titleOther}
|
||||
actions={
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<div className="relative">
|
||||
<Search className="w-4 absolute top-[50%] left-3 -translate-y-[50%] text-muted-foreground" />
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="search..."
|
||||
className="pl-8 w-[200px] lg:w-[300px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<DataTable
|
||||
containerClassName="min-h-[60vh]"
|
||||
tableKey="swarm-tasks"
|
||||
data={filtered}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "ID",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Id" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<SwarmLink
|
||||
type="Task"
|
||||
swarm_id={id}
|
||||
resource_id={row.original.ID}
|
||||
name={row.original.ID}
|
||||
/>
|
||||
),
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "service.Name",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Service" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<SwarmLink
|
||||
type="Service"
|
||||
swarm_id={id}
|
||||
resource_id={row.original.service?.ID}
|
||||
name={row.original.service?.Name}
|
||||
/>
|
||||
),
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "node.Hostname",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Node" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<SwarmLink
|
||||
type="Node"
|
||||
swarm_id={id}
|
||||
resource_id={row.original.node?.ID}
|
||||
name={row.original.node?.Hostname}
|
||||
/>
|
||||
),
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "State",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="State" />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "DesiredState",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Desired State" />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "UpdatedAt",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Updated" />
|
||||
),
|
||||
cell: ({ row }) =>
|
||||
row.original.UpdatedAt
|
||||
? new Date(row.original.UpdatedAt).toLocaleString()
|
||||
: "Unknown",
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "CreatedAt",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Created" />
|
||||
),
|
||||
cell: ({ row }) =>
|
||||
row.original.CreatedAt
|
||||
? new Date(row.original.CreatedAt).toLocaleString()
|
||||
: "Unknown",
|
||||
size: 200,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Section>
|
||||
_search={_search}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { DataTable, SortableHeader } from "@ui/data-table";
|
||||
import { ResourceLink } from "../common";
|
||||
import { TableTags } from "@components/tags";
|
||||
import { SwarmComponents } from ".";
|
||||
import { SwarmComponents, SwarmLink } from ".";
|
||||
import { Types } from "komodo_client";
|
||||
import { useSelectedResources } from "@lib/hooks";
|
||||
import { useRead, useSelectedResources } from "@lib/hooks";
|
||||
import { Dispatch, ReactNode, SetStateAction } from "react";
|
||||
import { filterBySplit } from "@lib/utils";
|
||||
import { Section } from "@components/layouts";
|
||||
import { Search } from "lucide-react";
|
||||
import { Input } from "@ui/input";
|
||||
|
||||
export const SwarmTable = ({ swarms }: { swarms: Types.SwarmListItem[] }) => {
|
||||
const [_, setSelectedResources] = useSelectedResources("Swarm");
|
||||
@@ -41,3 +46,437 @@ export const SwarmTable = ({ swarms }: { swarms: Types.SwarmListItem[] }) => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const SwarmServicesTable = ({
|
||||
id,
|
||||
services,
|
||||
titleOther,
|
||||
_search,
|
||||
}: {
|
||||
id: string;
|
||||
services: Types.SwarmServiceListItem[];
|
||||
titleOther: ReactNode;
|
||||
_search: [string, Dispatch<SetStateAction<string>>];
|
||||
}) => {
|
||||
const [search, setSearch] = _search;
|
||||
const filtered = filterBySplit(
|
||||
services,
|
||||
search,
|
||||
(service) => service.Name ?? service.ID ?? "Unknown"
|
||||
);
|
||||
return (
|
||||
<Section
|
||||
titleOther={titleOther}
|
||||
actions={
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<div className="relative">
|
||||
<Search className="w-4 absolute top-[50%] left-3 -translate-y-[50%] text-muted-foreground" />
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="search..."
|
||||
className="pl-8 w-[200px] lg:w-[300px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<DataTable
|
||||
containerClassName="min-h-[60vh]"
|
||||
tableKey="swarm-services"
|
||||
data={filtered}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "Name",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Name" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<SwarmLink
|
||||
type="Service"
|
||||
swarm_id={id}
|
||||
resource_id={row.original.Name}
|
||||
name={row.original.Name}
|
||||
/>
|
||||
),
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "ID",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Id" />
|
||||
),
|
||||
cell: ({ row }) => row.original.ID ?? "Unknown",
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "UpdatedAt",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Updated" />
|
||||
),
|
||||
cell: ({ row }) =>
|
||||
row.original.UpdatedAt
|
||||
? new Date(row.original.UpdatedAt).toLocaleString()
|
||||
: "Unknown",
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "CreatedAt",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Created" />
|
||||
),
|
||||
cell: ({ row }) =>
|
||||
row.original.CreatedAt
|
||||
? new Date(row.original.CreatedAt).toLocaleString()
|
||||
: "Unknown",
|
||||
size: 200,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
export const SwarmStackServicesTable = ({
|
||||
id,
|
||||
services,
|
||||
titleOther,
|
||||
_search,
|
||||
}: {
|
||||
id: string;
|
||||
services: Types.SwarmStackServiceListItem[];
|
||||
titleOther: ReactNode;
|
||||
_search: [string, Dispatch<SetStateAction<string>>];
|
||||
}) => {
|
||||
const [search, setSearch] = _search;
|
||||
const filtered = filterBySplit(
|
||||
services,
|
||||
search,
|
||||
(service) => service.Name ?? service.ID ?? "Unknown"
|
||||
);
|
||||
return (
|
||||
<Section
|
||||
titleOther={titleOther}
|
||||
actions={
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<div className="relative">
|
||||
<Search className="w-4 absolute top-[50%] left-3 -translate-y-[50%] text-muted-foreground" />
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="search..."
|
||||
className="pl-8 w-[200px] lg:w-[300px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<DataTable
|
||||
containerClassName="min-h-[60vh]"
|
||||
tableKey="swarm-services"
|
||||
data={filtered}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "Name",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Name" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<SwarmLink
|
||||
type="Service"
|
||||
swarm_id={id}
|
||||
resource_id={row.original.Name}
|
||||
name={row.original.Name}
|
||||
/>
|
||||
),
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "ID",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Id" />
|
||||
),
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "Image",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Image" />
|
||||
),
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "Mode",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Mode" />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "Replicas",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Replicas" />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "Ports",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Ports" />
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
export const SwarmTasksTable = ({
|
||||
id,
|
||||
tasks: _tasks,
|
||||
titleOther,
|
||||
_search,
|
||||
}: {
|
||||
id: string;
|
||||
tasks: Types.SwarmTaskListItem[];
|
||||
titleOther: ReactNode;
|
||||
_search: [string, Dispatch<SetStateAction<string>>];
|
||||
}) => {
|
||||
const [search, setSearch] = _search;
|
||||
|
||||
const nodes =
|
||||
useRead("ListSwarmNodes", { swarm: id }, { refetchInterval: 10_000 })
|
||||
.data ?? [];
|
||||
const services =
|
||||
useRead("ListSwarmServices", { swarm: id }, { refetchInterval: 10_000 })
|
||||
.data ?? [];
|
||||
const tasks = _tasks.map((task) => {
|
||||
return {
|
||||
...task,
|
||||
node: nodes.find((node) => task.NodeID === node.ID),
|
||||
service: services.find((service) => task.ServiceID === service.ID),
|
||||
};
|
||||
});
|
||||
|
||||
const filtered = filterBySplit(
|
||||
tasks,
|
||||
search,
|
||||
(task) =>
|
||||
task.Name ?? task.service?.Name ?? task.node?.Hostname ?? "Unknown"
|
||||
);
|
||||
|
||||
return (
|
||||
<Section
|
||||
titleOther={titleOther}
|
||||
actions={
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<div className="relative">
|
||||
<Search className="w-4 absolute top-[50%] left-3 -translate-y-[50%] text-muted-foreground" />
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="search..."
|
||||
className="pl-8 w-[200px] lg:w-[300px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<DataTable
|
||||
containerClassName="min-h-[60vh]"
|
||||
tableKey="swarm-services"
|
||||
data={filtered}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "ID",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Id" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<SwarmLink
|
||||
type="Task"
|
||||
swarm_id={id}
|
||||
resource_id={row.original.ID}
|
||||
name={row.original.ID}
|
||||
/>
|
||||
),
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "service.Name",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Service" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<SwarmLink
|
||||
type="Service"
|
||||
swarm_id={id}
|
||||
resource_id={row.original.service?.ID}
|
||||
name={row.original.service?.Name}
|
||||
/>
|
||||
),
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "node.Hostname",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Node" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<SwarmLink
|
||||
type="Node"
|
||||
swarm_id={id}
|
||||
resource_id={row.original.node?.ID}
|
||||
name={row.original.node?.Hostname}
|
||||
/>
|
||||
),
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "State",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="State" />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "DesiredState",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Desired State" />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "UpdatedAt",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Updated" />
|
||||
),
|
||||
cell: ({ row }) =>
|
||||
row.original.UpdatedAt
|
||||
? new Date(row.original.UpdatedAt).toLocaleString()
|
||||
: "Unknown",
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "CreatedAt",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Created" />
|
||||
),
|
||||
cell: ({ row }) =>
|
||||
row.original.CreatedAt
|
||||
? new Date(row.original.CreatedAt).toLocaleString()
|
||||
: "Unknown",
|
||||
size: 200,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
export const SwarmStackTasksTable = ({
|
||||
id,
|
||||
tasks: _tasks,
|
||||
titleOther,
|
||||
_search,
|
||||
}: {
|
||||
id: string;
|
||||
tasks: Types.SwarmStackTaskListItem[];
|
||||
titleOther: ReactNode;
|
||||
_search: [string, Dispatch<SetStateAction<string>>];
|
||||
}) => {
|
||||
const [search, setSearch] = _search;
|
||||
|
||||
const nodes =
|
||||
useRead("ListSwarmNodes", { swarm: id }, { refetchInterval: 10_000 })
|
||||
.data ?? [];
|
||||
const tasks = _tasks.map((task) => {
|
||||
return {
|
||||
...task,
|
||||
node: nodes.find(
|
||||
(node) =>
|
||||
(task.Node ?? false) &&
|
||||
(task.Node === node.ID ||
|
||||
task.Node === node.Hostname ||
|
||||
task.Node === node.Name)
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
const filtered = filterBySplit(
|
||||
tasks,
|
||||
search,
|
||||
(task) => task.Name ?? task.node?.Hostname ?? "Unknown"
|
||||
);
|
||||
|
||||
return (
|
||||
<Section
|
||||
titleOther={titleOther}
|
||||
actions={
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<div className="relative">
|
||||
<Search className="w-4 absolute top-[50%] left-3 -translate-y-[50%] text-muted-foreground" />
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="search..."
|
||||
className="pl-8 w-[200px] lg:w-[300px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<DataTable
|
||||
containerClassName="min-h-[60vh]"
|
||||
tableKey="swarm-tasks"
|
||||
data={filtered}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "ID",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Id" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<SwarmLink
|
||||
type="Task"
|
||||
swarm_id={id}
|
||||
resource_id={row.original.ID}
|
||||
name={row.original.ID}
|
||||
/>
|
||||
),
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "node.Hostname",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Node" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<SwarmLink
|
||||
type="Node"
|
||||
swarm_id={id}
|
||||
resource_id={row.original.node?.ID}
|
||||
name={row.original.node?.Hostname}
|
||||
/>
|
||||
),
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "Image",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Image" />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "CurrentState",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="State" />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "DesiredState",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Desired State" />
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -187,7 +187,19 @@ const on_update = (
|
||||
["ListSwarms"],
|
||||
["ListFullSwarms"],
|
||||
["GetSwarmsSummary"],
|
||||
["GetSwarm"]
|
||||
["GetSwarm"],
|
||||
["ListSwarmNodes"],
|
||||
["InspectSwarmNode"],
|
||||
["ListSwarmStacks"],
|
||||
["InspectSwarmStack"],
|
||||
["ListSwarmServices"],
|
||||
["InspectSwarmService"],
|
||||
["ListSwarmTasks"],
|
||||
["InspectSwarmTask"],
|
||||
["ListSwarmConfigs"],
|
||||
["InspectSwarmConfig"],
|
||||
["ListSwarmSecrets"],
|
||||
["InspectSwarmSecret"]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,10 +19,10 @@ import { Button } from "@ui/button";
|
||||
import { DataTable } from "@ui/data-table";
|
||||
import {
|
||||
ChevronLeft,
|
||||
Clapperboard,
|
||||
Info,
|
||||
Loader2,
|
||||
PlusCircle,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||
import { ContainerLogs } from "./log";
|
||||
@@ -169,7 +169,7 @@ const ContainerPageInner = ({
|
||||
<div className="mt-8 flex flex-col gap-12">
|
||||
{/* Actions */}
|
||||
{canExecute && (
|
||||
<Section title="Actions" icon={<Clapperboard className="w-4 h-4" />}>
|
||||
<Section title="Execute" icon={<Zap className="w-4 h-4" />}>
|
||||
<div className="flex gap-4 items-center flex-wrap">
|
||||
{Object.entries(Actions).map(([key, Action]) => (
|
||||
<Action key={key} id={id} container={container_name} />
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
} from "@lib/hooks";
|
||||
import { cn } from "@lib/utils";
|
||||
import { Types } from "komodo_client";
|
||||
import { ChevronLeft, Clapperboard, Layers2 } from "lucide-react";
|
||||
import { ChevronLeft, Layers2, Zap } from "lucide-react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { Button } from "@ui/button";
|
||||
import { ExportButton } from "@components/export";
|
||||
@@ -189,10 +189,7 @@ const StackServicePageInner = ({
|
||||
<div className="mt-8 flex flex-col gap-12">
|
||||
{/* Actions */}
|
||||
{canExecute && (
|
||||
<Section
|
||||
title="Actions (Service)"
|
||||
icon={<Clapperboard className="w-4 h-4" />}
|
||||
>
|
||||
<Section title="Execute (Service)" icon={<Zap className="w-4 h-4" />}>
|
||||
<div className="flex gap-4 items-center flex-wrap">
|
||||
{Object.entries(Actions).map(([key, Action]) => (
|
||||
<Action key={key} id={stack_id} service={service} />
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { ResourceLink } from "@components/resources/common";
|
||||
import { PageHeaderName } from "@components/util";
|
||||
import { useRead, useSetTitle } from "@lib/hooks";
|
||||
import { usePermissions, useRead, useSetTitle } from "@lib/hooks";
|
||||
import { Button } from "@ui/button";
|
||||
import { ChevronLeft, Loader2 } from "lucide-react";
|
||||
import { ChevronLeft, Loader2, Zap } from "lucide-react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { MonacoEditor } from "@components/monaco";
|
||||
import { SWARM_ICONS, useSwarm } from "@components/resources/swarm";
|
||||
import { Section } from "@components/layouts";
|
||||
import { RemoveSwarmResource } from "./remove";
|
||||
|
||||
export default function SwarmConfigPage() {
|
||||
const { id, config: __config } = useParams() as {
|
||||
@@ -22,6 +24,10 @@ export default function SwarmConfigPage() {
|
||||
useSetTitle(
|
||||
`${swarm?.name} | Config | ${config?.Spec?.Name ?? config?.ID ?? "Unknown"}`
|
||||
);
|
||||
const { canExecute } = usePermissions({
|
||||
type: "Swarm",
|
||||
id,
|
||||
});
|
||||
const nav = useNavigate();
|
||||
|
||||
if (isPending) {
|
||||
@@ -68,6 +74,19 @@ export default function SwarmConfigPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{canExecute && config.ID && (
|
||||
<Section title="Execute" icon={<Zap className="w-4 h-4" />}>
|
||||
<div className="flex gap-4 items-center flex-wrap">
|
||||
<RemoveSwarmResource
|
||||
id={id}
|
||||
type="Config"
|
||||
resource_id={config.ID}
|
||||
resource_name={config.Spec?.Name}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
<MonacoEditor
|
||||
value={JSON.stringify(config, null, 2)}
|
||||
language="json"
|
||||
|
||||
@@ -4,12 +4,12 @@ import { Log, LogSection } from "@components/log";
|
||||
import { ReactNode } from "react";
|
||||
import { Section } from "@components/layouts";
|
||||
|
||||
/* Can be used for service or task logs */
|
||||
export const SwarmServiceLogs = ({
|
||||
id,
|
||||
service,
|
||||
titleOther,
|
||||
disabled,
|
||||
extraParams,
|
||||
}: {
|
||||
/* Swarm id */
|
||||
id: string;
|
||||
@@ -17,6 +17,7 @@ export const SwarmServiceLogs = ({
|
||||
service: string;
|
||||
titleOther?: ReactNode;
|
||||
disabled: boolean;
|
||||
extraParams?: ReactNode;
|
||||
}) => {
|
||||
if (disabled) {
|
||||
return (
|
||||
@@ -27,7 +28,12 @@ export const SwarmServiceLogs = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<SwarmServiceLogsInner titleOther={titleOther} id={id} service={service} />
|
||||
<SwarmServiceLogsInner
|
||||
titleOther={titleOther}
|
||||
id={id}
|
||||
service={service}
|
||||
extraParams={extraParams}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -35,11 +41,13 @@ const SwarmServiceLogsInner = ({
|
||||
id,
|
||||
service,
|
||||
titleOther,
|
||||
extraParams,
|
||||
}: {
|
||||
/// Swarm id
|
||||
id: string;
|
||||
service: string;
|
||||
titleOther?: ReactNode;
|
||||
extraParams?: ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<LogSection
|
||||
@@ -50,6 +58,7 @@ const SwarmServiceLogsInner = ({
|
||||
search_logs={(timestamps, terms, invert, poll) =>
|
||||
SearchLogs(id, service, terms, invert, timestamps, poll)
|
||||
}
|
||||
extraParams={extraParams}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { ResourceLink } from "@components/resources/common";
|
||||
import { PageHeaderName } from "@components/util";
|
||||
import { useRead, useSetTitle } from "@lib/hooks";
|
||||
import { usePermissions, useRead, useSetTitle } from "@lib/hooks";
|
||||
import { Button } from "@ui/button";
|
||||
import { ChevronLeft, Loader2 } from "lucide-react";
|
||||
import { ChevronLeft, Loader2, Zap } from "lucide-react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { MonacoEditor } from "@components/monaco";
|
||||
import { SWARM_ICONS, useSwarm } from "@components/resources/swarm";
|
||||
import { Section } from "@components/layouts";
|
||||
import { RemoveSwarmResource } from "./remove";
|
||||
|
||||
export default function SwarmNodePage() {
|
||||
const { id, node: __node } = useParams() as {
|
||||
@@ -19,8 +21,12 @@ export default function SwarmNodePage() {
|
||||
node: _node,
|
||||
});
|
||||
useSetTitle(
|
||||
`${swarm?.name} | Node | ${node?.Spec?.Name ?? node?.ID ?? "Unknown"}`
|
||||
`${swarm?.name} | Node | ${node?.Spec?.Name ?? node?.Description?.Hostname ?? node?.ID ?? "Unknown"}`
|
||||
);
|
||||
const { canExecute } = usePermissions({
|
||||
type: "Swarm",
|
||||
id,
|
||||
});
|
||||
const nav = useNavigate();
|
||||
|
||||
if (isPending) {
|
||||
@@ -69,6 +75,19 @@ export default function SwarmNodePage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{canExecute && node.ID && (
|
||||
<Section title="Execute" icon={<Zap className="w-4 h-4" />}>
|
||||
<div className="flex gap-4 items-center flex-wrap">
|
||||
<RemoveSwarmResource
|
||||
id={id}
|
||||
type="Node"
|
||||
resource_id={node.ID}
|
||||
resource_name={node.Description?.Hostname}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
<MonacoEditor
|
||||
value={JSON.stringify(node, null, 2)}
|
||||
language="json"
|
||||
|
||||
41
frontend/src/pages/swarm/remove.tsx
Normal file
41
frontend/src/pages/swarm/remove.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { ActionWithDialog } from "@components/util";
|
||||
import { useExecute } from "@lib/hooks";
|
||||
import { Trash } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export type RemovableSwarmResourceType =
|
||||
| "Node"
|
||||
| "Stack"
|
||||
| "Service"
|
||||
| "Config"
|
||||
| "Secret";
|
||||
|
||||
export const RemoveSwarmResource = ({
|
||||
id,
|
||||
type,
|
||||
resource_id,
|
||||
resource_name,
|
||||
}: {
|
||||
id: string;
|
||||
type: RemovableSwarmResourceType;
|
||||
resource_id: string;
|
||||
resource_name?: string;
|
||||
}) => {
|
||||
const nav = useNavigate();
|
||||
const { mutate: remove, isPending } = useExecute(`RemoveSwarm${type}s`, {
|
||||
onSuccess: () => nav("/swarms/" + id),
|
||||
});
|
||||
let key = `${type.toLowerCase()}s`;
|
||||
return (
|
||||
<ActionWithDialog
|
||||
name={resource_name ?? resource_id}
|
||||
title="Remove"
|
||||
icon={<Trash className="h-4 w-4" />}
|
||||
onClick={() =>
|
||||
remove({ swarm: id, [key]: [resource_id], detach: false } as any)
|
||||
}
|
||||
disabled={isPending}
|
||||
loading={isPending}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,11 +1,13 @@
|
||||
import { ResourceLink } from "@components/resources/common";
|
||||
import { PageHeaderName } from "@components/util";
|
||||
import { useRead, useSetTitle } from "@lib/hooks";
|
||||
import { usePermissions, useRead, useSetTitle } from "@lib/hooks";
|
||||
import { Button } from "@ui/button";
|
||||
import { ChevronLeft, Loader2 } from "lucide-react";
|
||||
import { ChevronLeft, Loader2, Zap } from "lucide-react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { MonacoEditor } from "@components/monaco";
|
||||
import { SWARM_ICONS, useSwarm } from "@components/resources/swarm";
|
||||
import { RemoveSwarmResource } from "./remove";
|
||||
import { Section } from "@components/layouts";
|
||||
|
||||
export default function SwarmSecretPage() {
|
||||
const { id, secret: __secret } = useParams() as {
|
||||
@@ -21,6 +23,10 @@ export default function SwarmSecretPage() {
|
||||
useSetTitle(
|
||||
`${swarm?.name} | Secret | ${secret?.Spec?.Name ?? secret?.ID ?? "Unknown"}`
|
||||
);
|
||||
const { canExecute } = usePermissions({
|
||||
type: "Swarm",
|
||||
id,
|
||||
});
|
||||
const nav = useNavigate();
|
||||
|
||||
if (isPending) {
|
||||
@@ -67,6 +73,19 @@ export default function SwarmSecretPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{canExecute && secret.ID && (
|
||||
<Section title="Execute" icon={<Zap className="w-4 h-4" />}>
|
||||
<div className="flex gap-4 items-center flex-wrap">
|
||||
<RemoveSwarmResource
|
||||
id={id}
|
||||
type="Secret"
|
||||
resource_id={secret.ID}
|
||||
resource_name={secret.Spec?.Name}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
<MonacoEditor
|
||||
value={JSON.stringify(secret, null, 2)}
|
||||
language="json"
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
useSetTitle,
|
||||
} from "@lib/hooks";
|
||||
import { Button } from "@ui/button";
|
||||
import { ChevronLeft, Loader2 } from "lucide-react";
|
||||
import { ChevronLeft, Loader2, Zap } from "lucide-react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { MonacoEditor } from "@components/monaco";
|
||||
import { SWARM_ICONS, useSwarm } from "@components/resources/swarm";
|
||||
@@ -25,6 +25,7 @@ import { ReactNode, useMemo } from "react";
|
||||
import { MobileFriendlyTabsSelector } from "@ui/mobile-friendly-tabs";
|
||||
import { SwarmServiceLogs } from "./log";
|
||||
import { Section } from "@components/layouts";
|
||||
import { RemoveSwarmResource } from "./remove";
|
||||
|
||||
export default function SwarmServicePage() {
|
||||
const { id, service: __service } = useParams() as {
|
||||
@@ -36,13 +37,19 @@ export default function SwarmServicePage() {
|
||||
const { data: services, isPending } = useRead("ListSwarmServices", {
|
||||
swarm: id,
|
||||
});
|
||||
const service = services?.find((service) => service.ID === _service);
|
||||
const service = services?.find(
|
||||
(service) =>
|
||||
_service &&
|
||||
// First match on name here.
|
||||
// Then better to match on ID start to accept short ids too.
|
||||
(service.Name === _service || service.ID?.startsWith(_service))
|
||||
);
|
||||
const tasks =
|
||||
useRead("ListSwarmTasks", {
|
||||
swarm: id,
|
||||
}).data?.filter((task) => service?.ID && task.ServiceID === service.ID) ??
|
||||
[];
|
||||
const { canWrite } = usePermissions({
|
||||
const { canWrite, canExecute } = usePermissions({
|
||||
type: "Swarm",
|
||||
id,
|
||||
});
|
||||
@@ -110,6 +117,17 @@ export default function SwarmServicePage() {
|
||||
|
||||
<div className="mt-8 flex flex-col gap-12">
|
||||
{/* Actions */}
|
||||
{canExecute && service.Name && (
|
||||
<Section title="Execute" icon={<Zap className="w-4 h-4" />}>
|
||||
<div className="flex gap-4 items-center flex-wrap">
|
||||
<RemoveSwarmResource
|
||||
id={id}
|
||||
type="Service"
|
||||
resource_id={service.Name}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="pt-4">
|
||||
|
||||
@@ -1,11 +1,42 @@
|
||||
import { ResourceLink } from "@components/resources/common";
|
||||
import { PageHeaderName } from "@components/util";
|
||||
import { useRead, useSetTitle } from "@lib/hooks";
|
||||
import {
|
||||
ResourceDescription,
|
||||
ResourceLink,
|
||||
ResourcePageHeader,
|
||||
} from "@components/resources/common";
|
||||
import {
|
||||
useLocalStorage,
|
||||
usePermissions,
|
||||
useRead,
|
||||
useSetTitle,
|
||||
} from "@lib/hooks";
|
||||
import { Button } from "@ui/button";
|
||||
import { ChevronLeft, Loader2 } from "lucide-react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { MonacoEditor } from "@components/monaco";
|
||||
import { ChevronLeft, Loader2, Zap } from "lucide-react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { SWARM_ICONS, useSwarm } from "@components/resources/swarm";
|
||||
import {
|
||||
stroke_color_class_by_intention,
|
||||
swarm_state_intention,
|
||||
} from "@lib/color";
|
||||
import { ExportButton } from "@components/export";
|
||||
import { ResourceNotifications } from "@pages/resource-notifications";
|
||||
import { Types } from "komodo_client";
|
||||
import { ReactNode, useMemo, useState } from "react";
|
||||
import { MobileFriendlyTabsSelector } from "@ui/mobile-friendly-tabs";
|
||||
import { Section } from "@components/layouts";
|
||||
import { MonacoEditor } from "@components/monaco";
|
||||
import {
|
||||
SwarmStackServicesTable,
|
||||
SwarmStackTasksTable,
|
||||
} from "@components/resources/swarm/table";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@ui/select";
|
||||
import { SwarmServiceLogs } from "./log";
|
||||
import { RemoveSwarmResource } from "./remove";
|
||||
|
||||
export default function SwarmStackPage() {
|
||||
const { id, stack: __stack } = useParams() as {
|
||||
@@ -13,14 +44,16 @@ export default function SwarmStackPage() {
|
||||
stack: string;
|
||||
};
|
||||
const _stack = decodeURIComponent(__stack);
|
||||
console.log(_stack);
|
||||
const swarm = useSwarm(id);
|
||||
const { data: stack, isPending } = useRead("InspectSwarmStack", {
|
||||
swarm: id,
|
||||
stack: _stack,
|
||||
});
|
||||
const { canWrite, canExecute } = usePermissions({
|
||||
type: "Swarm",
|
||||
id,
|
||||
});
|
||||
useSetTitle(`${swarm?.name} | Stack | ${stack?.Name ?? "Unknown"}`);
|
||||
const nav = useNavigate();
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
@@ -35,42 +68,227 @@ export default function SwarmStackPage() {
|
||||
}
|
||||
|
||||
const Icon = SWARM_ICONS.Stack;
|
||||
const state = stack.State;
|
||||
const intention = swarm_state_intention(state);
|
||||
const strokeColor = stroke_color_class_by_intention(intention);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-16 mb-24">
|
||||
{/* HEADER */}
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* BACK */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Button
|
||||
className="gap-2"
|
||||
variant="secondary"
|
||||
onClick={() => nav("/swarms/" + id)}
|
||||
>
|
||||
<ChevronLeft className="w-4" /> Back
|
||||
<div>
|
||||
<div className="w-full flex items-center justify-between mb-12">
|
||||
<Link to={"/swarms/" + id}>
|
||||
<Button className="gap-2" variant="secondary">
|
||||
<ChevronLeft className="w-4" />
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* TITLE */}
|
||||
</Link>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="mt-1">
|
||||
<Icon size={8} />
|
||||
</div>
|
||||
<PageHeaderName name={stack?.Name} />
|
||||
</div>
|
||||
|
||||
{/* INFO */}
|
||||
<div className="flex flex-wrap gap-4 items-center text-muted-foreground">
|
||||
Swarm Stack
|
||||
<ResourceLink type="Swarm" id={id} />
|
||||
<ExportButton targets={[{ type: "Swarm", id }]} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col xl:flex-row gap-4">
|
||||
{/* HEADER */}
|
||||
<div className="w-full flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2 border rounded-md">
|
||||
<ResourcePageHeader
|
||||
type={undefined}
|
||||
id={undefined}
|
||||
intent={intention}
|
||||
icon={<Icon size={8} className={strokeColor} />}
|
||||
resource={undefined}
|
||||
name={stack.Name}
|
||||
state={state}
|
||||
status={`${stack.Services.length} Services`}
|
||||
/>
|
||||
<div className="flex flex-col pb-2 px-4">
|
||||
<div className="flex items-center gap-x-4 gap-y-0 flex-wrap text-muted-foreground">
|
||||
<ResourceLink type="Swarm" id={id} />
|
||||
<div>|</div>
|
||||
<div>Swarm Stack</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ResourceDescription type="Swarm" id={id} disabled={!canWrite} />
|
||||
</div>
|
||||
{/** NOTIFICATIONS */}
|
||||
<ResourceNotifications type="Swarm" id={id} />
|
||||
</div>
|
||||
|
||||
<MonacoEditor
|
||||
value={JSON.stringify(stack, null, 2)}
|
||||
language="json"
|
||||
readOnly
|
||||
/>
|
||||
<div className="mt-8 flex flex-col gap-12">
|
||||
{/* Actions */}
|
||||
{canExecute && (
|
||||
<Section title="Execute" icon={<Zap className="w-4 h-4" />}>
|
||||
<div className="flex gap-4 items-center flex-wrap">
|
||||
<RemoveSwarmResource
|
||||
id={id}
|
||||
type="Stack"
|
||||
resource_id={stack.Name}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="pt-4">
|
||||
{swarm && <SwarmStackTabs swarm={swarm} stack={stack} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* TABS */
|
||||
type SwarmStackTabsView = "Services" | "Tasks" | "Log" | "Inspect";
|
||||
|
||||
const SwarmStackTabs = ({
|
||||
swarm,
|
||||
stack,
|
||||
}: {
|
||||
swarm: Types.SwarmListItem;
|
||||
stack: Types.SwarmStack;
|
||||
}) => {
|
||||
const [_view, setView] = useLocalStorage<SwarmStackTabsView>(
|
||||
`swarm-${swarm.id}-stack-${stack}-tabs-v2`,
|
||||
"Services"
|
||||
);
|
||||
const _search = useState("");
|
||||
const { specificInspect, specificLogs } = usePermissions({
|
||||
type: "Swarm",
|
||||
id: swarm.id,
|
||||
});
|
||||
|
||||
const view =
|
||||
(!specificLogs && _view === "Log") ||
|
||||
(!specificInspect && _view === "Inspect")
|
||||
? "Services"
|
||||
: _view;
|
||||
|
||||
const tabs = useMemo(
|
||||
() => [
|
||||
{
|
||||
value: "Services",
|
||||
},
|
||||
{
|
||||
value: "Tasks",
|
||||
},
|
||||
{
|
||||
value: "Log",
|
||||
disabled: !specificLogs,
|
||||
},
|
||||
{
|
||||
value: "Inspect",
|
||||
disabled: !specificInspect,
|
||||
},
|
||||
],
|
||||
[specificLogs, specificInspect]
|
||||
);
|
||||
|
||||
const Selector = (
|
||||
<MobileFriendlyTabsSelector
|
||||
tabs={tabs}
|
||||
value={view}
|
||||
onValueChange={setView as any}
|
||||
tabsTriggerClassname="w-[110px]"
|
||||
/>
|
||||
);
|
||||
|
||||
switch (view) {
|
||||
case "Services":
|
||||
return (
|
||||
<SwarmStackServicesTable
|
||||
id={swarm.id}
|
||||
services={stack.Services}
|
||||
titleOther={Selector}
|
||||
_search={_search}
|
||||
/>
|
||||
);
|
||||
case "Tasks":
|
||||
return (
|
||||
<SwarmStackTasksTable
|
||||
id={swarm.id}
|
||||
tasks={stack.Tasks}
|
||||
titleOther={Selector}
|
||||
_search={_search}
|
||||
/>
|
||||
);
|
||||
case "Log":
|
||||
return (
|
||||
<SwarmStackLogs
|
||||
id={swarm.id}
|
||||
stack={stack}
|
||||
disabled={!specificLogs}
|
||||
titleOther={Selector}
|
||||
/>
|
||||
);
|
||||
case "Inspect":
|
||||
return (
|
||||
<SwarmStackInspect
|
||||
stack={stack}
|
||||
titleOther={Selector}
|
||||
disabled={!specificInspect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const SwarmStackLogs = ({
|
||||
id,
|
||||
stack,
|
||||
disabled,
|
||||
titleOther,
|
||||
}: {
|
||||
id: string;
|
||||
stack: Types.SwarmStack;
|
||||
disabled: boolean;
|
||||
titleOther: ReactNode;
|
||||
}) => {
|
||||
const [service, setService] = useState(stack.Services[0].Name ?? "");
|
||||
return (
|
||||
<SwarmServiceLogs
|
||||
id={id}
|
||||
service={service}
|
||||
titleOther={titleOther}
|
||||
disabled={disabled}
|
||||
extraParams={
|
||||
<Select value={service} onValueChange={setService}>
|
||||
<SelectTrigger className="w-fit">
|
||||
<div className="flex items-center gap-2 pr-2">
|
||||
<div className="text-xs text-muted-foreground">Service:</div>
|
||||
<SelectValue placeholder="Select Service" />
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{stack.Services.filter((service) => service.Name).map((service) => (
|
||||
<SelectItem key={service.Name} value={service.Name!}>
|
||||
{service.Name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const SwarmStackInspect = ({
|
||||
stack,
|
||||
titleOther,
|
||||
disabled,
|
||||
}: {
|
||||
stack: Types.SwarmStack;
|
||||
titleOther: ReactNode;
|
||||
disabled: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<Section titleOther={titleOther}>
|
||||
{disabled ? (
|
||||
<div>User does not have Inspect permission on Swarm.</div>
|
||||
) : (
|
||||
<MonacoEditor
|
||||
value={JSON.stringify(stack, null, 2)}
|
||||
language="json"
|
||||
readOnly
|
||||
/>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -36,7 +36,12 @@ export default function SwarmTaskPage() {
|
||||
const { data: tasks, isPending } = useRead("ListSwarmTasks", {
|
||||
swarm: id,
|
||||
});
|
||||
const task = tasks?.find((task) => task.ID === _task);
|
||||
const task = tasks?.find(
|
||||
(task) =>
|
||||
_task &&
|
||||
// Better to match on start to accept short ids too
|
||||
task.ID?.startsWith(_task)
|
||||
);
|
||||
const node = useRead("ListSwarmNodes", { swarm: id }).data?.find(
|
||||
(node) => node.ID === task?.NodeID
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user