Compare commits

..

2 Commits

Author SHA1 Message Date
Maxwell Becker
41d1ff9760 1.15.9 (#127)
* add close alert threshold to prevent Ok - Warning back and forth

* remove part about repo being deleted, no longer behavior

* resource sync share general common

* remove this changelog. use releases

* remove changelog from readme

* write commit file clean up path

* docs: supports any git provider repo

* fix docs: authorization

* multiline command supports escaped newlines

* move webhook to build config advanced

* parser comments with escaped newline

* improve parser

* save use Enter. escape monaco using escape

* improve logic when deployment / stack action buttons shown

* used_mem = total - available

* Fix unrecognized path have 404

* webhooks will 404 if misconfigured

* move update logger / alerter

* delete migrator

* update examples

* publish typescript client komodo_client
2024-10-14 23:04:49 -07:00
mbecker20
dfafadf57b demo / build username pw 2024-10-14 11:49:44 -04:00
151 changed files with 803 additions and 1396 deletions

37
Cargo.lock generated
View File

@@ -41,7 +41,7 @@ dependencies = [
[[package]]
name = "alerter"
version = "1.15.8"
version = "1.15.9"
dependencies = [
"anyhow",
"axum",
@@ -943,7 +943,7 @@ dependencies = [
[[package]]
name = "command"
version = "1.15.8"
version = "1.15.9"
dependencies = [
"komodo_client",
"run_command",
@@ -1355,7 +1355,7 @@ dependencies = [
[[package]]
name = "environment_file"
version = "1.15.8"
version = "1.15.9"
dependencies = [
"thiserror",
]
@@ -1439,7 +1439,7 @@ dependencies = [
[[package]]
name = "formatting"
version = "1.15.8"
version = "1.15.9"
dependencies = [
"serror",
]
@@ -1571,7 +1571,7 @@ checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
[[package]]
name = "git"
version = "1.15.8"
version = "1.15.9"
dependencies = [
"anyhow",
"command",
@@ -2192,7 +2192,7 @@ dependencies = [
[[package]]
name = "komodo_cli"
version = "1.15.8"
version = "1.15.9"
dependencies = [
"anyhow",
"clap",
@@ -2208,7 +2208,7 @@ dependencies = [
[[package]]
name = "komodo_client"
version = "1.15.8"
version = "1.15.9"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2239,7 +2239,7 @@ dependencies = [
[[package]]
name = "komodo_core"
version = "1.15.8"
version = "1.15.9"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2296,7 +2296,7 @@ dependencies = [
[[package]]
name = "komodo_periphery"
version = "1.15.8"
version = "1.15.9"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2383,7 +2383,7 @@ dependencies = [
[[package]]
name = "logger"
version = "1.15.8"
version = "1.15.9"
dependencies = [
"anyhow",
"komodo_client",
@@ -2445,19 +2445,6 @@ dependencies = [
"toml",
]
[[package]]
name = "migrator"
version = "1.15.8"
dependencies = [
"anyhow",
"dotenvy",
"envy",
"logger",
"serde",
"tokio",
"tracing",
]
[[package]]
name = "mime"
version = "0.3.17"
@@ -3102,7 +3089,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "periphery_client"
version = "1.15.8"
version = "1.15.9"
dependencies = [
"anyhow",
"komodo_client",
@@ -4880,7 +4867,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "update_logger"
version = "1.15.8"
version = "1.15.9"
dependencies = [
"anyhow",
"komodo_client",

View File

@@ -1,9 +1,15 @@
[workspace]
resolver = "2"
members = ["bin/*", "lib/*", "client/core/rs", "client/periphery/rs"]
members = [
"bin/*",
"lib/*",
"example/*",
"client/core/rs",
"client/periphery/rs",
]
[workspace.package]
version = "1.15.8"
version = "1.15.9"
edition = "2021"
authors = ["mbecker20 <becker.maxh@gmail.com>"]
license = "GPL-3.0-or-later"
@@ -108,4 +114,4 @@ octorust = "0.7.0"
dashmap = "6.1.0"
colored = "2.1.0"
regex = "1.11.0"
bson = "2.13.0"
bson = "2.13.0"

View File

@@ -16,7 +16,7 @@ WORKDIR /builder
COPY ./frontend ./frontend
COPY ./client/core/ts ./client
RUN cd client && yarn && yarn build && yarn link
RUN cd frontend && yarn link @komodo/client && yarn && yarn build
RUN cd frontend && yarn link komodo_client && yarn && yarn build
# Final Image
FROM alpine:3.20

View File

@@ -10,7 +10,7 @@ WORKDIR /builder
COPY ./frontend ./frontend
COPY ./client/core/ts ./client
RUN cd client && yarn && yarn build && yarn link
RUN cd frontend && yarn link @komodo/client && yarn && yarn build
RUN cd frontend && yarn link komodo_client && yarn && yarn build
# Final Image
FROM debian:bullseye-slim

View File

@@ -6,7 +6,9 @@ use komodo_client::{
api::execute::RunBuild,
entities::{build::Build, user::git_webhook_user},
};
use reqwest::StatusCode;
use resolver_api::Resolve;
use serror::AddStatusCode;
use crate::{
api::execute::ExecuteRequest,
@@ -20,22 +22,30 @@ fn build_locks() -> &'static ListenerLockCache {
BUILD_LOCKS.get_or_init(Default::default)
}
pub async fn handle_build_webhook(
build_id: String,
pub async fn auth_build_webhook(
build_id: &str,
headers: HeaderMap,
body: &str,
) -> serror::Result<Build> {
let build = resource::get::<Build>(build_id)
.await
.status_code(StatusCode::NOT_FOUND)?;
verify_gh_signature(headers, body, &build.config.webhook_secret)
.await
.status_code(StatusCode::UNAUTHORIZED)?;
Ok(build)
}
pub async fn handle_build_webhook(
build: Build,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through from action state busy.
let lock = build_locks().get_or_insert_default(&build_id).await;
let lock = build_locks().get_or_insert_default(&build.id).await;
let _lock = lock.lock().await;
let build = resource::get::<Build>(&build_id).await?;
verify_gh_signature(headers, &body, &build.config.webhook_secret)
.await?;
if !build.config.webhook_enabled {
return Err(anyhow!("build does not have webhook enabled"));
}
@@ -46,7 +56,7 @@ pub async fn handle_build_webhook(
}
let user = git_webhook_user().to_owned();
let req = ExecuteRequest::RunBuild(RunBuild { build: build_id });
let req = ExecuteRequest::RunBuild(RunBuild { build: build.id });
let update = init_execution_update(&req, &user).await?;
let ExecuteRequest::RunBuild(req) = req else {
unreachable!()

View File

@@ -39,10 +39,11 @@ pub fn router() -> Router {
"/build/:id",
post(
|Path(Id { id }), headers: HeaderMap, body: String| async move {
let build = build::auth_build_webhook(&id, headers, &body).await?;
tokio::spawn(async move {
let span = info_span!("build_webhook", id);
async {
let res = build::handle_build_webhook(id.clone(), headers, body).await;
let res = build::handle_build_webhook(build, body).await;
if let Err(e) = res {
warn!("failed to run build webook for build {id} | {e:#}");
}
@@ -50,6 +51,7 @@ pub fn router() -> Router {
.instrument(span)
.await
});
serror::Result::Ok(())
},
),
)
@@ -57,10 +59,11 @@ pub fn router() -> Router {
"/repo/:id/clone",
post(
|Path(Id { id }), headers: HeaderMap, body: String| async move {
let repo = repo::auth_repo_webhook(&id, headers, &body).await?;
tokio::spawn(async move {
let span = info_span!("repo_clone_webhook", id);
async {
let res = repo::handle_repo_clone_webhook(id.clone(), headers, body).await;
let res = repo::handle_repo_clone_webhook(repo, body).await;
if let Err(e) = res {
warn!("failed to run repo clone webook for repo {id} | {e:#}");
}
@@ -68,6 +71,7 @@ pub fn router() -> Router {
.instrument(span)
.await
});
serror::Result::Ok(())
},
)
)
@@ -75,10 +79,11 @@ pub fn router() -> Router {
"/repo/:id/pull",
post(
|Path(Id { id }), headers: HeaderMap, body: String| async move {
let repo = repo::auth_repo_webhook(&id, headers, &body).await?;
tokio::spawn(async move {
let span = info_span!("repo_pull_webhook", id);
async {
let res = repo::handle_repo_pull_webhook(id.clone(), headers, body).await;
let res = repo::handle_repo_pull_webhook(repo, body).await;
if let Err(e) = res {
warn!("failed to run repo pull webook for repo {id} | {e:#}");
}
@@ -86,6 +91,7 @@ pub fn router() -> Router {
.instrument(span)
.await
});
serror::Result::Ok(())
},
)
)
@@ -93,10 +99,11 @@ pub fn router() -> Router {
"/repo/:id/build",
post(
|Path(Id { id }), headers: HeaderMap, body: String| async move {
let repo = repo::auth_repo_webhook(&id, headers, &body).await?;
tokio::spawn(async move {
let span = info_span!("repo_build_webhook", id);
async {
let res = repo::handle_repo_build_webhook(id.clone(), headers, body).await;
let res = repo::handle_repo_build_webhook(repo, body).await;
if let Err(e) = res {
warn!("failed to run repo build webook for repo {id} | {e:#}");
}
@@ -104,6 +111,7 @@ pub fn router() -> Router {
.instrument(span)
.await
});
serror::Result::Ok(())
},
)
)
@@ -111,10 +119,11 @@ pub fn router() -> Router {
"/stack/:id/refresh",
post(
|Path(Id { id }), headers: HeaderMap, body: String| async move {
let stack = stack::auth_stack_webhook(&id, headers, &body).await?;
tokio::spawn(async move {
let span = info_span!("stack_clone_webhook", id);
async {
let res = stack::handle_stack_refresh_webhook(id.clone(), headers, body).await;
let res = stack::handle_stack_refresh_webhook(stack, body).await;
if let Err(e) = res {
warn!("failed to run stack clone webook for stack {id} | {e:#}");
}
@@ -122,6 +131,7 @@ pub fn router() -> Router {
.instrument(span)
.await
});
serror::Result::Ok(())
},
)
)
@@ -129,10 +139,11 @@ pub fn router() -> Router {
"/stack/:id/deploy",
post(
|Path(Id { id }), headers: HeaderMap, body: String| async move {
let stack = stack::auth_stack_webhook(&id, headers, &body).await?;
tokio::spawn(async move {
let span = info_span!("stack_pull_webhook", id);
async {
let res = stack::handle_stack_deploy_webhook(id.clone(), headers, body).await;
let res = stack::handle_stack_deploy_webhook(stack, body).await;
if let Err(e) = res {
warn!("failed to run stack pull webook for stack {id} | {e:#}");
}
@@ -140,6 +151,7 @@ pub fn router() -> Router {
.instrument(span)
.await
});
serror::Result::Ok(())
},
)
)
@@ -147,13 +159,13 @@ pub fn router() -> Router {
"/procedure/:id/:branch",
post(
|Path(IdBranch { id, branch }), headers: HeaderMap, body: String| async move {
let procedure = procedure::auth_procedure_webhook(&id, headers, &body).await?;
tokio::spawn(async move {
let span = info_span!("procedure_webhook", id, branch);
async {
let res = procedure::handle_procedure_webhook(
id.clone(),
procedure,
branch.unwrap_or_else(|| String::from("main")),
headers,
body
).await;
if let Err(e) = res {
@@ -163,6 +175,7 @@ pub fn router() -> Router {
.instrument(span)
.await
});
serror::Result::Ok(())
},
)
)
@@ -170,12 +183,12 @@ pub fn router() -> Router {
"/sync/:id/refresh",
post(
|Path(Id { id }), headers: HeaderMap, body: String| async move {
let sync = sync::auth_sync_webhook(&id, headers, &body).await?;
tokio::spawn(async move {
let span = info_span!("sync_refresh_webhook", id);
async {
let res = sync::handle_sync_refresh_webhook(
id.clone(),
headers,
sync,
body
).await;
if let Err(e) = res {
@@ -185,6 +198,7 @@ pub fn router() -> Router {
.instrument(span)
.await
});
serror::Result::Ok(())
},
)
)
@@ -192,12 +206,12 @@ pub fn router() -> Router {
"/sync/:id/sync",
post(
|Path(Id { id }), headers: HeaderMap, body: String| async move {
let sync = sync::auth_sync_webhook(&id, headers, &body).await?;
tokio::spawn(async move {
let span = info_span!("sync_execute_webhook", id);
async {
let res = sync::handle_sync_execute_webhook(
id.clone(),
headers,
sync,
body
).await;
if let Err(e) = res {
@@ -207,6 +221,7 @@ pub fn router() -> Router {
.instrument(span)
.await
});
serror::Result::Ok(())
},
)
)

View File

@@ -6,7 +6,9 @@ use komodo_client::{
api::execute::RunProcedure,
entities::{procedure::Procedure, user::git_webhook_user},
};
use reqwest::StatusCode;
use resolver_api::Resolve;
use serror::AddStatusCode;
use crate::{
api::execute::ExecuteRequest,
@@ -20,28 +22,36 @@ fn procedure_locks() -> &'static ListenerLockCache {
BUILD_LOCKS.get_or_init(Default::default)
}
pub async fn handle_procedure_webhook(
procedure_id: String,
target_branch: String,
pub async fn auth_procedure_webhook(
procedure_id: &str,
headers: HeaderMap,
body: &str,
) -> serror::Result<Procedure> {
let procedure = resource::get::<Procedure>(procedure_id)
.await
.status_code(StatusCode::NOT_FOUND)?;
verify_gh_signature(
headers,
body,
&procedure.config.webhook_secret,
)
.await
.status_code(StatusCode::UNAUTHORIZED)?;
Ok(procedure)
}
pub async fn handle_procedure_webhook(
procedure: Procedure,
target_branch: String,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through from action state busy.
let lock =
procedure_locks().get_or_insert_default(&procedure_id).await;
procedure_locks().get_or_insert_default(&procedure.id).await;
let _lock = lock.lock().await;
let procedure = resource::get::<Procedure>(&procedure_id).await?;
verify_gh_signature(
headers,
&body,
&procedure.config.webhook_secret,
)
.await?;
if !procedure.config.webhook_enabled {
return Err(anyhow!("procedure does not have webhook enabled"));
}
@@ -53,7 +63,7 @@ pub async fn handle_procedure_webhook(
let user = git_webhook_user().to_owned();
let req = ExecuteRequest::RunProcedure(RunProcedure {
procedure: procedure_id,
procedure: procedure.id,
});
let update = init_execution_update(&req, &user).await?;
let ExecuteRequest::RunProcedure(req) = req else {

View File

@@ -6,7 +6,9 @@ use komodo_client::{
api::execute::{BuildRepo, CloneRepo, PullRepo},
entities::{repo::Repo, user::git_webhook_user},
};
use reqwest::StatusCode;
use resolver_api::Resolve;
use serror::AddStatusCode;
use crate::{
helpers::update::init_execution_update, resource, state::State,
@@ -19,22 +21,30 @@ fn repo_locks() -> &'static ListenerLockCache {
REPO_LOCKS.get_or_init(Default::default)
}
pub async fn handle_repo_clone_webhook(
repo_id: String,
pub async fn auth_repo_webhook(
repo_id: &str,
headers: HeaderMap,
body: &str,
) -> serror::Result<Repo> {
let repo = resource::get::<Repo>(repo_id)
.await
.status_code(StatusCode::NOT_FOUND)?;
verify_gh_signature(headers, body, &repo.config.webhook_secret)
.await
.status_code(StatusCode::UNAUTHORIZED)?;
Ok(repo)
}
pub async fn handle_repo_clone_webhook(
repo: Repo,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through from action state busy.
let lock = repo_locks().get_or_insert_default(&repo_id).await;
let lock = repo_locks().get_or_insert_default(&repo.id).await;
let _lock = lock.lock().await;
let repo = resource::get::<Repo>(&repo_id).await?;
verify_gh_signature(headers, &body, &repo.config.webhook_secret)
.await?;
if !repo.config.webhook_enabled {
return Err(anyhow!("repo does not have webhook enabled"));
}
@@ -47,7 +57,7 @@ pub async fn handle_repo_clone_webhook(
let user = git_webhook_user().to_owned();
let req =
crate::api::execute::ExecuteRequest::CloneRepo(CloneRepo {
repo: repo_id,
repo: repo.id,
});
let update = init_execution_update(&req, &user).await?;
let crate::api::execute::ExecuteRequest::CloneRepo(req) = req
@@ -59,21 +69,15 @@ pub async fn handle_repo_clone_webhook(
}
pub async fn handle_repo_pull_webhook(
repo_id: String,
headers: HeaderMap,
repo: Repo,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through from action state busy.
let lock = repo_locks().get_or_insert_default(&repo_id).await;
let lock = repo_locks().get_or_insert_default(&repo.id).await;
let _lock = lock.lock().await;
let repo = resource::get::<Repo>(&repo_id).await?;
verify_gh_signature(headers, &body, &repo.config.webhook_secret)
.await?;
if !repo.config.webhook_enabled {
return Err(anyhow!("repo does not have webhook enabled"));
}
@@ -85,7 +89,7 @@ pub async fn handle_repo_pull_webhook(
let user = git_webhook_user().to_owned();
let req = crate::api::execute::ExecuteRequest::PullRepo(PullRepo {
repo: repo_id,
repo: repo.id,
});
let update = init_execution_update(&req, &user).await?;
let crate::api::execute::ExecuteRequest::PullRepo(req) = req else {
@@ -96,21 +100,15 @@ pub async fn handle_repo_pull_webhook(
}
pub async fn handle_repo_build_webhook(
repo_id: String,
headers: HeaderMap,
repo: Repo,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through from action state busy.
let lock = repo_locks().get_or_insert_default(&repo_id).await;
let lock = repo_locks().get_or_insert_default(&repo.id).await;
let _lock = lock.lock().await;
let repo = resource::get::<Repo>(&repo_id).await?;
verify_gh_signature(headers, &body, &repo.config.webhook_secret)
.await?;
if !repo.config.webhook_enabled {
return Err(anyhow!("repo does not have webhook enabled"));
}
@@ -123,7 +121,7 @@ pub async fn handle_repo_build_webhook(
let user = git_webhook_user().to_owned();
let req =
crate::api::execute::ExecuteRequest::BuildRepo(BuildRepo {
repo: repo_id,
repo: repo.id,
});
let update = init_execution_update(&req, &user).await?;
let crate::api::execute::ExecuteRequest::BuildRepo(req) = req

View File

@@ -9,7 +9,9 @@ use komodo_client::{
},
entities::{stack::Stack, user::git_webhook_user},
};
use reqwest::StatusCode;
use resolver_api::Resolve;
use serror::AddStatusCode;
use crate::{
api::execute::ExecuteRequest,
@@ -23,22 +25,30 @@ fn stack_locks() -> &'static ListenerLockCache {
STACK_LOCKS.get_or_init(Default::default)
}
pub async fn handle_stack_refresh_webhook(
stack_id: String,
pub async fn auth_stack_webhook(
stack_id: &str,
headers: HeaderMap,
body: &str,
) -> serror::Result<Stack> {
let stack = resource::get::<Stack>(stack_id)
.await
.status_code(StatusCode::NOT_FOUND)?;
verify_gh_signature(headers, body, &stack.config.webhook_secret)
.await
.status_code(StatusCode::UNAUTHORIZED)?;
Ok(stack)
}
pub async fn handle_stack_refresh_webhook(
stack: Stack,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through, from "action state busy".
let lock = stack_locks().get_or_insert_default(&stack_id).await;
let lock = stack_locks().get_or_insert_default(&stack.id).await;
let _lock = lock.lock().await;
let stack = resource::get::<Stack>(&stack_id).await?;
verify_gh_signature(headers, &body, &stack.config.webhook_secret)
.await?;
if !stack.config.webhook_enabled {
return Err(anyhow!("stack does not have webhook enabled"));
}
@@ -56,21 +66,15 @@ pub async fn handle_stack_refresh_webhook(
}
pub async fn handle_stack_deploy_webhook(
stack_id: String,
headers: HeaderMap,
stack: Stack,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through from action state busy.
let lock = stack_locks().get_or_insert_default(&stack_id).await;
let lock = stack_locks().get_or_insert_default(&stack.id).await;
let _lock = lock.lock().await;
let stack = resource::get::<Stack>(&stack_id).await?;
verify_gh_signature(headers, &body, &stack.config.webhook_secret)
.await?;
if !stack.config.webhook_enabled {
return Err(anyhow!("stack does not have webhook enabled"));
}
@@ -83,7 +87,7 @@ pub async fn handle_stack_deploy_webhook(
let user = git_webhook_user().to_owned();
if stack.config.webhook_force_deploy {
let req = ExecuteRequest::DeployStack(DeployStack {
stack: stack_id,
stack: stack.id,
stop_time: None,
});
let update = init_execution_update(&req, &user).await?;
@@ -94,7 +98,7 @@ pub async fn handle_stack_deploy_webhook(
} else {
let req =
ExecuteRequest::DeployStackIfChanged(DeployStackIfChanged {
stack: stack_id,
stack: stack.id,
stop_time: None,
});
let update = init_execution_update(&req, &user).await?;

View File

@@ -6,7 +6,9 @@ use komodo_client::{
api::{execute::RunSync, write::RefreshResourceSyncPending},
entities::{sync::ResourceSync, user::git_webhook_user},
};
use reqwest::StatusCode;
use resolver_api::Resolve;
use serror::AddStatusCode;
use crate::{
api::execute::ExecuteRequest,
@@ -20,22 +22,30 @@ fn sync_locks() -> &'static ListenerLockCache {
SYNC_LOCKS.get_or_init(Default::default)
}
pub async fn handle_sync_refresh_webhook(
sync_id: String,
pub async fn auth_sync_webhook(
sync_id: &str,
headers: HeaderMap,
body: &str,
) -> serror::Result<ResourceSync> {
let sync = resource::get::<ResourceSync>(sync_id)
.await
.status_code(StatusCode::NOT_FOUND)?;
verify_gh_signature(headers, body, &sync.config.webhook_secret)
.await
.status_code(StatusCode::UNAUTHORIZED)?;
Ok(sync)
}
pub async fn handle_sync_refresh_webhook(
sync: ResourceSync,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through from action state busy.
let lock = sync_locks().get_or_insert_default(&sync_id).await;
let lock = sync_locks().get_or_insert_default(&sync.id).await;
let _lock = lock.lock().await;
let sync = resource::get::<ResourceSync>(&sync_id).await?;
verify_gh_signature(headers, &body, &sync.config.webhook_secret)
.await?;
if !sync.config.webhook_enabled {
return Err(anyhow!("sync does not have webhook enabled"));
}
@@ -47,27 +57,21 @@ pub async fn handle_sync_refresh_webhook(
let user = git_webhook_user().to_owned();
State
.resolve(RefreshResourceSyncPending { sync: sync_id }, user)
.resolve(RefreshResourceSyncPending { sync: sync.id }, user)
.await?;
Ok(())
}
pub async fn handle_sync_execute_webhook(
sync_id: String,
headers: HeaderMap,
sync: ResourceSync,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through from action state busy.
let lock = sync_locks().get_or_insert_default(&sync_id).await;
let lock = sync_locks().get_or_insert_default(&sync.id).await;
let _lock = lock.lock().await;
let sync = resource::get::<ResourceSync>(&sync_id).await?;
verify_gh_signature(headers, &body, &sync.config.webhook_secret)
.await?;
if !sync.config.webhook_enabled {
return Err(anyhow!("sync does not have webhook enabled"));
}
@@ -79,7 +83,7 @@ pub async fn handle_sync_execute_webhook(
let user = git_webhook_user().to_owned();
let req = ExecuteRequest::RunSync(RunSync {
sync: sync_id,
sync: sync.id,
resource_type: None,
resources: None,
});

View File

@@ -2,9 +2,7 @@ use std::collections::HashMap;
use anyhow::Context;
use komodo_client::entities::{
resource::ResourceQuery,
server::{Server, ServerListItem},
user::User,
resource::ResourceQuery, server::Server, user::User,
};
use crate::resource;
@@ -32,11 +30,10 @@ pub async fn check_alerts(ts: i64) {
}
#[instrument(level = "debug")]
async fn get_all_servers_map() -> anyhow::Result<(
HashMap<String, ServerListItem>,
HashMap<String, String>,
)> {
let servers = resource::list_for_user::<Server>(
async fn get_all_servers_map(
) -> anyhow::Result<(HashMap<String, Server>, HashMap<String, String>)>
{
let servers = resource::list_full_for_user::<Server>(
ResourceQuery::default(),
&User {
admin: true,

View File

@@ -5,7 +5,7 @@ use derive_variants::ExtractVariant;
use komodo_client::entities::{
alert::{Alert, AlertData, AlertDataVariant, SeverityLevel},
komodo_timestamp, optional_string,
server::{ServerListItem, ServerState},
server::{Server, ServerState},
ResourceTarget,
};
use mongo_indexed::Indexed;
@@ -28,7 +28,7 @@ type OpenDiskAlertMap = OpenAlertMap<PathBuf>;
#[instrument(level = "debug")]
pub async fn alert_servers(
ts: i64,
mut servers: HashMap<String, ServerListItem>,
mut servers: HashMap<String, Server>,
) {
let server_statuses = server_status_cache().get_list().await;
@@ -70,12 +70,12 @@ pub async fn alert_servers(
data: AlertData::ServerUnreachable {
id: server_status.id.clone(),
name: server.name.clone(),
region: optional_string(&server.info.region),
region: optional_string(&server.config.region),
err: server_status.err.clone(),
},
};
alerts_to_open
.push((alert, server.info.send_unreachable_alerts))
.push((alert, server.config.send_unreachable_alerts))
}
(ServerState::NotOk, Some(alert)) => {
// update alert err
@@ -102,8 +102,10 @@ pub async fn alert_servers(
// Close an open alert
(ServerState::Ok | ServerState::Disabled, Some(alert)) => {
alert_ids_to_close
.push((alert.clone(), server.info.send_unreachable_alerts));
alert_ids_to_close.push((
alert.clone(),
server.config.send_unreachable_alerts,
));
}
_ => {}
}
@@ -119,20 +121,21 @@ pub async fn alert_servers(
.as_ref()
.and_then(|alerts| alerts.get(&AlertDataVariant::ServerCpu))
.cloned();
match (health.cpu, cpu_alert) {
(SeverityLevel::Warning | SeverityLevel::Critical, None) => {
match (health.cpu.level, cpu_alert, health.cpu.should_close_alert)
{
(SeverityLevel::Warning | SeverityLevel::Critical, None, _) => {
// open alert
let alert = Alert {
id: Default::default(),
ts,
resolved: false,
resolved_ts: None,
level: health.cpu,
level: health.cpu.level,
target: ResourceTarget::Server(server_status.id.clone()),
data: AlertData::ServerCpu {
id: server_status.id.clone(),
name: server.name.clone(),
region: optional_string(&server.info.region),
region: optional_string(&server.config.region),
percentage: server_status
.stats
.as_ref()
@@ -140,41 +143,44 @@ pub async fn alert_servers(
.unwrap_or(0.0),
},
};
alerts_to_open.push((alert, server.info.send_cpu_alerts));
alerts_to_open.push((alert, server.config.send_cpu_alerts));
}
(
SeverityLevel::Warning | SeverityLevel::Critical,
Some(mut alert),
_,
) => {
// modify alert level only if it has increased
if alert.level < health.cpu {
alert.level = health.cpu;
if alert.level < health.cpu.level {
alert.level = health.cpu.level;
alert.data = AlertData::ServerCpu {
id: server_status.id.clone(),
name: server.name.clone(),
region: optional_string(&server.info.region),
region: optional_string(&server.config.region),
percentage: server_status
.stats
.as_ref()
.map(|s| s.cpu_perc as f64)
.unwrap_or(0.0),
};
alerts_to_update.push((alert, server.info.send_cpu_alerts));
alerts_to_update
.push((alert, server.config.send_cpu_alerts));
}
}
(SeverityLevel::Ok, Some(alert)) => {
(SeverityLevel::Ok, Some(alert), true) => {
let mut alert = alert.clone();
alert.data = AlertData::ServerCpu {
id: server_status.id.clone(),
name: server.name.clone(),
region: optional_string(&server.info.region),
region: optional_string(&server.config.region),
percentage: server_status
.stats
.as_ref()
.map(|s| s.cpu_perc as f64)
.unwrap_or(0.0),
};
alert_ids_to_close.push((alert, server.info.send_cpu_alerts))
alert_ids_to_close
.push((alert, server.config.send_cpu_alerts))
}
_ => {}
}
@@ -186,20 +192,21 @@ pub async fn alert_servers(
.as_ref()
.and_then(|alerts| alerts.get(&AlertDataVariant::ServerMem))
.cloned();
match (health.mem, mem_alert) {
(SeverityLevel::Warning | SeverityLevel::Critical, None) => {
match (health.mem.level, mem_alert, health.mem.should_close_alert)
{
(SeverityLevel::Warning | SeverityLevel::Critical, None, _) => {
// open alert
let alert = Alert {
id: Default::default(),
ts,
resolved: false,
resolved_ts: None,
level: health.mem,
level: health.mem.level,
target: ResourceTarget::Server(server_status.id.clone()),
data: AlertData::ServerMem {
id: server_status.id.clone(),
name: server.name.clone(),
region: optional_string(&server.info.region),
region: optional_string(&server.config.region),
total_gb: server_status
.stats
.as_ref()
@@ -212,19 +219,20 @@ pub async fn alert_servers(
.unwrap_or(0.0),
},
};
alerts_to_open.push((alert, server.info.send_mem_alerts));
alerts_to_open.push((alert, server.config.send_mem_alerts));
}
(
SeverityLevel::Warning | SeverityLevel::Critical,
Some(mut alert),
_,
) => {
// modify alert level only if it has increased
if alert.level < health.mem {
alert.level = health.mem;
if alert.level < health.mem.level {
alert.level = health.mem.level;
alert.data = AlertData::ServerMem {
id: server_status.id.clone(),
name: server.name.clone(),
region: optional_string(&server.info.region),
region: optional_string(&server.config.region),
total_gb: server_status
.stats
.as_ref()
@@ -236,15 +244,16 @@ pub async fn alert_servers(
.map(|s| s.mem_used_gb)
.unwrap_or(0.0),
};
alerts_to_update.push((alert, server.info.send_mem_alerts));
alerts_to_update
.push((alert, server.config.send_mem_alerts));
}
}
(SeverityLevel::Ok, Some(alert)) => {
(SeverityLevel::Ok, Some(alert), true) => {
let mut alert = alert.clone();
alert.data = AlertData::ServerMem {
id: server_status.id.clone(),
name: server.name.clone(),
region: optional_string(&server.info.region),
region: optional_string(&server.config.region),
total_gb: server_status
.stats
.as_ref()
@@ -256,7 +265,8 @@ pub async fn alert_servers(
.map(|s| s.mem_used_gb)
.unwrap_or(0.0),
};
alert_ids_to_close.push((alert, server.info.send_mem_alerts))
alert_ids_to_close
.push((alert, server.config.send_mem_alerts))
}
_ => {}
}
@@ -273,8 +283,12 @@ pub async fn alert_servers(
.as_ref()
.and_then(|alerts| alerts.get(path))
.cloned();
match (*health, disk_alert) {
(SeverityLevel::Warning | SeverityLevel::Critical, None) => {
match (health.level, disk_alert, health.should_close_alert) {
(
SeverityLevel::Warning | SeverityLevel::Critical,
None,
_,
) => {
let disk = server_status.stats.as_ref().and_then(|stats| {
stats.disks.iter().find(|disk| disk.mount == *path)
});
@@ -283,58 +297,60 @@ pub async fn alert_servers(
ts,
resolved: false,
resolved_ts: None,
level: *health,
level: health.level,
target: ResourceTarget::Server(server_status.id.clone()),
data: AlertData::ServerDisk {
id: server_status.id.clone(),
name: server.name.clone(),
region: optional_string(&server.info.region),
region: optional_string(&server.config.region),
path: path.to_owned(),
total_gb: disk.map(|d| d.total_gb).unwrap_or_default(),
used_gb: disk.map(|d| d.used_gb).unwrap_or_default(),
},
};
alerts_to_open.push((alert, server.info.send_disk_alerts));
alerts_to_open
.push((alert, server.config.send_disk_alerts));
}
(
SeverityLevel::Warning | SeverityLevel::Critical,
Some(mut alert),
_,
) => {
// Disk is persistent, update alert if health changes regardless of direction
if *health != alert.level {
if health.level != alert.level {
let disk =
server_status.stats.as_ref().and_then(|stats| {
stats.disks.iter().find(|disk| disk.mount == *path)
});
alert.level = *health;
alert.level = health.level;
alert.data = AlertData::ServerDisk {
id: server_status.id.clone(),
name: server.name.clone(),
region: optional_string(&server.info.region),
region: optional_string(&server.config.region),
path: path.to_owned(),
total_gb: disk.map(|d| d.total_gb).unwrap_or_default(),
used_gb: disk.map(|d| d.used_gb).unwrap_or_default(),
};
alerts_to_update
.push((alert, server.info.send_disk_alerts));
.push((alert, server.config.send_disk_alerts));
}
}
(SeverityLevel::Ok, Some(alert)) => {
(SeverityLevel::Ok, Some(alert), true) => {
let mut alert = alert.clone();
let disk = server_status.stats.as_ref().and_then(|stats| {
stats.disks.iter().find(|disk| disk.mount == *path)
});
alert.level = *health;
alert.level = health.level;
alert.data = AlertData::ServerDisk {
id: server_status.id.clone(),
name: server.name.clone(),
region: optional_string(&server.info.region),
region: optional_string(&server.config.region),
path: path.to_owned(),
total_gb: disk.map(|d| d.total_gb).unwrap_or_default(),
used_gb: disk.map(|d| d.used_gb).unwrap_or_default(),
};
alert_ids_to_close
.push((alert, server.info.send_disk_alerts))
.push((alert, server.config.send_disk_alerts))
}
_ => {}
}
@@ -347,7 +363,7 @@ pub async fn alert_servers(
let mut alert = alert.clone();
alert.level = SeverityLevel::Ok;
alert_ids_to_close
.push((alert, server.info.send_disk_alerts));
.push((alert, server.config.send_disk_alerts));
}
}
}

View File

@@ -6,7 +6,10 @@ use komodo_client::entities::{
network::NetworkListItem, volume::VolumeListItem,
},
repo::Repo,
server::{Server, ServerConfig, ServerHealth, ServerState},
server::{
Server, ServerConfig, ServerHealth, ServerHealthState,
ServerState,
},
stack::{ComposeProject, Stack, StackState},
stats::{SingleDiskUsage, SystemStats},
};
@@ -126,6 +129,8 @@ pub async fn insert_server_status(
.await;
}
const ALERT_PERCENTAGE_THRESHOLD: f32 = 5.0;
fn get_server_health(
server: &Server,
SystemStats {
@@ -148,16 +153,22 @@ fn get_server_health(
let mut health = ServerHealth::default();
if cpu_perc >= cpu_critical {
health.cpu = SeverityLevel::Critical
health.cpu.level = SeverityLevel::Critical;
} else if cpu_perc >= cpu_warning {
health.cpu = SeverityLevel::Warning
health.cpu.level = SeverityLevel::Warning
} else if *cpu_perc < cpu_warning - ALERT_PERCENTAGE_THRESHOLD {
health.cpu.should_close_alert = true
}
let mem_perc = 100.0 * mem_used_gb / mem_total_gb;
if mem_perc >= *mem_critical {
health.mem = SeverityLevel::Critical
health.mem.level = SeverityLevel::Critical
} else if mem_perc >= *mem_warning {
health.mem = SeverityLevel::Warning
health.mem.level = SeverityLevel::Warning
} else if mem_perc
< mem_warning - (ALERT_PERCENTAGE_THRESHOLD as f64)
{
health.mem.should_close_alert = true
}
for SingleDiskUsage {
@@ -168,14 +179,17 @@ fn get_server_health(
} in disks
{
let perc = 100.0 * used_gb / total_gb;
let stats_state = if perc >= *disk_critical {
SeverityLevel::Critical
let mut state = ServerHealthState::default();
if perc >= *disk_critical {
state.level = SeverityLevel::Critical;
} else if perc >= *disk_warning {
SeverityLevel::Warning
} else {
SeverityLevel::Ok
state.level = SeverityLevel::Warning;
} else if perc
< disk_warning - (ALERT_PERCENTAGE_THRESHOLD as f64)
{
state.should_close_alert = true;
};
health.disks.insert(mount.clone(), stats_state);
health.disks.insert(mount.clone(), state);
}
health

View File

@@ -1,23 +0,0 @@
[package]
name = "migrator"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# komodo_client.workspace = true
logger.workspace = true
#
# mungos.workspace = true
#
tokio.workspace = true
anyhow.workspace = true
dotenvy.workspace = true
envy.workspace = true
serde.workspace = true
tracing.workspace = true

View File

@@ -1,16 +0,0 @@
FROM rust:1.80.1-bookworm AS builder
WORKDIR /builder
COPY . .
RUN cargo build -p migrator --release
# Final Image
FROM gcr.io/distroless/cc-debian12
COPY --from=builder /builder/target/release/migrator /
# Label for Ghcr
LABEL org.opencontainers.image.source=https://github.com/mbecker20/komodo
LABEL org.opencontainers.image.description="Database migrator for Komodo version upgrades"
LABEL org.opencontainers.image.licenses=GPL-3.0
CMD ["./migrator"]

View File

@@ -1,25 +0,0 @@
# Migrator
Performs schema changes on the Komodo database
## v1.7 - v1.11 migration
Run this before upgrading to latest from versions 1.7 to 1.11.
```sh
docker run --rm --name komodo-migrator \
--network "host" \
--env MIGRATION="v1.11" \
--env TARGET_URI="mongodb://<USERNAME>:<PASSWORD>@<ADDRESS>" \
--env TARGET_DB_NAME="<DB_NAME>" \
ghcr.io/mbecker20/komodo_migrator
```
## v1.0 - v1.6 migration
Run this before upgrading to latest from versions 1.0 to 1.6.
```sh
docker run --rm --name komodo-migrator \
--network "host" \
--env MIGRATION="v1.6" \
--env TARGET_URI="mongodb://<USERNAME>:<PASSWORD>@<ADDRESS>" \
--env TARGET_DB_NAME="<DB_NAME>" \
ghcr.io/mbecker20/komodo_migrator
```

View File

@@ -1,2 +0,0 @@
#[allow(unused)]
pub mod v1_11;

View File

@@ -1,261 +0,0 @@
use komodo_client::entities::{
build::StandardRegistryConfig, EnvironmentVar, NoData,
SystemCommand, Version, I64,
};
use serde::{Deserialize, Serialize};
use super::resource::Resource;
pub type Build = Resource<BuildConfig, BuildInfo>;
impl From<Build> for komodo_client::entities::build::Build {
fn from(value: Build) -> Self {
komodo_client::entities::build::Build {
id: value.id,
name: value.name,
description: value.description,
updated_at: value.updated_at,
tags: value.tags,
info: komodo_client::entities::build::BuildInfo {
last_built_at: value.info.last_built_at,
built_hash: None,
built_message: None,
latest_hash: None,
latest_message: None,
},
config: value.config.into(),
base_permission: Default::default(),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct BuildInfo {
pub last_built_at: I64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuildConfig {
/// Which builder is used to build the image.
#[serde(default, alias = "builder")]
pub builder_id: String,
/// The current version of the build.
#[serde(default)]
pub version: Version,
/// The Github repo used as the source of the build.
#[serde(default)]
pub repo: String,
/// The branch of the repo.
#[serde(default = "default_branch")]
pub branch: String,
/// Optionally set a specific commit hash.
#[serde(default)]
pub commit: String,
/// The github account used to clone (used to access private repos).
/// Empty string is public clone (only public repos).
#[serde(default)]
pub github_account: String,
/// The optional command run after repo clone and before docker build.
#[serde(default)]
pub pre_build: SystemCommand,
/// Configuration for the registry to push the built image to.
#[serde(default)]
pub image_registry: ImageRegistry,
/// The path of the docker build context relative to the root of the repo.
/// Default: "." (the root of the repo).
#[serde(default = "default_build_path")]
pub build_path: String,
/// The path of the dockerfile relative to the build path.
#[serde(default = "default_dockerfile_path")]
pub dockerfile_path: String,
/// Whether to skip secret interpolation in the build_args.
#[serde(default)]
pub skip_secret_interp: bool,
/// Whether to use buildx to build (eg `docker buildx build ...`)
#[serde(default)]
pub use_buildx: bool,
/// Whether incoming webhooks actually trigger action.
#[serde(default = "default_webhook_enabled")]
pub webhook_enabled: bool,
/// Any extra docker cli arguments to be included in the build command
#[serde(default)]
pub extra_args: Vec<String>,
/// Docker build arguments.
///
/// These values are visible in the final image by running `docker inspect`.
#[serde(
default,
deserialize_with = "komodo_client::entities::env_vars_deserializer"
)]
pub build_args: Vec<EnvironmentVar>,
/// Secret arguments.
///
/// These values remain hidden in the final image by using
/// docker secret mounts. See `<https://docs.docker.com/build/building/secrets>`.
///
/// The values can be used in RUN commands:
/// ```
/// RUN --mount=type=secret,id=SECRET_KEY \
/// SECRET_KEY=$(cat /run/secrets/SECRET_KEY) ...
/// ```
#[serde(
default,
deserialize_with = "komodo_client::entities::env_vars_deserializer"
)]
pub secret_args: Vec<EnvironmentVar>,
/// Docker labels
#[serde(
default,
deserialize_with = "komodo_client::entities::env_vars_deserializer"
)]
pub labels: Vec<EnvironmentVar>,
}
impl From<BuildConfig>
for komodo_client::entities::build::BuildConfig
{
fn from(value: BuildConfig) -> Self {
komodo_client::entities::build::BuildConfig {
builder_id: value.builder_id,
skip_secret_interp: value.skip_secret_interp,
version: komodo_client::entities::Version {
major: value.version.major,
minor: value.version.minor,
patch: value.version.patch,
},
links: Default::default(),
auto_increment_version: true,
image_name: Default::default(),
image_tag: Default::default(),
git_provider: String::from("github.com"),
git_https: true,
repo: value.repo,
branch: value.branch,
commit: value.commit,
git_account: value.github_account,
pre_build: komodo_client::entities::SystemCommand {
path: value.pre_build.path,
command: value.pre_build.command,
},
build_path: value.build_path,
dockerfile_path: value.dockerfile_path,
build_args: value
.build_args
.into_iter()
.map(Into::into)
.collect(),
secret_args: Default::default(),
labels: value.labels.into_iter().map(Into::into).collect(),
extra_args: value.extra_args,
use_buildx: value.use_buildx,
webhook_enabled: value.webhook_enabled,
webhook_secret: Default::default(),
image_registry: value.image_registry.into(),
}
}
}
fn default_branch() -> String {
String::from("main")
}
fn default_build_path() -> String {
String::from(".")
}
fn default_dockerfile_path() -> String {
String::from("Dockerfile")
}
fn default_webhook_enabled() -> bool {
true
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", content = "params")]
pub enum ImageRegistry {
/// Don't push the image to any registry
None(NoData),
/// Push the image to DockerHub
DockerHub(CloudRegistryConfig),
/// Push the image to the Github Container Registry.
///
/// See [the Github docs](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#pushing-container-images)
/// for information on creating an access token
Ghcr(CloudRegistryConfig),
/// Push the image to Aws Elastic Container Registry
///
/// The string held in 'params' should match a label of an `aws_ecr_registry` in the core config.
AwsEcr(String),
/// Todo. Will point to a custom "Registry" resource by id
Custom(String),
}
impl Default for ImageRegistry {
fn default() -> Self {
Self::None(NoData {})
}
}
impl From<ImageRegistry>
for komodo_client::entities::build::ImageRegistry
{
fn from(value: ImageRegistry) -> Self {
match value {
ImageRegistry::None(_) | ImageRegistry::Custom(_) => {
komodo_client::entities::build::ImageRegistry::None(NoData {})
}
ImageRegistry::DockerHub(params) => {
komodo_client::entities::build::ImageRegistry::Standard(
StandardRegistryConfig {
domain: String::from("docker.io"),
account: params.account,
organization: params.organization,
},
)
}
ImageRegistry::Ghcr(params) => {
komodo_client::entities::build::ImageRegistry::Standard(
StandardRegistryConfig {
domain: String::from("ghcr.io"),
account: params.account,
organization: params.organization,
},
)
}
ImageRegistry::AwsEcr(label) => {
komodo_client::entities::build::ImageRegistry::None(NoData {})
}
}
}
}
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct CloudRegistryConfig {
/// Specify an account to use with the cloud registry.
#[serde(default)]
pub account: String,
/// Optional. Specify an organization to push the image under.
/// Empty string means no organization.
#[serde(default)]
pub organization: String,
}

View File

@@ -1,168 +0,0 @@
use komodo_client::entities::{
deployment::{
conversions_deserializer, term_labels_deserializer, Conversion,
DeploymentImage, RestartMode, TerminationSignalLabel,
},
env_vars_deserializer, EnvironmentVar, TerminationSignal,
};
use serde::{Deserialize, Serialize};
use super::{build::ImageRegistry, resource::Resource};
pub type Deployment = Resource<DeploymentConfig, ()>;
impl From<Deployment>
for komodo_client::entities::deployment::Deployment
{
fn from(value: Deployment) -> Self {
komodo_client::entities::deployment::Deployment {
id: value.id,
name: value.name,
description: value.description,
updated_at: value.updated_at,
tags: value.tags,
info: (),
config: value.config.into(),
base_permission: Default::default(),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct DeploymentConfig {
/// The id of server the deployment is deployed on.
#[serde(default, alias = "server")]
pub server_id: String,
/// The image which the deployment deploys.
/// Can either be a user inputted image, or a Komodo build.
#[serde(default)]
pub image: DeploymentImage,
/// Configure the registry used to pull the image from the registry.
/// Used with `docker login`.
///
/// When using attached build as image source:
/// - If the field is `None` variant, will use the same ImageRegistry config as the build.
/// - Otherwise, it must match the variant of the ImageRegistry build config.
/// - Only the account is used, the organization is not needed here
#[serde(default)]
pub image_registry: ImageRegistry,
/// Whether to skip secret interpolation into the deployment environment variables.
#[serde(default)]
pub skip_secret_interp: bool,
/// Whether to redeploy the deployment whenever the attached build finishes.
#[serde(default)]
pub redeploy_on_build: bool,
/// Whether to send ContainerStateChange alerts for this deployment.
#[serde(default = "default_send_alerts")]
pub send_alerts: bool,
/// The network attached to the container.
/// Default is `host`.
#[serde(default = "default_network")]
pub network: String,
/// The restart mode given to the container.
#[serde(default)]
pub restart: RestartMode,
/// This is interpolated at the end of the `docker run` command,
/// which means they are either passed to the containers inner process,
/// or replaces the container command, depending on use of ENTRYPOINT or CMD in dockerfile.
/// Empty is no command.
#[serde(default)]
pub command: String,
/// The default termination signal to use to stop the deployment. Defaults to SigTerm (default docker signal).
#[serde(default)]
pub termination_signal: TerminationSignal,
/// The termination timeout.
#[serde(default = "default_termination_timeout")]
pub termination_timeout: i32,
/// Extra args which are interpolated into the `docker run` command,
/// and affect the container configuration.
#[serde(default)]
pub extra_args: Vec<String>,
/// Labels attached to various termination signal options.
/// Used to specify different shutdown functionality depending on the termination signal.
#[serde(
default = "default_term_signal_labels",
deserialize_with = "term_labels_deserializer"
)]
pub term_signal_labels: Vec<TerminationSignalLabel>,
/// The container port mapping.
/// Irrelevant if container network is `host`.
/// Maps ports on host to ports on container.
#[serde(default, deserialize_with = "conversions_deserializer")]
pub ports: Vec<Conversion>,
/// The container volume mapping.
/// Maps files / folders on host to files / folders in container.
#[serde(default, deserialize_with = "conversions_deserializer")]
pub volumes: Vec<Conversion>,
/// The environment variables passed to the container.
#[serde(default, deserialize_with = "env_vars_deserializer")]
pub environment: Vec<EnvironmentVar>,
/// The docker labels given to the container.
#[serde(default, deserialize_with = "env_vars_deserializer")]
pub labels: Vec<EnvironmentVar>,
}
fn default_send_alerts() -> bool {
true
}
fn default_term_signal_labels() -> Vec<TerminationSignalLabel> {
vec![TerminationSignalLabel::default()]
}
fn default_termination_timeout() -> i32 {
10
}
fn default_network() -> String {
String::from("host")
}
impl From<DeploymentConfig>
for komodo_client::entities::deployment::DeploymentConfig
{
fn from(value: DeploymentConfig) -> Self {
komodo_client::entities::deployment::DeploymentConfig {
server_id: value.server_id,
image: value.image,
image_registry_account: match value.image_registry {
ImageRegistry::None(_)
| ImageRegistry::AwsEcr(_)
| ImageRegistry::Custom(_) => String::new(),
ImageRegistry::DockerHub(params) => params.account,
ImageRegistry::Ghcr(params) => params.account,
},
skip_secret_interp: value.skip_secret_interp,
redeploy_on_build: value.redeploy_on_build,
send_alerts: value.send_alerts,
network: value.network,
restart: value.restart,
command: value.command,
termination_signal: value.termination_signal,
termination_timeout: value.termination_timeout,
extra_args: value.extra_args,
term_signal_labels: value.term_signal_labels,
ports: value.ports,
volumes: value.volumes,
environment: value.environment,
labels: value.labels,
links: Default::default(),
}
}
}

View File

@@ -1,48 +0,0 @@
// use mungos::{init::MongoBuilder, mongodb::Collection};
// use serde::{Deserialize, Serialize};
// pub mod build;
// pub mod deployment;
// pub mod resource;
// pub struct DbClient {
// pub builds: Collection<build::Build>,
// pub deployments: Collection<deployment::Deployment>,
// }
// impl DbClient {
// pub async fn new(
// legacy_uri: &str,
// legacy_db_name: &str,
// ) -> DbClient {
// let client = MongoBuilder::default()
// .uri(legacy_uri)
// .build()
// .await
// .expect("failed to init legacy mongo client");
// let db = client.database(legacy_db_name);
// DbClient {
// builds: db.collection("Build"),
// deployments: db.collection("Deployment"),
// }
// }
// }
// #[derive(
// Serialize, Deserialize, Debug, Clone, Default, PartialEq,
// )]
// pub struct Version {
// pub major: i32,
// pub minor: i32,
// pub patch: i32,
// }
// #[derive(
// Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq,
// )]
// pub struct SystemCommand {
// #[serde(default)]
// pub path: String,
// #[serde(default)]
// pub command: String,
// }

View File

@@ -1,54 +0,0 @@
use mungos::mongodb::bson::serde_helpers::hex_string_as_object_id;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Resource<Config, Info: Default = ()> {
/// The Mongo ID of the resource.
/// This field is de/serialized from/to JSON as
/// `{ "_id": { "$oid": "..." }, ...(rest of serialized Resource<T>) }`
#[serde(
default,
rename = "_id",
skip_serializing_if = "String::is_empty",
with = "hex_string_as_object_id"
)]
pub id: String,
/// The resource name.
/// This is guaranteed unique among others of the same resource type.
pub name: String,
/// A description for the resource
#[serde(default)]
pub description: String,
/// When description last updated
#[serde(default)]
pub updated_at: i64,
/// Tag Ids
#[serde(default)]
pub tags: Vec<String>,
/// Resource-specific information (not user configurable).
#[serde(default)]
pub info: Info,
/// Resource-specific configuration.
pub config: Config,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ResourceListItem<Info> {
/// The resource id
pub id: String,
/// The resource type, ie `Server` or `Deployment`
// #[serde(rename = "type")]
// pub resource_type: ResourceTargetVariant,
/// The resource name
pub name: String,
/// Tag Ids
pub tags: Vec<String>,
/// Resource specific info
pub info: Info,
}

View File

@@ -1,46 +0,0 @@
#![allow(unused)]
#[macro_use]
extern crate tracing;
use serde::Deserialize;
mod legacy;
mod migrate;
#[derive(Deserialize)]
enum Migration {
#[serde(alias = "v1.11")]
V1_11,
}
#[derive(Deserialize)]
struct Env {
migration: Migration,
target_uri: String,
target_db_name: String,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
dotenvy::dotenv().ok();
logger::init(&Default::default())?;
info!("starting migrator");
let env: Env = envy::from_env()?;
match env.migration {
Migration::V1_11 => {
// let db = legacy::v1_11::DbClient::new(
// &env.target_uri,
// &env.target_db_name,
// )
// .await;
// migrate::v1_11::migrate_all_in_place(&db).await?
}
}
info!("finished!");
Ok(())
}

View File

@@ -1 +0,0 @@
pub mod v1_11;

View File

@@ -1,70 +0,0 @@
// use anyhow::Context;
// use komodo_client::entities::{build::Build, deployment::Deployment};
// use mungos::{
// find::find_collect,
// mongodb::bson::{doc, to_document},
// };
// use crate::legacy::v1_11;
// pub async fn migrate_all_in_place(
// db: &v1_11::DbClient,
// ) -> anyhow::Result<()> {
// migrate_builds_in_place(db).await?;
// migrate_deployments_in_place(db).await?;
// Ok(())
// }
// pub async fn migrate_builds_in_place(
// db: &v1_11::DbClient,
// ) -> anyhow::Result<()> {
// let builds = find_collect(&db.builds, None, None)
// .await
// .context("failed to get builds")?
// .into_iter()
// .map(Into::into)
// .collect::<Vec<Build>>();
// info!("migrating {} builds...", builds.len());
// for build in builds {
// db.builds
// .update_one(
// doc! { "name": &build.name },
// doc! { "$set": to_document(&build)? },
// )
// .await
// .context("failed to insert builds on target")?;
// }
// info!("builds have been migrated\n");
// Ok(())
// }
// pub async fn migrate_deployments_in_place(
// db: &v1_11::DbClient,
// ) -> anyhow::Result<()> {
// let deployments = find_collect(&db.deployments, None, None)
// .await
// .context("failed to get deployments")?
// .into_iter()
// .map(Into::into)
// .collect::<Vec<Deployment>>();
// info!("migrating {} deployments...", deployments.len());
// for deployment in deployments {
// db.deployments
// .update_one(
// doc! { "name": &deployment.name },
// doc! { "$set": to_document(&deployment)? },
// )
// .await
// .context("failed to insert deployments on target")?;
// }
// info!("deployments have been migrated\n");
// Ok(())
// }

View File

@@ -91,10 +91,13 @@ impl StatsClient {
}
pub fn get_system_stats(&self) -> SystemStats {
let total_mem = self.system.total_memory();
let available_mem = self.system.available_memory();
SystemStats {
cpu_perc: self.system.global_cpu_usage(),
mem_used_gb: self.system.used_memory() as f64 / BYTES_PER_GB,
mem_total_gb: self.system.total_memory() as f64 / BYTES_PER_GB,
mem_free_gb: self.system.free_memory() as f64 / BYTES_PER_GB,
mem_used_gb: (total_mem - available_mem) as f64 / BYTES_PER_GB,
mem_total_gb: total_mem as f64 / BYTES_PER_GB,
disks: self.get_disks(),
polling_rate: self.stats.polling_rate,
refresh_ts: self.stats.refresh_ts,

View File

@@ -1,109 +0,0 @@
# Changelog
## <ins>Komodo v1.14 (Sep 2024)</ins>
- Renamed the project to **Komodo**.
- Manage docker networks, volumes, and images.
- Manage Containers at the server level, without creating any Deployment.
- Add bulk Start / Restart / Pause actions for all containers on a server.
- Add **Secret** mode to Variables to hide the value in updates / logs
- Secret mode also prevents any non-admin users from retrieving the value from the API. Non admin users will still see the variable name.
- Interpolate Variables / Secrets into everything I could think of
- Deployment / Stack / Repo / Build **extra args**.
- Deployment **command**.
- Build **pre build**.
- Repo **on_clone / on_pull**.
- Added **Hetzner Singapore** datacenter for Hetzner ServerTemplates
- **Removed Google Font** - now just use system local font to avoid any third party calls.
## <ins>Monitor v1.13 - Komodo (Aug 2024)</ins>
- This is the first named release, as I think it is really big. The Komodo Dragon is the largest species of Monitor lizard.
- **Deploy docker compose** with the new **Stack** resource.
- Can define the compose file in the UI, or direct Monitor to clone a git repo containing compose files.
- Use webhooks to redeploy the stack on push to the repo
- Manage the environment variables passed to the compose command.
- **Builds** can now be configured with an alternate repository name to push the image under.
-An optional tag can also be configured to be postfixed onto the version, like image:1.13-aarch64.
This helps for pushing alternate build configurations under the same image repo, just under different tags.
- **Repos** can now be "built" using builders. The idea is, you spawn an AWS instance, clone a repo, execute a shell command
(like running a script in the repo), and terminating the instance. The script can build a binary, and push it to some binary repository.
Users will have to manage their own versioning though.
- **High level UI Updates** courtesy of @karamvirsingh98
## <ins>v1.12 (July 2024)</ins>
- Break free of Github dependance. Use other git providers, including self hosted ones.
- Same for Docker registry. You can also now use any docker registry for your images.
## <ins>v1 (Spring 2024)</ins>
- **New resource types**:
- **Repo**: Clone / pull configured repositories on desired Server. Run shell commands in the repo on every clone / pull to acheive automation. Listen for pushes to a particular branch to automatically pull the repo and run the command.
- **Procedure**: Combine multiple *executions* (Deploy, RunBuild) and run them in sequence or in parallel. *RunProcedure* is an execution type, meaning procedures can run *other procedures*.
- **Builder**: Ephemeral builder configuration has moved to being an API / UI managed entity for greater observability and ease of management.
- **Alerter**: Define multiple alerting endpoints and manage them via the API / UI.
- Slack support continues with the *Slack* Alerter variant.
- Send JSON serialized alert data to any HTTP endpoint with the *Custom* Alerter variant.
- **Template**: Define a template for your cloud provider's VM configuration
- Launch VMs based on the template and automatically add them as Monitor servers.
- Supports AWS EC2 and Hetzner Cloud.
- **Sync**: Sync resources declared in toml files in Github repos.
- Manage resources declaratively, with git history for configuration rollbacks.
- See the actions which will be performed in the UI, and execute them upon manual confirmation.
- Use a Git webhook to automatically execute syncs on git push.
- **Resource Tagging**
- Attach multiple *tags* to resources, which can be used to group related resources together. These can be used to filter resources in the UI.
- For example, resources can be given tags for *environment*, like `Prod`, `Uat`, or `Dev`. This can be combined with tags for the larger system the resource is a part of, such as `Authentication`, `Logging`, or `Messaging`.
- Proper tagging will make it easy to find resources and visualize system components, even as the number of resources grows large.
- **Variables**
- Manage global, non-secret key-value pairs using the API / UI.
- These values can be interpolated into deployment `environments` and build `build_args`
- **Core Accounts and Secrets**
- Docker / Github accounts and Secrets can now be configured in the Core configuration file.
- They can still be added to the Periphery configuration as before. Accounts / Secrets defined in the Core configuration will be preferentially used over like ones defined in Periphery configuration.
- **User Groups**
- Admins can now create User Groups and assign permissions to them as if they were a user.
- Multiple users can then be added to the group, and a user can be added to multiple groups.
- Users in the group inherit the group's permissions.
- **Builds**
- Build log displays the **latest commit hash and message**.
- In-progress builds are able to be **cancelled before completion**.
- Specify a specific commit hash to build from.
- **Deployments**
- Filter log lines using multiple search terms.
- **Alerting**
- The alerting system has been redesigned with a stateful model.
- Alerts can be in an Open or a Resolved state, and alerts are only sent on state changes.
- For example, say a server has just crossed 80% memory usage, the configured memory threshold. An alert will be created in the Open state and the alert data will be sent out. Later, it has dropped down to 70%. The alert will be changed to the Resolved state and the alert data will again be sent.
- In addition to server usage alerts, Monitor now supports deployment state change alerts. These are sent when a deployment's state changes without being caused by a Monitor action. For example, if a deployment goes from the Running state to the Exited state unexpectedly, say from a crash, an alert will be sent.
- Current and past alerts can be retrieved using the API and viewed on the UI.
- **New UI**:
- The Monitor UI has been revamped to support the new features and improve the user experience.
## <ins>v0 (Winter 2022)</ins>
- Move Core and Periphery implementation to Rust.
- Add AWS Ec2 ephemeral build instance support.
- Configuration added to core configuration file.
- Automatic build versioning system, supporting image rollback.
- Realtime and historical system stats - CPU, memory, disk.
- Simple stats alerting based on out-of-bounds values for system stats.
- Support sending alerts to Slack.
## <ins>Pre-versioned releases</ins>
- Defined main resource types:
- Server
- Deployment
- Build
- Basics of Monitor:
- Build docker images from Github repos.
- Manage image deployment on connected servers, see container status, get container logs.
- Add account credentials in Periphery configuration.
- Core and Periphery implemented in Typescript.

View File

@@ -1,4 +1,4 @@
# Komodo
*A system to build and deploy software accross many servers*
*A system to build and deploy software across many servers*
Docs: [https://docs.rs/komodo_client/latest/komodo_client](https://docs.rs/komodo_client/latest/komodo_client)

View File

@@ -7,11 +7,14 @@
//! - Path: `/auth`, `/user`, `/read`, `/write`, `/execute`
//! - Headers:
//! - Content-Type: `application/json`
//! - Authorication: `your_jwt`
//! - Authorization: `your_jwt`
//! - X-Api-Key: `your_api_key`
//! - X-Api-Secret: `your_api_secret`
//! - Use either Authorization *or* X-Api-Key and X-Api-Secret to authenticate requests.
//! - Body: JSON specifying the request type (`type`) and the parameters (`params`).
//!
//! You can create API keys for your user, or for a Service User with limited permissions,
//! from the Komodo UI Settings page.
//!
//! To call the api, construct JSON bodies following
//! the schemas given in [read], [mod@write], [execute], and so on.
@@ -28,6 +31,18 @@
//!
//! The request's parent module (eg. [read], [mod@write]) determines the http path which
//! must be used for the requests. For example, requests under [read] are made using http path `/read`.
//!
//! ## Curl Example
//!
//! Putting it all together, here is an example `curl` for [write::UpdateBuild], to update the version:
//!
//! ```text
//! curl --header "Content-Type: application/json" \
//! --header "X-Api-Key: your_api_key" \
//! --header "X-Api-Secret: your_api_secret" \
//! --data '{ "type": "UpdateBuild", "params": { "id": "67076689ed600cfdd52ac637", "config": { "version": "1.15.9" } } }' \
//! https://komodo.example.com/write
//! ```
//!
//! ## Modules
//!

View File

@@ -223,13 +223,22 @@ impl Default for ServerConfig {
}
}
/// The health of a part of the server.
#[typeshare]
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
pub struct ServerHealthState {
pub level: SeverityLevel,
/// Whether the health is good enough to close an open alert.
pub should_close_alert: bool,
}
/// Summary of the health of the server.
#[typeshare]
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
pub struct ServerHealth {
pub cpu: SeverityLevel,
pub mem: SeverityLevel,
pub disks: HashMap<PathBuf, SeverityLevel>,
pub cpu: ServerHealthState,
pub mem: ServerHealthState,
pub disks: HashMap<PathBuf, ServerHealthState>,
}
/// Current pending actions on the server.

View File

@@ -59,7 +59,13 @@ pub struct SystemStatsRecord {
pub struct SystemStats {
/// Cpu usage percentage
pub cpu_perc: f32,
/// Memory used in GB
/// [1.15.9+]
/// Free memory in GB.
/// This is really the 'Free' memory, not the 'Available' memory.
/// It may be different than mem_total_gb - mem_used_gb.
#[serde(default)]
pub mem_free_gb: f64,
/// Used memory in GB. 'Total' - 'Available' (not free) memory.
pub mem_used_gb: f64,
/// Total memory in GB
pub mem_total_gb: f64,

View File

@@ -1,5 +1,5 @@
//! # Komodo
//! *A system to build and deploy software accross many servers*
//! *A system to build and deploy software across many servers*
//!
//! This is a client library for the Komodo Core API.
//! It contains:
@@ -35,9 +35,9 @@ use serde::Deserialize;
pub mod api;
pub mod busy;
pub mod entities;
pub mod parser;
pub mod ws;
mod parser;
mod request;
#[derive(Deserialize)]

View File

@@ -55,3 +55,51 @@ pub fn parse_key_value_list(
})
.collect::<anyhow::Result<Vec<_>>>()
}
/// Parses commands out of multiline string
/// and chains them together with '&&'
///
/// Supports full line and end of line comments, and escaped newlines.
///
/// ## Example:
/// ```sh
/// # comments supported
/// sh ./shell1.sh # end of line supported
/// sh ./shell2.sh
///
/// # escaped newlines supported
/// curl --header "Content-Type: application/json" \
/// --request POST \
/// --data '{"key": "value"}' \
/// https://destination.com
///
/// # print done
/// echo done
/// ```
/// becomes
/// ```sh
/// sh ./shell1.sh && sh ./shell2.sh && {long curl command} && echo done
/// ```
pub fn parse_multiline_command(command: impl AsRef<str>) -> String {
command
.as_ref()
// Remove comments and join back
.split('\n')
.map(str::trim)
.filter(|line| !line.is_empty() && !line.starts_with('#'))
.filter_map(|line| line.split(" #").next())
.collect::<Vec<_>>()
.join("\n")
// Remove escaped newlines
.split(" \\")
.map(str::trim)
.fold(String::new(), |acc, el| acc + " " + el)
// Then final split by newlines and join with &&
.split('\n')
.map(str::trim)
.filter(|line| !line.is_empty() && !line.starts_with('#'))
.filter_map(|line| line.split(" #").next())
.map(str::trim)
.collect::<Vec<_>>()
.join(" && ")
}

37
client/core/ts/README.md Normal file
View File

@@ -0,0 +1,37 @@
# Komodo
_A system to build and deploy software across many servers_
```sh
npm install komodo_client
```
or
```sh
yarn add komodo_client
```
```ts
import { KomodoClient, Types } from "komodo_client";
const komodo = KomodoClient("https://demo.komo.do", {
type: "api-key",
params: {
api_key: "your_key",
secret: "your secret",
},
});
const stacks: Types.StackListItem[] = await komodo.read({
type: "ListStacks",
params: {},
});
const stack: Types.Stack = await komodo.read({
type: "GetStack",
params: {
stack: stacks[0].name,
}
});
```

View File

@@ -1,16 +1,21 @@
{
"name": "@komodo/client",
"version": "1.0.0",
"name": "komodo_client",
"version": "1.15.9",
"description": "Komodo client package",
"homepage": "https://komo.do",
"main": "dist/lib.js",
"license": "MIT",
"license": "GPL-3.0",
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "tsc"
},
"dependencies": {
"axios": "^1.4.0"
"axios": "^1.7.7"
},
"devDependencies": {
"typescript": "^5.1.6"
"typescript": "^5.6.3"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}

View File

@@ -5,25 +5,25 @@ import {
ReadResponses,
UserResponses,
WriteResponses,
} from "./responses";
} from "./responses.js";
import {
AuthRequest,
ExecuteRequest,
ReadRequest,
UserRequest,
WriteRequest,
} from "./types";
} from "./types.js";
export * as Types from "./types";
export * as Types from "./types.js";
type InitOptions =
| { type: "jwt"; params: { jwt: string } }
| { type: "api-key"; params: { api_key: string; secret: string } };
| { type: "api-key"; params: { key: string; secret: string } };
export function KomodoClient(url: string, options: InitOptions) {
const state = {
jwt: options.type === "jwt" ? options.params.jwt : undefined,
api_key: options.type === "api-key" ? options.params.api_key : undefined,
key: options.type === "api-key" ? options.params.key : undefined,
secret: options.type === "api-key" ? options.params.secret : undefined,
};
@@ -32,7 +32,7 @@ export function KomodoClient(url: string, options: InitOptions) {
.post<Res>(url + path, request, {
headers: {
Authorization: state.jwt,
"X-API-KEY": state.api_key,
"X-API-KEY": state.key,
"X-API-SECRET": state.secret,
},
})

View File

@@ -1,4 +1,4 @@
import * as Types from "./types";
import * as Types from "./types.js";
export type AuthResponses = {
GetLoginOptions: Types.GetLoginOptionsResponse;

View File

@@ -2243,7 +2243,14 @@ export enum Timelength {
export interface SystemStats {
/** Cpu usage percentage */
cpu_perc: number;
/** Memory used in GB */
/**
* [1.15.9+]
* Free memory in GB.
* This is really the 'Free' memory, not the 'Available' memory.
* It may be different than mem_total_gb - mem_used_gb.
*/
mem_free_gb?: number;
/** Used memory in GB. 'Total' - 'Available' (not free) memory. */
mem_used_gb: number;
/** Total memory in GB */
mem_total_gb: number;
@@ -6435,11 +6442,18 @@ export interface CloneArgs {
account?: string;
}
/** The health of a part of the server. */
export interface ServerHealthState {
level: SeverityLevel;
/** Whether the health is good enough to close an open alert. */
should_close_alert: boolean;
}
/** Summary of the health of the server. */
export interface ServerHealth {
cpu: SeverityLevel;
mem: SeverityLevel;
disks: Record<string, SeverityLevel>;
cpu: ServerHealthState;
mem: ServerHealthState;
disks: Record<string, ServerHealthState>;
}
export enum AwsVolumeType {

View File

@@ -7,12 +7,12 @@ asynckit@^0.4.0:
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
axios@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f"
integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==
axios@^1.7.7:
version "1.7.7"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f"
integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==
dependencies:
follow-redirects "^1.15.0"
follow-redirects "^1.15.6"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
@@ -28,10 +28,10 @@ delayed-stream@~1.0.0:
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
follow-redirects@^1.15.0:
version "1.15.2"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
follow-redirects@^1.15.6:
version "1.15.9"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
form-data@^4.0.0:
version "4.0.0"
@@ -59,7 +59,7 @@ proxy-from-env@^1.1.0:
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
typescript@^5.1.6:
version "5.1.6"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274"
integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==
typescript@^5.6.3:
version "5.6.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.3.tgz#5f3449e31c9d94febb17de03cc081dd56d81db5b"
integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==

View File

@@ -43,10 +43,4 @@ This works by:
Just like all other resources with Environments (Deployments, Repos, Builds),
Stack Environments support **Variable and Secret interpolation**. Define global variables
in the UI and share the values across environments.
:::
## Using bind mounts
Repo-based stacks must delete the stack folder before it is able to reclone for the latest repo contents.
Because of this, users should **avoid using relative file paths that are placed inside the repo directory**.
Or better yet, make things simple and use **absolute file paths** or **docker volumes** instead.
:::

View File

@@ -12,7 +12,7 @@ const FeatureList: FeatureItem[] = [
title: "Automated builds 🛠️",
description: (
<>
Build auto versioned docker images from github repos, trigger builds on
Build auto versioned docker images from git repos, trigger builds on
git push
</>
),

View File

@@ -8,6 +8,23 @@ use axum::{routing::post, Json, Router};
use komodo_client::entities::alert::{Alert, SeverityLevel};
use serde::Deserialize;
/// Entrypoint for handling each incoming alert.
async fn handle_incoming_alert(Json(alert): Json<Alert>) {
if alert.resolved {
info!("Alert Resolved!: {alert:?}");
return;
}
match alert.level {
SeverityLevel::Ok => info!("{alert:?}"),
SeverityLevel::Warning => warn!("{alert:?}"),
SeverityLevel::Critical => error!("{alert:?}"),
}
}
/// ========================
/// Http server boilerplate.
/// ========================
#[derive(Deserialize)]
struct Env {
#[serde(default = "default_port")]
@@ -30,20 +47,7 @@ async fn app() -> anyhow::Result<()> {
info!("v {} | {socket_addr}", env!("CARGO_PKG_VERSION"));
let app = Router::new().route(
"/",
post(|Json(alert): Json<Alert>| async move {
if alert.resolved {
info!("Alert Resolved!: {alert:?}");
return;
}
match alert.level {
SeverityLevel::Ok => info!("{alert:?}"),
SeverityLevel::Warning => warn!("{alert:?}"),
SeverityLevel::Critical => error!("{alert:?}"),
}
}),
);
let app = Router::new().route("/", post(handle_incoming_alert));
let listener = tokio::net::TcpListener::bind(socket_addr)
.await

View File

@@ -1,7 +1,16 @@
#[macro_use]
extern crate tracing;
use komodo_client::KomodoClient;
use komodo_client::{ws::UpdateWsMessage, KomodoClient};
/// Entrypoint for handling each incoming update.
async fn handle_incoming_update(update: UpdateWsMessage) {
info!("{update:?}");
}
/// ========================
/// Ws Listener boilerplate.
/// ========================
async fn app() -> anyhow::Result<()> {
logger::init(&Default::default())?;
@@ -13,12 +22,14 @@ async fn app() -> anyhow::Result<()> {
let (mut rx, _) = komodo.subscribe_to_updates(1000, 5)?;
loop {
let msg = rx.recv().await;
if let Err(e) = msg {
error!("🚨 recv error | {e:?}");
break;
}
info!("{msg:?}");
let update = match rx.recv().await {
Ok(msg) => msg,
Err(e) => {
error!("🚨 recv error | {e:?}");
break;
}
};
handle_incoming_update(update).await
}
Ok(())

View File

@@ -9,7 +9,7 @@ ARG VITE_KOMODO_HOST
ENV VITE_KOMODO_HOST ${VITE_KOMODO_HOST}
RUN cd client && yarn && yarn build && yarn link
RUN cd frontend && yarn link @komodo/client && yarn && yarn build
RUN cd frontend && yarn link komodo_client && yarn && yarn build
ENV PORT 4174

View File

@@ -4,14 +4,14 @@ Komodo JS stack uses Yarn + Vite + React + Tailwind + shadcn/ui
## Setup Dev Environment
The frontend depends on the local package `@komodo/client` located at `/client/core/ts`.
The frontend depends on the local package `komodo_client` located at `/client/core/ts`.
This must first be built and prepared for yarn link.
The following command should setup everything up (run with /frontend as working directory):
```sh
cd ../client/core/ts && yarn && yarn build && yarn link && \
cd ../../../frontend && yarn link @komodo/client && yarn
cd ../../../frontend && yarn link komodo_client && yarn
```
You can make a new file `.env.development` (gitignored) which holds:

View File

@@ -17,7 +17,7 @@ import {
text_color_class_by_intention,
} from "@lib/color";
import { MonacoEditor } from "@components/monaco";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
export const AlertDetailsDialog = ({ id }: { id: string }) => {
const [open, set] = useState(false);

View File

@@ -1,7 +1,7 @@
import { Section } from "@components/layouts";
import { alert_level_intention } from "@lib/color";
import { useRead, atomWithStorage } from "@lib/hooks";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { Button } from "@ui/button";
import { useAtom } from "jotai";
import { AlertTriangle } from "lucide-react";

View File

@@ -1,4 +1,4 @@
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { DataTable } from "@ui/data-table";
import { AlertLevel } from ".";
import { AlertDetailsDialog } from "./details";

View File

@@ -11,7 +11,7 @@ import { AlertLevel } from ".";
import { ResourceLink } from "@components/resources/common";
import { UsableResource } from "@types";
import { Dialog } from "@ui/dialog";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { useState } from "react";
import { AlertDetailsDialogContent } from "./details";

View File

@@ -1,6 +1,6 @@
import { SecretSelector } from "@components/config/util";
import { useRead } from "@lib/hooks";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { useToast } from "@ui/use-toast";
export const SecretsSearch = ({

View File

@@ -5,7 +5,7 @@ import {
} from "@components/config/util";
import { Section } from "@components/layouts";
import { MonacoLanguage } from "@components/monaco";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { cn } from "@lib/utils";
import { Button } from "@ui/button";
import {

View File

@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useCtrlKeyListener, useRead } from "@lib/hooks";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import {
Select,
SelectTrigger,
@@ -594,7 +594,7 @@ export function ConfirmUpdate<T>({
file_contents_language,
}: ConfirmUpdateProps<T>) {
const [open, set] = useState(false);
useCtrlKeyListener("s", () => {
useCtrlKeyListener("Enter", () => {
if (open) {
onConfirm();
} else {

View File

@@ -1,5 +1,5 @@
import { useRead } from "@lib/hooks";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { Button } from "@ui/button";
import {
Dialog,

View File

@@ -1,5 +1,5 @@
import { CopyButton } from "@components/util";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { DataTable } from "@ui/data-table";
import { Input } from "@ui/input";
import { ReactNode } from "react";

View File

@@ -10,7 +10,7 @@ import {
DialogTitle,
DialogTrigger,
} from "@ui/dialog";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { ResourceComponents } from "./resources";
import { Card, CardHeader, CardTitle, CardContent, CardFooter } from "@ui/card";
import { ResourceTags } from "./tags";

View File

@@ -1,5 +1,5 @@
import { logToHtml } from "@lib/utils";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { Button } from "@ui/button";
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@ui/select";
import { ChevronDown } from "lucide-react";

View File

@@ -31,17 +31,34 @@ export const MonacoEditor = ({
const [editor, setEditor] =
useState<monaco.editor.IStandaloneCodeEditor | null>(null);
useEffect(() => {
if (!editor) return;
let node = editor.getDomNode();
if (!node) return;
const callback = (e: any) => {
if (e.key === "Escape") {
(document.activeElement as any)?.blur?.();
}
};
node.addEventListener("keydown", callback);
return () => node.removeEventListener("keydown", callback);
}, [editor]);
const line_count = value?.split(/\r\n|\r|\n/).length ?? 0;
useEffect(() => {
if (!editor) return;
const contentHeight = line_count * 18 + 30;
const node = editor.getContainerDomNode();
node.style.height = `${Math.max(
const containerNode = editor.getContainerDomNode();
containerNode.style.height = `${Math.max(
Math.ceil(contentHeight),
MIN_EDITOR_HEIGHT
)}px`;
// node.style.height = `${Math.max(
// containerNode.style.height = `${Math.max(
// Math.min(Math.ceil(contentHeight), MAX_EDITOR_HEIGHT),
// MIN_EDITOR_HEIGHT
// )}px`;

View File

@@ -1,5 +1,5 @@
import { ConfigItem } from "@components/config/util";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { Badge } from "@ui/badge";
import {
Select,

View File

@@ -1,6 +1,6 @@
import { ConfigItem } from "@components/config/util";
import { MonacoEditor } from "@components/monaco";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import {
Select,
SelectContent,

View File

@@ -1,6 +1,6 @@
import { Config } from "@components/config";
import { useRead, useWrite } from "@lib/hooks";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { useState } from "react";
import { EndpointConfig } from "./endpoint";
import { AlertTypeConfig } from "./alert_types";

View File

@@ -3,7 +3,7 @@ import { ResourceComponents } from "@components/resources";
import { ResourceLink } from "@components/resources/common";
import { useRead } from "@lib/hooks";
import { resource_name } from "@lib/utils";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { UsableResource } from "@types";
import { Button } from "@ui/button";
import { DataTable, SortableHeader } from "@ui/data-table";

View File

@@ -6,7 +6,7 @@ import { Card, CardDescription, CardHeader, CardTitle } from "@ui/card";
import { AlerterConfig } from "./config";
import { DeleteResource, NewResource } from "../common";
import { AlerterTable } from "./table";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { ResourcePageHeader } from "@components/util";
const useAlerter = (id?: string) =>

View File

@@ -1,7 +1,7 @@
import { DataTable, SortableHeader } from "@ui/data-table";
import { ResourceLink } from "../common";
import { TableTags } from "@components/tags";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
export const AlerterTable = ({
alerters,

View File

@@ -1,6 +1,6 @@
import { ConfirmButton } from "@components/util";
import { useExecute, useRead } from "@lib/hooks";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { Ban, Hammer, Loader2 } from "lucide-react";
import { useBuilder } from "../builder";

View File

@@ -10,7 +10,7 @@ import {
SystemCommand,
} from "@components/config/util";
import { useInvalidate, useRead, useWrite } from "@lib/hooks";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { Button } from "@ui/button";
import { Ban, CirclePlus, PlusCircle } from "lucide-react";
import { ReactNode, useState } from "react";
@@ -190,98 +190,6 @@ export const BuildConfig = ({
),
},
},
{
label: "Webhook",
description:
"Configure your repo provider to send webhooks to Komodo",
components: {
["Guard" as any]: () => {
if (update.branch ?? config.branch) {
return null;
}
return (
<ConfigItem label="Configure Branch">
<div>Must configure Branch before webhooks will work.</div>
</ConfigItem>
);
},
["build" as any]: () => (
<ConfigItem label="Webhook Url">
<CopyGithubWebhook path={`/build/${id}`} />
</ConfigItem>
),
webhook_enabled: webhook !== undefined && !webhook.managed,
webhook_secret: {
description:
"Provide a custom webhook secret for this resource, or use the global default.",
placeholder: "Input custom secret",
},
["managed" as any]: () => {
const inv = useInvalidate();
const { toast } = useToast();
const { mutate: createWebhook, isPending: createPending } =
useWrite("CreateBuildWebhook", {
onSuccess: () => {
toast({ title: "Webhook Created" });
inv(["GetBuildWebhookEnabled", { build: id }]);
},
});
const { mutate: deleteWebhook, isPending: deletePending } =
useWrite("DeleteBuildWebhook", {
onSuccess: () => {
toast({ title: "Webhook Deleted" });
inv(["GetBuildWebhookEnabled", { build: id }]);
},
});
if (!webhook || !webhook.managed) return;
return (
<ConfigItem label="Manage Webhook">
{webhook.enabled && (
<div className="flex items-center gap-4 flex-wrap">
<div className="flex items-center gap-2">
Incoming webhook is{" "}
<div
className={text_color_class_by_intention("Good")}
>
ENABLED
</div>
</div>
<ConfirmButton
title="Disable"
icon={<Ban className="w-4 h-4" />}
variant="destructive"
onClick={() => deleteWebhook({ build: id })}
loading={deletePending}
disabled={disabled || deletePending}
/>
</div>
)}
{!webhook.enabled && (
<div className="flex items-center gap-4 flex-wrap">
<div className="flex items-center gap-2">
Incoming webhook is{" "}
<div
className={text_color_class_by_intention(
"Critical"
)}
>
DISABLED
</div>
</div>
<ConfirmButton
title="Enable Build"
icon={<CirclePlus className="w-4 h-4" />}
onClick={() => createWebhook({ build: id })}
loading={createPending}
disabled={disabled || createPending}
/>
</div>
)}
</ConfigItem>
);
},
},
},
{
label: "Links",
description: "Add quick links in the resource header",
@@ -447,6 +355,98 @@ export const BuildConfig = ({
),
},
},
{
label: "Webhook",
description:
"Configure your repo provider to send webhooks to Komodo",
components: {
["Guard" as any]: () => {
if (update.branch ?? config.branch) {
return null;
}
return (
<ConfigItem label="Configure Branch">
<div>Must configure Branch before webhooks will work.</div>
</ConfigItem>
);
},
["build" as any]: () => (
<ConfigItem label="Webhook Url">
<CopyGithubWebhook path={`/build/${id}`} />
</ConfigItem>
),
webhook_enabled: webhook !== undefined && !webhook.managed,
webhook_secret: {
description:
"Provide a custom webhook secret for this resource, or use the global default.",
placeholder: "Input custom secret",
},
["managed" as any]: () => {
const inv = useInvalidate();
const { toast } = useToast();
const { mutate: createWebhook, isPending: createPending } =
useWrite("CreateBuildWebhook", {
onSuccess: () => {
toast({ title: "Webhook Created" });
inv(["GetBuildWebhookEnabled", { build: id }]);
},
});
const { mutate: deleteWebhook, isPending: deletePending } =
useWrite("DeleteBuildWebhook", {
onSuccess: () => {
toast({ title: "Webhook Deleted" });
inv(["GetBuildWebhookEnabled", { build: id }]);
},
});
if (!webhook || !webhook.managed) return;
return (
<ConfigItem label="Manage Webhook">
{webhook.enabled && (
<div className="flex items-center gap-4 flex-wrap">
<div className="flex items-center gap-2">
Incoming webhook is{" "}
<div
className={text_color_class_by_intention("Good")}
>
ENABLED
</div>
</div>
<ConfirmButton
title="Disable"
icon={<Ban className="w-4 h-4" />}
variant="destructive"
onClick={() => deleteWebhook({ build: id })}
loading={deletePending}
disabled={disabled || deletePending}
/>
</div>
)}
{!webhook.enabled && (
<div className="flex items-center gap-4 flex-wrap">
<div className="flex items-center gap-2">
Incoming webhook is{" "}
<div
className={text_color_class_by_intention(
"Critical"
)}
>
DISABLED
</div>
</div>
<ConfirmButton
title="Enable Build"
icon={<CirclePlus className="w-4 h-4" />}
onClick={() => createWebhook({ build: id })}
loading={createPending}
disabled={disabled || createPending}
/>
</div>
)}
</ConfigItem>
);
},
},
},
],
}}
/>

View File

@@ -16,7 +16,7 @@ import { cn } from "@lib/utils";
import { useState } from "react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ui/tabs";
import { ResourceComponents } from "..";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { DashboardPieChart } from "@pages/home/dashboard";
import { ResourcePageHeader, StatusBadge } from "@components/util";
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@ui/hover-card";

View File

@@ -3,7 +3,7 @@ import { DataTable, SortableHeader } from "@ui/data-table";
import { fmt_version } from "@lib/formatting";
import { ResourceLink } from "../common";
import { BuildComponents } from ".";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
export const BuildTable = ({ builds }: { builds: Types.BuildListItem[] }) => {
return (

View File

@@ -1,7 +1,7 @@
import { Config } from "@components/config";
import { ConfigItem, ConfigList } from "@components/config/util";
import { useRead, useWrite } from "@lib/hooks";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { useState } from "react";
import { ResourceLink, ResourceSelector } from "../common";
import { Button } from "@ui/button";

View File

@@ -1,6 +1,6 @@
import { NewLayout } from "@components/layouts";
import { useRead, useUser, useWrite } from "@lib/hooks";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { RequiredResourceComponents } from "@types";
import { Card, CardDescription, CardHeader, CardTitle } from "@ui/card";
import { Input } from "@ui/input";

View File

@@ -2,7 +2,7 @@ import { DataTable, SortableHeader } from "@ui/data-table";
import { ResourceLink } from "../common";
import { TableTags } from "@components/tags";
import { BuilderInstanceType } from ".";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
export const BuilderTable = ({
builders,

View File

@@ -31,7 +31,7 @@ import { ResourceComponents } from ".";
import { Input } from "@ui/input";
import { useToast } from "@ui/use-toast";
import { NewLayout } from "@components/layouts";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { filterBySplit, usableResourcePath } from "@lib/utils";
export const ResourceDescription = ({

View File

@@ -12,7 +12,7 @@ import { useExecute, useInvalidate, useRead, useWrite } from "@lib/hooks";
import { Input } from "@ui/input";
import { useToast } from "@ui/use-toast";
import { useEffect, useState } from "react";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import {
Select,
SelectContent,
@@ -203,7 +203,7 @@ export const StartStopDeployment = ({ id }: DeploymentId) => {
/>
);
}
if (state === Types.DeploymentState.Running) {
if (state !== Types.DeploymentState.NotDeployed) {
return <StopDeployment id={id} />;
}
};

View File

@@ -3,7 +3,7 @@ import { ResourceSelector } from "@components/resources/common";
import { fmt_date, fmt_version } from "@lib/formatting";
import { useRead } from "@lib/hooks";
import { filterBySplit } from "@lib/utils";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { CaretSortIcon } from "@radix-ui/react-icons";
import {
Command,

View File

@@ -1,5 +1,5 @@
import { ConfigItem } from "@components/config/util";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import {
Select,
SelectContent,

View File

@@ -1,5 +1,5 @@
import { ConfigItem } from "@components/config/util";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { Input } from "@ui/input";
import {
Select,

View File

@@ -1,5 +1,5 @@
import { useRead, useWrite } from "@lib/hooks";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { ReactNode, useState } from "react";
import {
AccountSelectorConfig,

View File

@@ -1,5 +1,5 @@
import { useLocalStorage, useRead } from "@lib/hooks";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { RequiredResourceComponents } from "@types";
import { HardDrive, Rocket, Server } from "lucide-react";
import { cn } from "@lib/utils";

View File

@@ -1,6 +1,6 @@
import { Section } from "@components/layouts";
import { useLocalStorage, useRead } from "@lib/hooks";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { Button } from "@ui/button";
import { RefreshCw, X, AlertOctagon } from "lucide-react";
import { ReactNode, useEffect, useState } from "react";

View File

@@ -1,5 +1,5 @@
import { TableTags } from "@components/tags";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { DataTable, SortableHeader } from "@ui/data-table";
import { useRead } from "@lib/hooks";
import { ResourceLink } from "../common";

View File

@@ -1,7 +1,7 @@
import { ConfigItem } from "@components/config/util";
import { Section } from "@components/layouts";
import { useRead, useWrite } from "@lib/hooks";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { Card, CardHeader } from "@ui/card";
import { Input } from "@ui/input";
import { useEffect, useState } from "react";

View File

@@ -14,7 +14,7 @@ import {
stroke_color_class_by_intention,
} from "@lib/color";
import { cn } from "@lib/utils";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { DashboardPieChart } from "@pages/home/dashboard";
const useProcedure = (id?: string) =>

View File

@@ -2,7 +2,7 @@ import { DataTable, SortableHeader } from "@ui/data-table";
import { TableTags } from "@components/tags";
import { ResourceLink } from "../common";
import { ProcedureComponents } from ".";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
export const ProcedureTable = ({
procedures,

View File

@@ -8,7 +8,7 @@ import {
Loader2,
} from "lucide-react";
import { useRepo } from ".";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { useBuilder } from "../builder";
export const CloneRepo = ({ id }: { id: string }) => {

View File

@@ -7,7 +7,7 @@ import {
SystemCommand,
} from "@components/config/util";
import { useInvalidate, useRead, useWrite } from "@lib/hooks";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { useState } from "react";
import { CopyGithubWebhook, ResourceLink, ResourceSelector } from "../common";
import { useToast } from "@ui/use-toast";

View File

@@ -18,7 +18,7 @@ import {
import { cn } from "@lib/utils";
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@ui/hover-card";
import { useServer } from "../server";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { DashboardPieChart } from "@pages/home/dashboard";
import { ResourcePageHeader, StatusBadge } from "@components/util";
import { Badge } from "@ui/badge";

View File

@@ -2,7 +2,7 @@ import { DataTable, SortableHeader } from "@ui/data-table";
import { ResourceLink } from "../common";
import { TableTags } from "@components/tags";
import { RepoComponents } from ".";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
export const RepoTable = ({ repos }: { repos: Types.RepoListItem[] }) => {
return (

View File

@@ -6,7 +6,7 @@ import {
ProviderSelectorConfig,
} from "@components/config/util";
import { useInvalidate, useLocalStorage, useRead, useWrite } from "@lib/hooks";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { ReactNode, useState } from "react";
import { CopyGithubWebhook } from "../common";
import { useToast } from "@ui/use-toast";
@@ -148,6 +148,22 @@ export const ResourceSyncConfig = ({
},
};
const general_common: ConfigComponent<Types.ResourceSyncConfig> = {
label: "General",
components: {
delete: !managed && {
label: "Delete Unmatched Resources",
description:
"Executions will delete any resources not found in the resource files. Only use this when using one sync for everything.",
},
managed: {
label: "Managed",
description:
"Enabled managed mode / the 'Commit' button. Commit is the 'reverse' of Execute, and will update the sync file with your configs updated in the UI.",
},
},
};
const match_tags: ConfigComponent<Types.ResourceSyncConfig> = {
label: "Match Tags",
description: "Only sync resources matching all of these tags.",
@@ -178,16 +194,7 @@ export const ResourceSyncConfig = ({
placeholder="Input resource path"
/>
),
delete: !managed && {
label: "Delete Unmatched Resources",
description:
"Executions will delete any resources not found in the resource files. Only use this when using one sync for everything.",
},
managed: {
label: "Managed",
description:
"Enabled managed mode / the 'Commit' button. Commit is the 'reverse' of Execute, and will update the sync file with your configs updated in the UI.",
},
...general_common.components,
},
},
match_tags,
@@ -264,16 +271,7 @@ export const ResourceSyncConfig = ({
placeholder="Input resource path"
/>
),
delete: !managed && {
label: "Delete Unmatched Resources",
description:
"Executions will delete any resources not found in the resource files. Only use this when using one sync for everything.",
},
managed: {
label: "Managed",
description:
"Enabled managed mode / the 'Commit' button. Commit is the 'reverse' of Execute, and will update the sync file with your configs updated in the UI.",
},
...general_common.components,
},
},
match_tags,
@@ -471,21 +469,7 @@ export const ResourceSyncConfig = ({
},
},
},
{
label: "General",
components: {
delete: !managed && {
label: "Delete Unmatched Resources",
description:
"Executions will delete any resources not found in the resource files. Only use this when using one sync for everything.",
},
managed: {
label: "Managed",
description:
"Enabled managed mode / the 'Commit' button. Commit is the 'reverse' of Execute, and will update the sync file with your configs updated in the UI.",
},
},
},
general_common,
match_tags,
],
};

View File

@@ -4,7 +4,7 @@ import { Card } from "@ui/card";
import { Clock, FolderSync } from "lucide-react";
import { DeleteResource, NewResource } from "../common";
import { ResourceSyncTable } from "./table";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { CommitSync, ExecuteSync, RefreshSync } from "./actions";
import {
border_color_class_by_intention,

View File

@@ -1,7 +1,7 @@
import { DataTable, SortableHeader } from "@ui/data-table";
import { ResourceLink } from "../common";
import { TableTags } from "@components/tags";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { ResourceSyncComponents } from ".";
export const ResourceSyncTable = ({

View File

@@ -15,7 +15,7 @@ import {
} from "@ui/dialog";
import { useWebsocketMessages } from "@lib/socket";
import { useNavigate } from "react-router-dom";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
export const LaunchServer = ({ id }: { id: string }) => {
const nav = useNavigate();

View File

@@ -2,7 +2,7 @@ import { Config } from "@components/config";
import { ConfigList } from "@components/config/util";
import { useRead, useWrite } from "@lib/hooks";
import { cn } from "@lib/utils";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { Button } from "@ui/button";
import { Card } from "@ui/card";
import {

View File

@@ -2,7 +2,7 @@ import { Config } from "@components/config";
import { ConfigItem, ConfigList } from "@components/config/util";
import { useRead, useWrite } from "@lib/hooks";
import { cn, filterBySplit } from "@lib/utils";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { Button } from "@ui/button";
import { Card } from "@ui/card";
import {

View File

@@ -1,7 +1,7 @@
import { useRead } from "@lib/hooks";
import { AwsServerTemplateConfig } from "./aws";
import { HetznerServerTemplateConfig } from "./hetzner";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
export const ServerTemplateConfig = ({ id }: { id: string }) => {
const config = useRead("GetServerTemplate", { server_template: id }).data

View File

@@ -6,7 +6,7 @@ import { ServerTemplateConfig } from "./config";
import { Link, useNavigate } from "react-router-dom";
import { Card, CardDescription, CardHeader, CardTitle } from "@ui/card";
import { useState } from "react";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
import { NewLayout } from "@components/layouts";
import { Input } from "@ui/input";
import {

View File

@@ -1,7 +1,7 @@
import { DataTable, SortableHeader } from "@ui/data-table";
import { ResourceLink } from "../common";
import { TableTags } from "@components/tags";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
export const ServerTemplateTable = ({
serverTemplates,

View File

@@ -6,7 +6,7 @@ import { Pen, Scissors } from "lucide-react";
import { useState } from "react";
import { useServer } from ".";
import { has_minimum_permissions } from "@lib/utils";
import { Types } from "@komodo/client";
import { Types } from "komodo_client";
export const RenameServer = ({ id }: { id: string }) => {
const invalidate = useInvalidate();

Some files were not shown because too many files have changed in this diff Show More