Compare commits

..
Author SHA1 Message Date
EtienneandGitHub f45205011e Fix compose.env (#415)
Flip "stacks" and "repos" paths
2025-04-15 12:11:48 -07:00
5b211fb8f0 1.17.1 (#383)
* interpolate into slack / discord url

* fix js client docs

* js client should be type: module

* click table tags to toggle tag filter

* git token helper early return when empty provider

* reorder Stack fields

* action support interpolation doc

* Fix for the {account} login fails when the account name contains '$' (#385)

* GetDeploymentsSummary (#386)

* added GetDockerContainersSummary endpoint in rust api

* typescript stuff

* more autogenned typescript stuff

* fixed comments to be in line with actual behaviour

* fixed ReadResponse for GetDockerContainersSummary

* I64 -> u32 for response types

* more accurate error context

* backend for build files on host / ui defined

* core api supports non repo based build

* Ntfy as Alerter (#404)

* add ntfy alerter

* add ntfy alerter

* add ntfy alerter

---------

Co-authored-by: GFXSpeed <github@schafhaupt.com>

* Improve docs around running periphery in a container (#402)

* Add ports that need to be exposed if periphery is remote from core

* Spelling: overide -> override

* Add info about using a custom config file when running periphery in docker

* clean up ntfy alerter

* clean up ResourceSyncConfig

* update build cache after create / update

* refresh stack cache log

* Build UI Defined / file on host frontend

* update clap + rustls

* don't cleanup build files

* clean up dockerfile full path

* update BuildListItemInfo + UI table

* add Other Resources page

* add 5 second ws reconnection timeout

* Make listener address configurable & Add support for IPv6 (#405)

* Make listener address configurable

* Make listener address configurable for periphery

* rename listener_address -> bind_ip

---------

Co-authored-by: Jacky Fong <hello@huzky.dev>
Co-authored-by: Alex Shore <kntrllr@gmail.com>
Co-authored-by: Niklas <108990063+GFXSpeed@users.noreply.github.com>
Co-authored-by: GFXSpeed <github@schafhaupt.com>
Co-authored-by: theRAAPster <theraapster@gmail.com>
Co-authored-by: Daniel Vos <45820840+vosdev@users.noreply.github.com>
2025-04-14 14:48:12 -07:00
Justin GoetteandGitHub 3f04614303 docs: Typo (#392)
FerretDB vs. FerritDB
2025-04-02 00:05:29 -07:00
mbecker20 4c3210f70a update dashboard screenshots with colored tags 2025-03-23 23:17:33 -07:00
mbecker20 2c37ac26a5 ts client 1.17.0 2025-03-23 17:42:47 -07:00
db1cf786ac 1.17.0 (#248)
* resolver v3

add new ec2 instance types

clean up testing config

document the libraries a bit

clean up main

update sysinfo and otel

update client resolver 3.0

resolver v3 prog

clean up gitignore

implement periphery resolver v3

clean up

core read api v3

more prog

execute api

missing apis

compiling

1.16.13

work on more granular traits

prog on crud

* fmt

* format

* resource2 not really a benefit

* axum to 0.8

* bump aws deps

* just make it 1.17.0

* clean up cors

* the komodo env file should be highest priority over additional files

* add entities / message for test alerter

* test alert implementation

* rust 1.84.0

* axum update :param to {param} syntax

* fix last axum updates

* Add test alerter button

* higher quality / colored icons

* komodo-logo

* simplify network stats

* rename Test Alerter button

* escape incoming sync backslashes (BREAKING)

* clean up rust client websocket subscription

* finish oidc comment

* show update available stack table

* update available deployment table

* feature: use the repo path instead of name in GetLatestCommit (#282)

* Update repo path handling in commit fetching

- Changed `name` to `path` for repository identification.
- Updated cache update function to use the new path field.
- Improved error message for non-directory repo paths.

* feat: use optional name and path in GetLatestCommit

* review: don't use optional for name

* review: use helper

* review: remove redundant to_string()

* 1.17.0-dev

* feature: add post_deploy command (#288)

* feature: add post_deploy command

* review: do not run post_deploy if deploy failed

* feature: interpolate secrets in custom alerter (#289)

* feature: interpolate secrets in custom alerter

* fix rust warning

* review: sanitize errors

* review: sanitize error message

* Remove .git from remote_url (#299)

Remove .git from remote_url

Co-authored-by: Deon Marshall <dmarshall@ccp.com.au>

* mbecker20 -> moghtech

* remove example from cargo toml workspace

* dev-1

* fix login screen logo

* more legible favicon

* fix new compose images

* docs new organization

* typescript subscribe_to_update_websocket

* add donate button docsite

* add config save button in desktop sidebar navigator

* add save button to config bottom

* feature: allow docker image text to overflow in table (#301)

* feature: allow docker image text to overflow in table

* review: use break-words

* wip: revert line break in css file

* feature: update devcontainer node release

* improve First Login docs

* FIx PullStack re #302 and record docker compose config on stack deploy

* requery alerts more often

* improve update indicator style and also put on home screen

* Add all services stack log

* 1.17.0-dev-2

* fix api name chnage

* choose which stack services to include in logs

* feature: improve tables quick actions on mobile (#312)

* feature: improve tables quick actions on mobile

* review: fix gap4

* review: use flex-wrap

* improve pull to git init on existing folder without .git

* Fix unclear ComposePull log re #244

* use komodo_client.subscribe_to_update_websocket, and click indicator to reconnect

* dev-3

* ServerTemplate description

* improve WriteComposeContentsToHost instrument fields

* give server stat charts labels

* filters wrap

* show provider usernames from config file

* Stack: Fix git repo new compose file initialization

* init sync file new repo

* set branch on git init folder

* ResourceSync: pending view toggle between "Execute" vs "Commit" sync direction

* Improve resource sync Execute / Pending view selector

* standardize running commands with interpolation / output sanitizations

* fix all clippy lints

* fix rand

* lock certain users username / password, prevent demo creds from being changed.

* revert to login screen whenever the call to check login fails

* ResourceSync state resolution refinement

* make sure parent directories exist whenever writing files

* don't prune images if server not enabled

* update most deps

* update openidconnect dependency, and use reqwest rustls-tls-native-roots

* dev-4

* resource sync only add escaping on toml between the """

* Stacks executions take list of services -- Auto update only redeploys services with update

* auto update all service deploy option

* dev-5 fix the stack service executions

* clean up service_args

* rust 1.85

* store sync edits on localstorage

* stack edits on localstorage and show last deployed config

* add yarn install to runfile

* Fix actions when core on https

* add update_available query parameter to filter for only stacks /deployments with available update

* rust 2024 and fmt

* rename test.compose.yaml to dev.compose.yaml, and update runfile

* update .devcontainer / dev docs for updated runfile

* use png in topbar logo, svg quality sometimes bad

* OIDC: Support PKCE auth (secret optional)

* update docs on OIDC and client secret

* cycle the oidc client on interval to ensure up to date JWKs

* add KOMODO_LOCK_LOGIN_CREDENTIALS_FOR in config doc

* update deps

* resource sync toggle resource / variable / user group inclusion independantly

* use jsonwebtoken

* improve variable value table overflow

* colored tags

* fix sync summary count ok

* default new tag colors to grey

* soften tag opacity a bit

* Update config.tsx (#358)

* isolate stacks / deployments with pending updates

* update some deps

* use Tooltip component instead of HoverCard for mobile compatibility

* batch Build builds

* link to typescript client in the intro

* add link to main docs from client docs

* doc tweaks

* use moghtech/komodo-core and moghtech/komodo-periphery as images

* remove unnecessary explicit network

* periphery.compose.yaml

* clean up periphery compose

* add link to config

* update periphery container compose config

* rust 1.85.1

* update sync docs

* 1.17.0

---------

Co-authored-by: unsync <1211591+unsync@users.noreply.github.com>
Co-authored-by: Deon Marshall <dmarshall@ccp.com.au>
Co-authored-by: komodo <komodo@komo.do>
Co-authored-by: wlatic <jamesoh@gmail.com>
2025-03-23 16:47:06 -07:00
102 changed files with 12414 additions and 2154 deletions
Generated
+696 -629
View File
File diff suppressed because it is too large Load Diff
+19 -19
View File
@@ -8,7 +8,7 @@ members = [
]
[workspace.package]
version = "1.17.0-dev-7"
version = "1.17.1"
edition = "2024"
authors = ["mbecker20 <becker.maxh@gmail.com>"]
license = "GPL-3.0-or-later"
@@ -30,7 +30,7 @@ git = { path = "lib/git" }
# MOGH
run_command = { version = "0.0.6", features = ["async_tokio"] }
serror = { version = "0.5.0", default-features = false }
slack = { version = "0.3.0", package = "slack_client_rs", default-features = false, features = ["rustls"] }
slack = { version = "0.4.0", package = "slack_client_rs", default-features = false, features = ["rustls"] }
derive_default_builder = "0.1.8"
derive_empty_traits = "0.1.0"
merge_config_files = "0.1.5"
@@ -44,9 +44,9 @@ mungos = "3.2.0"
svi = "1.0.1"
# ASYNC
reqwest = { version = "0.12.12", default-features = false, features = ["json", "rustls-tls-native-roots"] }
tokio = { version = "1.44.0", features = ["full"] }
tokio-util = "0.7.13"
reqwest = { version = "0.12.15", default-features = false, features = ["json", "rustls-tls-native-roots"] }
tokio = { version = "1.44.1", features = ["full"] }
tokio-util = "0.7.14"
futures = "0.3.31"
futures-util = "0.3.31"
arc-swap = "1.7.1"
@@ -54,7 +54,7 @@ arc-swap = "1.7.1"
# SERVER
axum-extra = { version = "0.10.0", features = ["typed-header"] }
tower-http = { version = "0.6.2", features = ["fs", "cors"] }
axum-server = { version = "0.7.1", features = ["tls-rustls"] }
axum-server = { version = "0.7.2", features = ["tls-rustls"] }
axum = { version = "0.8.1", features = ["ws", "json", "macros"] }
tokio-tungstenite = "0.26.2"
@@ -71,28 +71,28 @@ anyhow = "1.0.97"
thiserror = "2.0.12"
# LOGGING
opentelemetry-otlp = { version = "0.28.0", features = ["tls-roots", "reqwest-rustls"] }
opentelemetry_sdk = { version = "0.28.0", features = ["rt-tokio"] }
opentelemetry-otlp = { version = "0.29.0", features = ["tls-roots", "reqwest-rustls"] }
opentelemetry_sdk = { version = "0.29.0", features = ["rt-tokio"] }
tracing-subscriber = { version = "0.3.19", features = ["json"] }
opentelemetry-semantic-conventions = "0.28.0"
tracing-opentelemetry = "0.29.0"
opentelemetry = "0.28.0"
opentelemetry-semantic-conventions = "0.29.0"
tracing-opentelemetry = "0.30.0"
opentelemetry = "0.29.0"
tracing = "0.1.41"
# CONFIG
clap = { version = "4.5.32", features = ["derive"] }
clap = { version = "4.5.36", features = ["derive"] }
dotenvy = "0.15.7"
envy = "0.4.2"
# CRYPTO / AUTH
uuid = { version = "1.15.1", features = ["v4", "fast-rng", "serde"] }
uuid = { version = "1.16.0", features = ["v4", "fast-rng", "serde"] }
jsonwebtoken = { version = "9.3.1", default-features = false }
openidconnect = "4.0.0"
urlencoding = "2.1.3"
nom_pem = "4.0.0"
bcrypt = "0.17.0"
base64 = "0.22.1"
rustls = "0.23.23"
rustls = "0.23.26"
hmac = "0.12.1"
sha2 = "0.10.8"
rand = "0.9.0"
@@ -100,19 +100,19 @@ hex = "0.4.3"
# SYSTEM
bollard = "0.18.1"
sysinfo = "0.33.1"
sysinfo = "0.34.2"
# CLOUD
aws-config = "1.6.0"
aws-sdk-ec2 = "1.117.0"
aws-config = "1.6.1"
aws-sdk-ec2 = "1.121.1"
aws-credential-types = "1.2.2"
# MISC
derive_builder = "0.20.2"
typeshare = "1.0.4"
octorust = "0.9.0"
octorust = "0.10.0"
dashmap = "6.1.0"
wildcard = "0.3.0"
colored = "3.0.0"
regex = "1.11.1"
bson = "2.13.0"
bson = "2.14.0"
+1 -1
View File
@@ -1,7 +1,7 @@
## Builds the Komodo Core and Periphery binaries
## for a specific architecture.
FROM rust:1.85.0-bullseye AS builder
FROM rust:1.85.1-bullseye AS builder
WORKDIR /builder
COPY Cargo.toml Cargo.lock ./
+1 -1
View File
@@ -1,7 +1,7 @@
## All in one, multi stage compile + runtime Docker build for your architecture.
# Build Core
FROM rust:1.85.0-bullseye AS core-builder
FROM rust:1.85.1-bullseye AS core-builder
WORKDIR /builder
COPY Cargo.toml Cargo.lock ./
+25 -1
View File
@@ -192,7 +192,31 @@ pub async fn send_alert(
AlertData::None {} => Default::default(),
};
if !content.is_empty() {
send_message(url, &content).await?;
let vars_and_secrets = get_variables_and_secrets().await?;
let mut global_replacers = HashSet::new();
let mut secret_replacers = HashSet::new();
let mut url_interpolated = url.to_string();
// interpolate variables and secrets into the url
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut url_interpolated,
&mut global_replacers,
&mut secret_replacers,
)?;
send_message(&url_interpolated, &content)
.await
.map_err(|e| {
let replacers =
secret_replacers.into_iter().collect::<Vec<_>>();
let sanitized_error =
svi::replace_in_string(&format!("{e:?}"), &replacers);
anyhow::Error::msg(format!(
"Error with slack request: {}",
sanitized_error
))
})?;
}
Ok(())
}
+6
View File
@@ -19,6 +19,7 @@ use crate::{config::core_config, state::db_client};
mod discord;
mod slack;
mod ntfy;
#[instrument(level = "debug")]
pub async fn send_alerts(alerts: &[Alert]) {
@@ -128,6 +129,11 @@ pub async fn send_alert_to_alerter(
)
})
}
AlerterEndpoint::Ntfy(NtfyAlerterEndpoint { url }) => {
ntfy::send_alert(url, alert).await.with_context(|| {
format!("Failed to send alert to ntfy Alerter {}", alerter.name)
})
}
}
}
+248
View File
@@ -0,0 +1,248 @@
use std::sync::OnceLock;
use super::*;
#[instrument(level = "debug")]
pub async fn send_alert(
url: &str,
alert: &Alert,
) -> anyhow::Result<()> {
let level = fmt_level(alert.level);
let content = match &alert.data {
AlertData::Test { id, name } => {
let link = resource_link(ResourceTargetVariant::Alerter, id);
format!(
"{level} | If you see this message, then Alerter {} is working\n{link}",
name,
)
}
AlertData::ServerUnreachable {
id,
name,
region,
err,
} => {
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
match alert.level {
SeverityLevel::Ok => {
format!(
"{level} | {}{} is now reachable\n{link}",
name, region
)
}
SeverityLevel::Critical => {
let err = err
.as_ref()
.map(|e| format!("\nerror: {:#?}", e))
.unwrap_or_default();
format!(
"{level} | {}{} is unreachable ❌\n{link}{err}",
name, region
)
}
_ => unreachable!(),
}
}
AlertData::ServerCpu {
id,
name,
region,
percentage,
} => {
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
format!(
"{level} | {}{} cpu usage at {percentage:.1}%\n{link}",
name, region,
)
}
AlertData::ServerMem {
id,
name,
region,
used_gb,
total_gb,
} => {
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
let percentage = 100.0 * used_gb / total_gb;
format!(
"{level} | {}{} memory usage at {percentage:.1}%💾\n\nUsing {used_gb:.1} GiB / {total_gb:.1} GiB\n{link}",
name, region,
)
}
AlertData::ServerDisk {
id,
name,
region,
path,
used_gb,
total_gb,
} => {
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
let percentage = 100.0 * used_gb / total_gb;
format!(
"{level} | {}{} disk usage at {percentage:.1}%💿\nmount point: {:?}\nusing {used_gb:.1} GiB / {total_gb:.1} GiB\n{link}",
name, region, path,
)
}
AlertData::ContainerStateChange {
id,
name,
server_id: _server_id,
server_name,
from,
to,
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
let to_state = fmt_docker_container_state(to);
format!(
"📦Deployment {} is now {}\nserver: {}\nprevious: {}\n{link}",
name, to_state, server_name, from,
)
}
AlertData::DeploymentImageUpdateAvailable {
id,
name,
server_id: _server_id,
server_name,
image,
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
format!(
"⬆ Deployment {} has an update available\nserver: {}\nimage: {}\n{link}",
name, server_name, image,
)
}
AlertData::DeploymentAutoUpdated {
id,
name,
server_id: _server_id,
server_name,
image,
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
format!(
"⬆ Deployment {} was updated automatically\nserver: {}\nimage: {}\n{link}",
name, server_name, image,
)
}
AlertData::StackStateChange {
id,
name,
server_id: _server_id,
server_name,
from,
to,
} => {
let link = resource_link(ResourceTargetVariant::Stack, id);
let to_state = fmt_stack_state(to);
format!(
"🥞 Stack {} is now {}\nserver: {}\nprevious: {}\n{link}",
name, to_state, server_name, from,
)
}
AlertData::StackImageUpdateAvailable {
id,
name,
server_id: _server_id,
server_name,
service,
image,
} => {
let link = resource_link(ResourceTargetVariant::Stack, id);
format!(
"⬆ Stack {} has an update available\nserver: {}\nservice: {}\nimage: {}\n{link}",
name, server_name, service, image,
)
}
AlertData::StackAutoUpdated {
id,
name,
server_id: _server_id,
server_name,
images,
} => {
let link = resource_link(ResourceTargetVariant::Stack, id);
let images_label =
if images.len() > 1 { "images" } else { "image" };
let images_str = images.join(", ");
format!(
"⬆ Stack {} was updated automatically ⏫\nserver: {}\n{}: {}\n{link}",
name, server_name, images_label, images_str,
)
}
AlertData::AwsBuilderTerminationFailed {
instance_id,
message,
} => {
format!(
"{level} | Failed to terminate AWS builder instance\ninstance id: {}\n{}",
instance_id, message,
)
}
AlertData::ResourceSyncPendingUpdates { id, name } => {
let link =
resource_link(ResourceTargetVariant::ResourceSync, id);
format!(
"{level} | Pending resource sync updates on {}\n{link}",
name,
)
}
AlertData::BuildFailed { id, name, version } => {
let link = resource_link(ResourceTargetVariant::Build, id);
format!(
"{level} | Build {} failed\nversion: v{}\n{link}",
name, version,
)
}
AlertData::RepoBuildFailed { id, name } => {
let link = resource_link(ResourceTargetVariant::Repo, id);
format!("{level} | Repo build for {} failed\n{link}", name,)
}
AlertData::None {} => Default::default(),
};
if !content.is_empty() {
send_message(url, content).await?;
}
Ok(())
}
async fn send_message(
url: &str,
content: String,
) -> anyhow::Result<()> {
let response = http_client()
.post(url)
.header("Title", "ntfy Alert")
.body(content)
.send()
.await
.context("Failed to send message")?;
let status = response.status();
if status.is_success() {
debug!("ntfy alert sent successfully: {}", status);
Ok(())
} else {
let text = response.text().await.with_context(|| {
format!(
"Failed to send message to ntfy | {} | failed to get response text",
status
)
})?;
Err(anyhow!(
"Failed to send message to ntfy | {} | {}",
status,
text
))
}
}
fn http_client() -> &'static reqwest::Client {
static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
CLIENT.get_or_init(reqwest::Client::new)
}
+24 -2
View File
@@ -399,8 +399,30 @@ pub async fn send_alert(
AlertData::None {} => Default::default(),
};
if !text.is_empty() {
let slack = ::slack::Client::new(url);
slack.send_message(text, blocks).await?;
let vars_and_secrets = get_variables_and_secrets().await?;
let mut global_replacers = HashSet::new();
let mut secret_replacers = HashSet::new();
let mut url_interpolated = url.to_string();
// interpolate variables and secrets into the url
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut url_interpolated,
&mut global_replacers,
&mut secret_replacers,
)?;
let slack = ::slack::Client::new(url_interpolated);
slack.send_message(text, blocks).await.map_err(|e| {
let replacers =
secret_replacers.into_iter().collect::<Vec<_>>();
let sanitized_error =
svi::replace_in_string(&format!("{e:?}"), &replacers);
anyhow::Error::msg(format!(
"Error with slack request: {}",
sanitized_error
))
})?;
}
Ok(())
}
+88 -90
View File
@@ -86,7 +86,13 @@ impl Resolve<ExecuteArgs> for RunBuild {
PermissionLevel::Execute,
)
.await?;
let mut vars_and_secrets = get_variables_and_secrets().await?;
// Add the $VERSION to variables. Use with [[$VERSION]]
vars_and_secrets.variables.insert(
String::from("$VERSION"),
build.config.version.to_string(),
);
if build.config.builder_id.is_empty() {
return Err(anyhow!("Must attach builder to RunBuild").into());
@@ -110,14 +116,6 @@ impl Resolve<ExecuteArgs> for RunBuild {
update.version = build.config.version;
update_update(update.clone()).await?;
// Add the $VERSION to variables. Use with [[$VERSION]]
if !vars_and_secrets.variables.contains_key("$VERSION") {
vars_and_secrets.variables.insert(
String::from("$VERSION"),
build.config.version.to_string(),
);
}
let git_token = git_token(
&build.config.git_provider,
&build.config.git_account,
@@ -177,7 +175,6 @@ impl Resolve<ExecuteArgs> for RunBuild {
});
// GET BUILDER PERIPHERY
let (periphery, cleanup_data) = match get_builder_periphery(
build.name.clone(),
Some(build.config.version),
@@ -203,9 +200,8 @@ impl Resolve<ExecuteArgs> for RunBuild {
}
};
// CLONE REPO
// INTERPOLATE VARIABLES
let secret_replacers = if !build.config.skip_secret_interp {
// Interpolate variables / secrets into pre build command
let mut global_replacers = HashSet::new();
let mut secret_replacers = HashSet::new();
@@ -216,6 +212,34 @@ impl Resolve<ExecuteArgs> for RunBuild {
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut build.config.build_args,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut build.config.secret_args,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut build.config.dockerfile,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_extra_args(
&vars_and_secrets,
&mut build.config.extra_args,
&mut global_replacers,
&mut secret_replacers,
)?;
add_interp_update_log(
&mut update,
&global_replacers,
@@ -227,84 +251,57 @@ impl Resolve<ExecuteArgs> for RunBuild {
Default::default()
};
let res = tokio::select! {
res = periphery
.request(api::git::CloneRepo {
args: (&build).into(),
git_token,
environment: Default::default(),
env_file_path: Default::default(),
skip_secret_interp: Default::default(),
replacers: secret_replacers.into_iter().collect(),
}) => res,
_ = cancel.cancelled() => {
debug!("build cancelled during clone, cleaning up builder");
update.push_error_log("build cancelled", String::from("user cancelled build during repo clone"));
cleanup_builder_instance(periphery, cleanup_data, &mut update)
.await;
info!("builder cleaned up");
return handle_early_return(update, build.id, build.name, true).await
},
};
let commit_message = match res {
Ok(res) => {
debug!("finished repo clone");
update.logs.extend(res.logs);
update.commit_hash =
res.commit_hash.unwrap_or_default().to_string();
res.commit_message.unwrap_or_default()
}
Err(e) => {
warn!("failed build at clone repo | {e:#}");
update.push_error_log(
"clone repo",
format_serror(&e.context("failed to clone repo").into()),
);
Default::default()
}
};
update_update(update.clone()).await?;
if all_logs_success(&update.logs) {
let secret_replacers = if !build.config.skip_secret_interp {
// Interpolate variables / secrets into build args
let mut global_replacers = HashSet::new();
let mut secret_replacers = HashSet::new();
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut build.config.build_args,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut build.config.secret_args,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_extra_args(
&vars_and_secrets,
&mut build.config.extra_args,
&mut global_replacers,
&mut secret_replacers,
)?;
add_interp_update_log(
&mut update,
&global_replacers,
&secret_replacers,
);
secret_replacers
} else {
Default::default()
let commit_message = if !build.config.files_on_host
&& !build.config.repo.is_empty()
{
// CLONE REPO
let res = tokio::select! {
res = periphery
.request(api::git::CloneRepo {
args: (&build).into(),
git_token,
environment: Default::default(),
env_file_path: Default::default(),
skip_secret_interp: Default::default(),
replacers: Default::default(),
}) => res,
_ = cancel.cancelled() => {
debug!("build cancelled during clone, cleaning up builder");
update.push_error_log("build cancelled", String::from("user cancelled build during repo clone"));
cleanup_builder_instance(cleanup_data, &mut update)
.await;
info!("builder cleaned up");
return handle_early_return(update, build.id, build.name, true).await
},
};
let commit_message = match res {
Ok(res) => {
debug!("finished repo clone");
update.logs.extend(res.logs);
update.commit_hash =
res.commit_hash.unwrap_or_default().to_string();
res.commit_message.unwrap_or_default()
}
Err(e) => {
warn!("failed build at clone repo | {e:#}");
update.push_error_log(
"clone repo",
format_serror(&e.context("failed to clone repo").into()),
);
Default::default()
}
};
update_update(update.clone()).await?;
Some(commit_message)
} else {
None
};
if all_logs_success(&update.logs) {
// RUN BUILD
let res = tokio::select! {
res = periphery
.request(api::build::Build {
@@ -321,7 +318,7 @@ impl Resolve<ExecuteArgs> for RunBuild {
_ = cancel.cancelled() => {
info!("build cancelled during build, cleaning up builder");
update.push_error_log("build cancelled", String::from("user cancelled build during docker build"));
cleanup_builder_instance(periphery, cleanup_data, &mut update)
cleanup_builder_instance(cleanup_data, &mut update)
.await;
return handle_early_return(update, build.id, build.name, true).await
},
@@ -365,8 +362,9 @@ impl Resolve<ExecuteArgs> for RunBuild {
// stop the cancel listening task from going forever
cancel.cancel();
cleanup_builder_instance(periphery, cleanup_data, &mut update)
.await;
// If building on temporary cloud server (AWS),
// this will terminate the server.
cleanup_builder_instance(cleanup_data, &mut update).await;
// Need to manually update the update before cache refresh,
// and before broadcast with add_update.
+4 -3
View File
@@ -462,7 +462,7 @@ impl Resolve<ExecuteArgs> for BuildRepo {
_ = cancel.cancelled() => {
debug!("build cancelled during clone, cleaning up builder");
update.push_error_log("build cancelled", String::from("user cancelled build during repo clone"));
cleanup_builder_instance(periphery, cleanup_data, &mut update)
cleanup_builder_instance(cleanup_data, &mut update)
.await;
info!("builder cleaned up");
return handle_builder_early_return(update, repo.id, repo.name, true).await
@@ -506,8 +506,9 @@ impl Resolve<ExecuteArgs> for BuildRepo {
// stop the cancel listening task from going forever
cancel.cancel();
cleanup_builder_instance(periphery, cleanup_data, &mut update)
.await;
// If building on temporary cloud server (AWS),
// this will terminate the server.
cleanup_builder_instance(cleanup_data, &mut update).await;
// Need to manually update the update before cache refresh,
// and before broadcast with add_update.
+1
View File
@@ -116,6 +116,7 @@ enum ReadRequest {
InspectDockerImage(InspectDockerImage),
ListDockerImageHistory(ListDockerImageHistory),
InspectDockerVolume(InspectDockerVolume),
GetDockerContainersSummary(GetDockerContainersSummary),
ListAllDockerContainers(ListAllDockerContainers),
ListDockerContainers(ListDockerContainers),
ListDockerNetworks(ListDockerNetworks),
+42 -1
View File
@@ -14,7 +14,9 @@ use komodo_client::{
ResourceTarget,
deployment::Deployment,
docker::{
container::{Container, ContainerListItem},
container::{
Container, ContainerListItem, ContainerStateStatusEnum,
},
image::{Image, ImageHistoryResponseItem},
network::Network,
volume::Volume,
@@ -413,6 +415,45 @@ impl Resolve<ReadArgs> for ListAllDockerContainers {
}
}
impl Resolve<ReadArgs> for GetDockerContainersSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetDockerContainersSummaryResponse> {
let servers = resource::list_full_for_user::<Server>(
Default::default(),
user,
&[],
)
.await
.context("failed to get servers from db")?;
let mut res = GetDockerContainersSummaryResponse::default();
for server in servers {
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(containers) = &cache.containers {
for container in containers {
res.total += 1;
match container.state {
ContainerStateStatusEnum::Created
| ContainerStateStatusEnum::Paused
| ContainerStateStatusEnum::Exited => res.stopped += 1,
ContainerStateStatusEnum::Running => res.running += 1,
ContainerStateStatusEnum::Empty => res.unknown += 1,
_ => res.unhealthy += 1,
}
}
}
}
Ok(res)
}
}
impl Resolve<ReadArgs> for InspectDockerContainer {
async fn resolve(
self,
+339 -37
View File
@@ -1,12 +1,17 @@
use std::{path::PathBuf, str::FromStr, time::Duration};
use anyhow::{Context, anyhow};
use formatting::format_serror;
use git::GitRes;
use komodo_client::{
api::write::*,
entities::{
CloneArgs, NoData,
CloneArgs, FileContents, NoData, Operation, all_logs_success,
build::{Build, BuildInfo, PartialBuildConfig},
builder::{Builder, BuilderConfig},
config::core::CoreConfig,
permission::PermissionLevel,
server::ServerState,
update::Update,
},
};
@@ -15,11 +20,22 @@ use mungos::mongodb::bson::to_document;
use octorust::types::{
ReposCreateWebhookRequest, ReposCreateWebhookRequestConfig,
};
use periphery_client::{
PeripheryClient,
api::build::{
GetDockerfileContentsOnHost, WriteDockerfileContentsToHost,
},
};
use resolver_api::Resolve;
use tokio::fs;
use crate::{
config::core_config,
helpers::git_token,
helpers::{
git_token, periphery_client,
query::get_server_with_state,
update::{add_update, make_update},
},
resource,
state::{db_client, github_client},
};
@@ -88,6 +104,184 @@ impl Resolve<WriteArgs> for RenameBuild {
}
}
impl Resolve<WriteArgs> for WriteBuildFileContents {
#[instrument(name = "WriteBuildFileContents", skip(args))]
async fn resolve(self, args: &WriteArgs) -> serror::Result<Update> {
let build = resource::get_check_permissions::<Build>(
&self.build,
&args.user,
PermissionLevel::Write,
)
.await?;
if !build.config.files_on_host && build.config.repo.is_empty() {
return Err(anyhow!(
"Build is not configured to use Files on Host or Git Repo, can't write dockerfile contents"
).into());
}
let mut update =
make_update(&build, Operation::WriteDockerfile, &args.user);
update.push_simple_log("Dockerfile to write", &self.contents);
if build.config.files_on_host {
match get_on_host_periphery(&build)
.await?
.request(WriteDockerfileContentsToHost {
name: build.name,
build_path: build.config.build_path,
dockerfile_path: build.config.dockerfile_path,
contents: self.contents,
})
.await
.context("Failed to write dockerfile contents to host")
{
Ok(log) => {
update.logs.push(log);
}
Err(e) => {
update.push_error_log(
"Write Dockerfile Contents",
format_serror(&e.into()),
);
}
};
if !all_logs_success(&update.logs) {
update.finalize();
update.id = add_update(update.clone()).await?;
return Ok(update);
}
if let Err(e) =
(RefreshBuildCache { build: build.id }).resolve(args).await
{
update.push_error_log(
"Refresh build cache",
format_serror(&e.error.into()),
);
}
update.finalize();
update.id = add_update(update.clone()).await?;
Ok(update)
} else {
write_dockerfile_contents_git(self, args, build, update).await
}
}
}
async fn write_dockerfile_contents_git(
req: WriteBuildFileContents,
args: &WriteArgs,
build: Build,
mut update: Update,
) -> serror::Result<Update> {
let WriteBuildFileContents { build: _, contents } = req;
let mut clone_args: CloneArgs = (&build).into();
let root = clone_args.unique_path(&core_config().repo_directory)?;
let build_path = build
.config
.build_path
.parse::<PathBuf>()
.context("Invalid build path")?;
let dockerfile_path = build
.config
.dockerfile_path
.parse::<PathBuf>()
.context("Invalid dockerfile path")?;
let full_path = root.join(&build_path).join(&dockerfile_path);
if let Some(parent) = full_path.parent() {
fs::create_dir_all(parent).await.with_context(|| {
format!(
"Failed to initialize dockerfile parent directory {parent:?}"
)
})?;
}
// Ensure the folder is initialized as git repo.
// This allows a new file to be committed on a branch that may not exist.
if !root.join(".git").exists() {
let access_token = if let Some(account) = &clone_args.account {
git_token(&clone_args.provider, account, |https| clone_args.https = https)
.await
.with_context(
|| format!("Failed to get git token in call to db. Stopping run. | {} | {account}", clone_args.provider),
)?
} else {
None
};
git::init_folder_as_repo(
&root,
&clone_args,
access_token.as_deref(),
&mut update.logs,
)
.await;
if !all_logs_success(&update.logs) {
update.finalize();
update.id = add_update(update.clone()).await?;
return Ok(update);
}
}
if let Err(e) =
fs::write(&full_path, &contents).await.with_context(|| {
format!("Failed to write dockerfile contents to {full_path:?}")
})
{
update
.push_error_log("Write Dockerfile", format_serror(&e.into()));
} else {
update.push_simple_log(
"Write Dockerfile",
format!("File written to {full_path:?}"),
);
};
if !all_logs_success(&update.logs) {
update.finalize();
update.id = add_update(update.clone()).await?;
return Ok(update);
}
let commit_res = git::commit_file(
&format!("{}: Commit Dockerfile", args.user.username),
&root,
&build_path.join(&dockerfile_path),
&build.config.branch,
)
.await;
update.logs.extend(commit_res.logs);
if let Err(e) = (RefreshBuildCache { build: build.name })
.resolve(args)
.await
{
update.push_error_log(
"Refresh build cache",
format_serror(&e.error.into()),
);
}
update.finalize();
update.id = add_update(update.clone()).await?;
Ok(update)
}
impl Resolve<WriteArgs> for RefreshBuildCache {
#[instrument(
name = "RefreshBuildCache",
@@ -107,55 +301,104 @@ impl Resolve<WriteArgs> for RefreshBuildCache {
)
.await?;
if build.config.repo.is_empty()
|| build.config.git_provider.is_empty()
{
// Nothing to do here
return Ok(NoData {});
}
let (
remote_path,
remote_contents,
remote_error,
latest_hash,
latest_message,
) = if build.config.files_on_host {
// =============
// FILES ON HOST
// =============
match get_on_host_dockerfile(&build).await {
Ok(FileContents { path, contents }) => {
(Some(path), Some(contents), None, None, None)
}
Err(e) => {
(None, None, Some(format_serror(&e.into())), None, None)
}
}
} else if !build.config.repo.is_empty() {
// ================
// REPO BASED BUILD
// ================
if build.config.git_provider.is_empty() {
// Nothing to do here
return Ok(NoData {});
}
let config = core_config();
let config = core_config();
let mut clone_args: CloneArgs = (&build).into();
let repo_path =
clone_args.unique_path(&core_config().repo_directory)?;
clone_args.destination = Some(repo_path.display().to_string());
// Don't want to run these on core.
clone_args.on_clone = None;
clone_args.on_pull = None;
let mut clone_args: CloneArgs = (&build).into();
let repo_path =
clone_args.unique_path(&core_config().repo_directory)?;
clone_args.destination = Some(repo_path.display().to_string());
// Don't want to run these on core.
clone_args.on_clone = None;
clone_args.on_pull = None;
let access_token = if let Some(username) = &clone_args.account {
git_token(&clone_args.provider, username, |https| {
let access_token = if let Some(username) = &clone_args.account {
git_token(&clone_args.provider, username, |https| {
clone_args.https = https
})
.await
.with_context(
|| format!("Failed to get git token in call to db. Stopping run. | {} | {username}", clone_args.provider),
)?
} else {
None
};
} else {
None
};
let GitRes {
hash: latest_hash,
message: latest_message,
..
} = git::pull_or_clone(
clone_args,
&config.repo_directory,
access_token,
&[],
"",
None,
&[],
)
.await
.context("failed to clone build repo")?;
let GitRes { hash, message, .. } = git::pull_or_clone(
clone_args,
&config.repo_directory,
access_token,
&[],
"",
None,
&[],
)
.await
.context("failed to clone build repo")?;
let relative_path = PathBuf::from_str(&build.config.build_path)
.context("Invalid build path")?
.join(&build.config.dockerfile_path);
let full_path = repo_path.join(&relative_path);
let (contents, error) = match fs::read_to_string(&full_path)
.await
.with_context(|| {
format!(
"Failed to read dockerfile contents at {full_path:?}"
)
}) {
Ok(contents) => (Some(contents), None),
Err(e) => (None, Some(format_serror(&e.into()))),
};
(
Some(relative_path.display().to_string()),
contents,
error,
hash,
message,
)
} else {
// =============
// UI BASED FILE
// =============
(None, None, None, None, None)
};
let info = BuildInfo {
last_built_at: build.info.last_built_at,
built_hash: build.info.built_hash,
built_message: build.info.built_message,
built_contents: build.info.built_contents,
remote_path,
remote_contents,
remote_error,
latest_hash,
latest_message,
};
@@ -176,6 +419,65 @@ impl Resolve<WriteArgs> for RefreshBuildCache {
}
}
async fn get_on_host_periphery(
build: &Build,
) -> anyhow::Result<PeripheryClient> {
if build.config.builder_id.is_empty() {
return Err(anyhow!("No builder associated with build"));
}
let builder = resource::get::<Builder>(&build.config.builder_id)
.await
.context("Failed to get builder")?;
match builder.config {
BuilderConfig::Aws(_) => {
return Err(anyhow!(
"Files on host doesn't work with AWS builder"
));
}
BuilderConfig::Url(config) => {
let periphery = PeripheryClient::new(
config.address,
config.passkey,
Duration::from_secs(3),
);
periphery.health_check().await?;
Ok(periphery)
}
BuilderConfig::Server(config) => {
if config.server_id.is_empty() {
return Err(anyhow!(
"Builder is type server, but has no server attached"
));
}
let (server, state) =
get_server_with_state(&config.server_id).await?;
if state != ServerState::Ok {
return Err(anyhow!(
"Builder server is disabled or not reachable"
));
};
periphery_client(&server)
}
}
}
/// The successful case will be included as Some(remote_contents).
/// The error case will be included as Some(remote_error)
async fn get_on_host_dockerfile(
build: &Build,
) -> anyhow::Result<FileContents> {
get_on_host_periphery(build)
.await?
.request(GetDockerfileContentsOnHost {
name: build.name.clone(),
build_path: build.config.build_path.clone(),
dockerfile_path: build.config.dockerfile_path.clone(),
})
.await
}
impl Resolve<WriteArgs> for CreateBuildWebhook {
#[instrument(name = "CreateBuildWebhook", skip(args))]
async fn resolve(
+1
View File
@@ -96,6 +96,7 @@ pub enum WriteRequest {
DeleteBuild(DeleteBuild),
UpdateBuild(UpdateBuild),
RenameBuild(RenameBuild),
WriteBuildFileContents(WriteBuildFileContents),
RefreshBuildCache(RefreshBuildCache),
CreateBuildWebhook(CreateBuildWebhook),
DeleteBuildWebhook(DeleteBuildWebhook),
+1 -1
View File
@@ -211,7 +211,7 @@ impl Resolve<WriteArgs> for WriteStackFileContents {
}
update.finalize();
add_update(update.clone()).await?;
update.id = add_update(update.clone()).await?;
Ok(update)
}
+33 -32
View File
@@ -132,6 +132,38 @@ impl Resolve<WriteArgs> for RenameResourceSync {
}
}
impl Resolve<WriteArgs> for WriteSyncFileContents {
#[instrument(name = "WriteSyncFileContents", skip(args))]
async fn resolve(self, args: &WriteArgs) -> serror::Result<Update> {
let sync = resource::get_check_permissions::<ResourceSync>(
&self.sync,
&args.user,
PermissionLevel::Write,
)
.await?;
if !sync.config.files_on_host && sync.config.repo.is_empty() {
return Err(
anyhow!(
"This method is only for 'files on host' or 'repo' based syncs."
)
.into(),
);
}
let mut update =
make_update(&sync, Operation::WriteSyncContents, &args.user);
update.push_simple_log("File contents", &self.contents);
if sync.config.files_on_host {
write_sync_file_contents_on_host(self, args, sync, update).await
} else {
write_sync_file_contents_git(self, args, sync, update).await
}
}
}
async fn write_sync_file_contents_on_host(
req: WriteSyncFileContents,
args: &WriteArgs,
@@ -298,7 +330,7 @@ async fn write_sync_file_contents_git(
.await
{
update.push_error_log(
"Refresh failed",
"Refresh sync pending",
format_serror(&e.error.into()),
);
}
@@ -309,37 +341,6 @@ async fn write_sync_file_contents_git(
Ok(update)
}
impl Resolve<WriteArgs> for WriteSyncFileContents {
async fn resolve(self, args: &WriteArgs) -> serror::Result<Update> {
let sync = resource::get_check_permissions::<ResourceSync>(
&self.sync,
&args.user,
PermissionLevel::Write,
)
.await?;
if !sync.config.files_on_host && sync.config.repo.is_empty() {
return Err(
anyhow!(
"This method is only for 'files on host' or 'repo' based syncs."
)
.into(),
);
}
let mut update =
make_update(&sync, Operation::WriteSyncContents, &args.user);
update.push_simple_log("File contents", &self.contents);
if sync.config.files_on_host {
write_sync_file_contents_on_host(self, args, sync, update).await
} else {
write_sync_file_contents_git(self, args, sync, update).await
}
}
}
impl Resolve<WriteArgs> for CommitSync {
#[instrument(name = "CommitSync", skip(args))]
async fn resolve(self, args: &WriteArgs) -> serror::Result<Update> {
+3 -1
View File
@@ -5,6 +5,8 @@ pub mod hetzner;
#[derive(Debug)]
pub enum BuildCleanupData {
Server { repo_name: String },
/// Nothing to clean up
Server,
/// Clean up AWS instance
Aws { instance_id: String, region: String },
}
+1
View File
@@ -139,6 +139,7 @@ pub fn core_config() -> &'static CoreConfig {
title: env.komodo_title.unwrap_or(config.title),
host: env.komodo_host.unwrap_or(config.host),
port: env.komodo_port.unwrap_or(config.port),
bind_ip: env.komodo_bind_ip.unwrap_or(config.bind_ip),
first_server: env.komodo_first_server.unwrap_or(config.first_server),
frontend_path: env.komodo_frontend_path.unwrap_or(config.frontend_path),
jwt_ttl: env
+5 -18
View File
@@ -61,12 +61,7 @@ pub async fn get_builder_periphery(
.health_check()
.await
.context("Url Builder failed health check")?;
Ok((
periphery,
BuildCleanupData::Server {
repo_name: resource_name,
},
))
Ok((periphery, BuildCleanupData::Server))
}
BuilderConfig::Server(config) => {
if config.server_id.is_empty() {
@@ -74,12 +69,7 @@ pub async fn get_builder_periphery(
}
let server = resource::get::<Server>(&config.server_id).await?;
let periphery = periphery_client(&server)?;
Ok((
periphery,
BuildCleanupData::Server {
repo_name: resource_name,
},
))
Ok((periphery, BuildCleanupData::Server))
}
BuilderConfig::Aws(config) => {
get_aws_builder(&resource_name, version, config, update).await
@@ -179,17 +169,14 @@ async fn get_aws_builder(
)
}
#[instrument(skip(periphery, update))]
#[instrument(skip(update))]
pub async fn cleanup_builder_instance(
periphery: PeripheryClient,
cleanup_data: BuildCleanupData,
update: &mut Update,
) {
match cleanup_data {
BuildCleanupData::Server { repo_name } => {
let _ = periphery
.request(api::git::DeleteRepo { name: repo_name })
.await;
BuildCleanupData::Server => {
// Nothing to clean up
}
BuildCleanupData::Aws {
instance_id,
+9 -2
View File
@@ -78,6 +78,9 @@ pub async fn git_token(
account_username: &str,
mut on_https_found: impl FnMut(bool),
) -> anyhow::Result<Option<String>> {
if provider_domain.is_empty() || account_username.is_empty() {
return Ok(None);
}
let db_provider = db_client()
.git_accounts
.find_one(doc! { "domain": provider_domain, "username": account_username })
@@ -143,7 +146,11 @@ pub fn periphery_client(
let client = PeripheryClient::new(
&server.config.address,
&core_config().passkey,
if server.config.passkey.is_empty() {
&core_config().passkey
} else {
&server.config.passkey
},
Duration::from_secs(server.config.timeout_seconds as u64),
);
@@ -352,4 +359,4 @@ pub async fn ensure_first_server_and_builder() {
e.error
);
}
}
}
+3 -2
View File
@@ -86,9 +86,10 @@ async fn app() -> anyhow::Result<()> {
)
.into_make_service();
let addr = format!("{}:{}", core_config().bind_ip, core_config().port);
let socket_addr =
SocketAddr::from_str(&format!("0.0.0.0:{}", core_config().port))
.context("failed to parse socket addr")?;
SocketAddr::from_str(&addr)
.context("failed to parse listen address")?;
if config.ssl_enabled {
info!("🔒 Core SSL Enabled");
+40 -18
View File
@@ -1,26 +1,32 @@
use std::time::Duration;
use anyhow::Context;
use komodo_client::entities::{
Operation, ResourceTargetVariant,
build::{
Build, BuildConfig, BuildConfigDiff, BuildInfo, BuildListItem,
BuildListItemInfo, BuildQuerySpecifics, BuildState,
PartialBuildConfig,
use formatting::format_serror;
use komodo_client::{
api::write::RefreshBuildCache,
entities::{
Operation, ResourceTargetVariant,
build::{
Build, BuildConfig, BuildConfigDiff, BuildInfo, BuildListItem,
BuildListItemInfo, BuildQuerySpecifics, BuildState,
PartialBuildConfig,
},
builder::Builder,
environment_vars_from_str, optional_string,
permission::PermissionLevel,
resource::Resource,
update::Update,
user::{User, build_user},
},
builder::Builder,
environment_vars_from_str,
permission::PermissionLevel,
resource::Resource,
update::Update,
user::User,
};
use mungos::{
find::find_collect,
mongodb::{Collection, bson::doc, options::FindOptions},
};
use resolver_api::Resolve;
use crate::{
api::write::WriteArgs,
config::core_config,
helpers::{empty_or_only_spaces, query::get_latest_update},
state::{action_states, build_state_cache, db_client},
@@ -56,10 +62,13 @@ impl super::KomodoResource for Build {
last_built_at: build.info.last_built_at,
version: build.config.version,
builder_id: build.config.builder_id,
git_provider: build.config.git_provider,
image_registry_domain: build.config.image_registry.domain,
repo: build.config.repo,
branch: build.config.branch,
files_on_host: build.config.files_on_host,
git_provider: optional_string(build.config.git_provider),
repo: optional_string(build.config.repo),
branch: optional_string(build.config.branch),
image_registry_domain: optional_string(
build.config.image_registry.domain,
),
built_hash: build.info.built_hash,
latest_hash: build.info.latest_hash,
state,
@@ -96,10 +105,23 @@ impl super::KomodoResource for Build {
}
async fn post_create(
_created: &Resource<Self::Config, Self::Info>,
_update: &mut Update,
created: &Resource<Self::Config, Self::Info>,
update: &mut Update,
) -> anyhow::Result<()> {
refresh_build_state_cache().await;
if let Err(e) = (RefreshBuildCache {
build: created.name.clone(),
})
.resolve(&WriteArgs {
user: build_user().to_owned(),
})
.await
{
update.push_error_log(
"Refresh build cache",
format_serror(&e.error.context("The build cache has failed to refresh. This may be due to a misconfiguration of the Build").into())
);
};
Ok(())
}
+1
View File
@@ -170,6 +170,7 @@ impl super::KomodoResource for Repo {
} else {
repo.config.path.clone()
},
is_build: false,
})
.await
{
+1 -1
View File
@@ -162,7 +162,7 @@ impl super::KomodoResource for Stack {
{
update.push_error_log(
"Refresh stack cache",
format_serror(&e.error.context("The stack cache has failed to refresh. This is likely due to a misconfiguration of the Stack").into())
format_serror(&e.error.context("The stack cache has failed to refresh. This may be due to a misconfiguration of the Stack").into())
);
};
if created.config.server_id.is_empty() {
+3 -3
View File
@@ -26,7 +26,7 @@ pub async fn get_repo_compose_contents(
let (repo_path, _logs, hash, message) =
ensure_remote_repo(clone_args)
.await
.context("failed to clone stack repo")?;
.context("Failed to clone stack repo")?;
let run_directory = repo_path.join(&stack.config.run_directory);
// This will remove any intermediate '/./' which can be a problem for some OS.
@@ -44,7 +44,7 @@ pub async fn get_repo_compose_contents(
}
// If file does not exist, will show up in err case so the log is handled
match fs::read_to_string(&file_path).with_context(|| {
format!("failed to read file contents from {file_path:?}")
format!("Failed to read file contents from {file_path:?}")
}) {
Ok(contents) => successful.push(FileContents {
path: path.to_string(),
@@ -102,6 +102,6 @@ pub async fn ensure_remote_repo(
&[],
)
.await
.context("failed to clone stack repo")
.context("Failed to clone stack repo")
.map(|res| (repo_path, res.logs, res.hash, res.message))
}
+9 -12
View File
@@ -8,18 +8,15 @@ pub fn extract_services_from_stack(
stack: &Stack,
) -> Vec<StackServiceNames> {
if let Some(mut services) = stack.info.deployed_services.clone() {
if services.iter().any(|service| service.image.is_empty()) {
for service in
services.iter_mut().filter(|s| s.image.is_empty())
{
service.image = stack
.info
.latest_services
.iter()
.find(|s| s.service_name == service.service_name)
.map(|s| s.image.clone())
.unwrap_or_default();
}
for service in services.iter_mut().filter(|s| s.image.is_empty())
{
service.image = stack
.info
.latest_services
.iter()
.find(|s| s.service_name == service.service_name)
.map(|s| s.image.clone())
.unwrap_or_default();
}
services
} else {
+1 -1
View File
@@ -1,6 +1,6 @@
## All in one, multi stage compile + runtime Docker build for your architecture.
FROM rust:1.85.0-bullseye AS builder
FROM rust:1.85.1-bullseye AS builder
WORKDIR /builder
COPY Cargo.toml Cargo.lock ./
+178 -14
View File
@@ -1,8 +1,12 @@
use std::{fmt::Write, path::Path};
use std::{
fmt::Write,
path::{Path, PathBuf},
};
use anyhow::{Context, anyhow};
use command::{
run_komodo_command, run_komodo_command_with_interpolation,
run_komodo_command, run_komodo_command_multiline,
run_komodo_command_with_interpolation,
};
use formatting::format_serror;
use komodo_client::{
@@ -16,9 +20,12 @@ use komodo_client::{
parsers::QUOTE_PATTERN,
};
use periphery_client::api::build::{
self, PruneBuilders, PruneBuildx,
self, GetDockerfileContentsOnHost,
GetDockerfileContentsOnHostResponse, PruneBuilders, PruneBuildx,
WriteDockerfileContentsToHost,
};
use resolver_api::Resolve;
use tokio::fs;
use crate::{
config::periphery_config,
@@ -26,6 +33,88 @@ use crate::{
helpers::{parse_extra_args, parse_labels},
};
impl Resolve<super::Args> for GetDockerfileContentsOnHost {
#[instrument(name = "GetDockerfileContentsOnHost", level = "debug")]
async fn resolve(
self,
_: &super::Args,
) -> serror::Result<GetDockerfileContentsOnHostResponse> {
let GetDockerfileContentsOnHost {
name,
build_path,
dockerfile_path,
} = self;
let root =
periphery_config().build_dir.join(to_komodo_name(&name));
let build_dir =
root.join(&build_path).components().collect::<PathBuf>();
if !build_dir.exists() {
fs::create_dir_all(&build_dir)
.await
.context("Failed to initialize build directory")?;
}
let full_path = build_dir
.join(&dockerfile_path)
.components()
.collect::<PathBuf>();
let contents =
fs::read_to_string(&full_path).await.with_context(|| {
format!("Failed to read dockerfile contents at {full_path:?}")
})?;
Ok(GetDockerfileContentsOnHostResponse {
contents,
path: full_path.display().to_string(),
})
}
}
impl Resolve<super::Args> for WriteDockerfileContentsToHost {
#[instrument(
name = "WriteDockerfileContentsToHost",
skip_all,
fields(
stack = &self.name,
build_path = &self.build_path,
dockerfile_path = &self.dockerfile_path,
)
)]
async fn resolve(self, _: &super::Args) -> serror::Result<Log> {
let WriteDockerfileContentsToHost {
name,
build_path,
dockerfile_path,
contents,
} = self;
let full_path = periphery_config()
.build_dir
.join(to_komodo_name(&name))
.join(&build_path)
.join(dockerfile_path)
.components()
.collect::<PathBuf>();
// Ensure parent directory exists
if let Some(parent) = full_path.parent() {
if !parent.exists() {
tokio::fs::create_dir_all(parent)
.await
.with_context(|| format!("Failed to initialize dockerfile parent directory {parent:?}"))?;
}
}
fs::write(&full_path, contents).await.with_context(|| {
format!("Failed to write dockerfile contents to {full_path:?}")
})?;
Ok(Log::simple(
"Write dockerfile to host",
format!("dockerfile contents written to {full_path:?}"),
))
}
}
impl Resolve<super::Args> for build::Build {
#[instrument(name = "Build", skip_all, fields(build = self.build.name.to_string()))]
async fn resolve(
@@ -36,7 +125,7 @@ impl Resolve<super::Args> for build::Build {
build,
registry_token,
additional_tags,
replacers: core_replacers,
replacers: mut core_replacers,
} = self;
let Build {
name,
@@ -53,11 +142,19 @@ impl Resolve<super::Args> for build::Build {
extra_args,
use_buildx,
image_registry,
repo,
files_on_host,
dockerfile,
pre_build,
..
},
..
} = &build;
if !*files_on_host && repo.is_empty() && dockerfile.is_empty() {
return Err(anyhow!("Build must be files on host mode, have a repo attached, or have dockerfile contents set to build").into());
}
let mut logs = Vec::new();
// Maybe docker login
@@ -82,14 +179,81 @@ impl Resolve<super::Args> for build::Build {
let name = to_komodo_name(name);
// Get paths
let build_dir =
periphery_config().repo_dir.join(&name).join(build_path);
let dockerfile_path = match optional_string(dockerfile_path) {
Some(dockerfile_path) => dockerfile_path.to_owned(),
None => "Dockerfile".to_owned(),
let build_path =
periphery_config().build_dir.join(&name).join(build_path);
let dockerfile_path = optional_string(dockerfile_path)
.unwrap_or("Dockerfile".to_owned());
// Write UI defined Dockerfile to host
if !*files_on_host && repo.is_empty() && !dockerfile.is_empty() {
let dockerfile = if *skip_secret_interp {
dockerfile.to_string()
} else {
let (dockerfile, replacers) = svi::interpolate_variables(
dockerfile,
&periphery_config().secrets,
svi::Interpolator::DoubleBrackets,
true,
).context("Failed to interpolate variables into UI defined dockerfile")?;
core_replacers.extend(replacers);
dockerfile
};
let full_path = build_path
.join(&dockerfile_path)
.components()
.collect::<PathBuf>();
// Ensure parent directory exists
if let Some(parent) = full_path.parent() {
if !parent.exists() {
tokio::fs::create_dir_all(parent)
.await
.with_context(|| format!("Failed to initialize dockerfile parent directory {parent:?}"))?;
}
}
fs::write(&full_path, dockerfile).await.with_context(|| {
format!(
"Failed to write dockerfile contents to {full_path:?}"
)
})?;
logs.push(Log::simple(
"Write Dockerfile",
format!("Dockerfile contents written to {full_path:?}"),
));
};
// Pre Build
if !pre_build.is_none() {
let pre_build_path = build_path.join(&pre_build.path);
if let Some(log) = if !skip_secret_interp {
run_komodo_command_with_interpolation(
"Pre Build",
Some(pre_build_path.as_path()),
&pre_build.command,
true,
&periphery_config().secrets,
&core_replacers,
)
.await
} else {
run_komodo_command_multiline(
"Pre Build",
Some(pre_build_path.as_path()),
&pre_build.command,
)
.await
} {
let success = log.success;
logs.push(log);
if success {
return Ok(logs);
}
};
}
// Get command parts
let image_name =
get_image_name(&build).context("failed to make image name")?;
@@ -109,7 +273,7 @@ impl Resolve<super::Args> for build::Build {
.context("Invalid secret_args")?;
let command_secret_args = parse_secret_args(
&secret_args,
&build_dir,
&build_path,
*skip_secret_interp,
)
.await?;
@@ -131,14 +295,14 @@ impl Resolve<super::Args> for build::Build {
if *skip_secret_interp {
let build_log = run_komodo_command(
"Docker Build",
build_dir.as_ref(),
build_path.as_ref(),
command,
)
.await;
logs.push(build_log);
} else if let Some(log) = run_komodo_command_with_interpolation(
"Docker Build",
build_dir.as_ref(),
build_path.as_ref(),
command,
false,
&periphery_config().secrets,
@@ -192,7 +356,7 @@ fn parse_build_args(build_args: &[EnvironmentVar]) -> String {
.join("")
}
/// https://docs.docker.com/build/building/secrets/#using-build-secrets
/// <https://docs.docker.com/build/building/secrets/#using-build-secrets>
async fn parse_secret_args(
secret_args: &[EnvironmentVar],
build_dir: &Path,
+1 -1
View File
@@ -206,7 +206,7 @@ impl Resolve<super::Args> for WriteComposeContentsToHost {
if let Some(parent) = file_path.parent() {
fs::create_dir_all(&parent)
.await
.with_context(|| format!("Failed to initialize environment file parent directory {parent:?}"))?;
.with_context(|| format!("Failed to initialize compose file parent directory {parent:?}"))?;
}
fs::write(&file_path, contents).await.with_context(|| {
format!(
+37 -12
View File
@@ -1,4 +1,5 @@
use anyhow::{Context, anyhow};
use formatting::format_serror;
use git::GitRes;
use komodo_client::entities::{CloneArgs, LatestCommit, update::Log};
use periphery_client::api::git::{
@@ -12,7 +13,7 @@ use tokio::fs;
use crate::config::periphery_config;
impl Resolve<super::Args> for GetLatestCommit {
#[instrument(name = "CloneRepo", level = "debug")]
#[instrument(name = "GetLatestCommit", level = "debug")]
async fn resolve(
self,
_: &super::Args,
@@ -68,9 +69,14 @@ impl Resolve<super::Args> for CloneRepo {
)?,
),
};
let parent_dir = if args.is_build {
&periphery_config().build_dir
} else {
&periphery_config().repo_dir
};
git::clone(
args,
&periphery_config().repo_dir,
parent_dir,
token,
&environment,
&env_file_path,
@@ -133,9 +139,14 @@ impl Resolve<super::Args> for PullRepo {
)?,
),
};
let parent_dir = if args.is_build {
&periphery_config().build_dir
} else {
&periphery_config().repo_dir
};
git::pull(
args,
&periphery_config().repo_dir,
parent_dir,
token,
&environment,
&env_file_path,
@@ -198,9 +209,14 @@ impl Resolve<super::Args> for PullOrCloneRepo {
)?,
),
};
let parent_dir = if args.is_build {
&periphery_config().build_dir
} else {
&periphery_config().repo_dir
};
git::pull_or_clone(
args,
&periphery_config().repo_dir,
parent_dir,
token,
&environment,
&env_file_path,
@@ -254,16 +270,25 @@ impl Resolve<super::Args> for RenameRepo {
impl Resolve<super::Args> for DeleteRepo {
#[instrument(name = "DeleteRepo")]
async fn resolve(self, _: &super::Args) -> serror::Result<Log> {
let DeleteRepo { name } = self;
let DeleteRepo { name, is_build } = self;
// If using custom clone path, it will be passed by core instead of name.
// So the join will resolve to just the absolute path.
let deleted =
fs::remove_dir_all(periphery_config().repo_dir.join(&name))
.await;
let msg = match deleted {
Ok(_) => format!("Deleted Repo {name}"),
Err(_) => format!("No Repo at {name} to delete"),
let root = if is_build {
&periphery_config().build_dir
} else {
&periphery_config().repo_dir
};
Ok(Log::simple("Delete Repo on Host", msg))
let full_path = root.join(&name);
let deleted =
fs::remove_dir_all(&full_path).await.with_context(|| {
format!("Failed to delete repo at {full_path:?}")
});
let log = match deleted {
Ok(_) => {
Log::simple("Delete repo", format!("Deleted Repo {name}"))
}
Err(e) => Log::error("Delete repo", format_serror(&e.into())),
};
Ok(log)
}
}
+2
View File
@@ -66,6 +66,8 @@ pub enum PeripheryRequest {
DeleteRepo(DeleteRepo),
// Build
GetDockerfileContentsOnHost(GetDockerfileContentsOnHost),
WriteDockerfileContentsToHost(WriteDockerfileContentsToHost),
Build(Build),
PruneBuilders(PruneBuilders),
PruneBuildx(PruneBuildx),
+2
View File
@@ -35,8 +35,10 @@ pub fn periphery_config() -> &'static PeripheryConfig {
PeripheryConfig {
port: env.periphery_port.unwrap_or(config.port),
bind_ip: env.periphery_bind_ip.unwrap_or(config.bind_ip),
repo_dir: env.periphery_repo_dir.unwrap_or(config.repo_dir),
stack_dir: env.periphery_stack_dir.unwrap_or(config.stack_dir),
build_dir: env.periphery_build_dir.unwrap_or(config.build_dir),
stats_polling_rate: env
.periphery_stats_polling_rate
.unwrap_or(config.stats_polling_rate),
+1 -1
View File
@@ -945,7 +945,7 @@ pub async fn docker_login(
None => crate::helpers::registry_token(domain, account)?,
};
let log = async_run_command(&format!(
"echo {registry_token} | docker login {domain} --username {account} --password-stdin",
"echo {registry_token} | docker login {domain} --username '{account}' --password-stdin",
))
.await;
if log.success() {
+5 -3
View File
@@ -26,9 +26,11 @@ async fn app() -> anyhow::Result<()> {
stats::spawn_system_stats_polling_thread();
let socket_addr =
SocketAddr::from_str(&format!("0.0.0.0:{}", config.port))
.context("failed to parse socket addr")?;
let addr = format!("{}:{}", config::periphery_config().bind_ip, config::periphery_config().port);
let socket_addr =
SocketAddr::from_str(&addr)
.context("failed to parse listen address")?;
let app = router::router()
.into_make_service_with_connect_info::<SocketAddr>();
+1 -1
View File
@@ -194,7 +194,7 @@ fn get_system_information(
os: System::long_os_version(),
kernel: System::kernel_version(),
host_name: System::host_name(),
core_count: sys.physical_core_count().map(|c| c as u32),
core_count: System::physical_core_count().map(|c| c as u32),
cpu_brand: sys
.cpus()
.iter()
+2 -2
View File
@@ -1,7 +1,7 @@
# Komodo
*A system to build and deploy software across many servers*
*A system to build and deploy software across many servers*. [https://komo.do](https://komo.do)
Full Docs: [https://docs.rs/komodo_client/latest/komodo_client](https://docs.rs/komodo_client/latest/komodo_client).
Docs: [https://docs.rs/komodo_client/latest/komodo_client](https://docs.rs/komodo_client/latest/komodo_client).
This is a client library for the Komodo Core API.
It contains:
+29
View File
@@ -294,6 +294,35 @@ pub type ListAllDockerContainersResponse = Vec<ContainerListItem>;
//
/// Gets a summary of data relating to all containers.
/// Response: [GetDockerContainersSummaryResponse].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoReadRequest)]
#[response(GetDockerContainersSummaryResponse)]
#[error(serror::Error)]
pub struct GetDockerContainersSummary {}
/// Response for [GetDockerContainersSummary]
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct GetDockerContainersSummaryResponse {
/// The total number of Containers
pub total: u32,
/// The number of Containers with Running state
pub running: u32,
/// The number of Containers with Stopped or Paused or Created state
pub stopped: u32,
/// The number of Containers with Restarting or Dead state
pub unhealthy: u32,
/// The number of Containers with Unknown state
pub unknown: u32,
}
//
/// Inspect a docker container on the server. Response: [Container].
#[typeshare]
#[derive(
-1
View File
@@ -98,7 +98,6 @@ pub struct UpdateAction {
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
#[error(serror::Error)]
pub struct RenameAction {
/// The id or name of the Action to rename.
pub id: String,
-1
View File
@@ -95,7 +95,6 @@ pub struct UpdateAlerter {
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
#[error(serror::Error)]
pub struct RenameAlerter {
/// The id or name of the Alerter to rename.
pub id: String,
+18 -1
View File
@@ -98,7 +98,6 @@ pub struct UpdateBuild {
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
#[error(serror::Error)]
pub struct RenameBuild {
/// The id or name of the Build to rename.
pub id: String,
@@ -108,6 +107,24 @@ pub struct RenameBuild {
//
/// Update dockerfile contents in Files on Server or Git Repo mode. Response: [Update].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
pub struct WriteBuildFileContents {
/// The name or id of the target Build.
#[serde(alias = "id", alias = "name")]
pub build: String,
/// The dockerfile contents to write.
pub contents: String,
}
//
/// Trigger a refresh of the cached latest hash and message.
#[typeshare]
#[derive(
-1
View File
@@ -97,7 +97,6 @@ pub struct UpdateBuilder {
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
#[error(serror::Error)]
pub struct RenameBuilder {
/// The id or name of the Builder to rename.
pub id: String,
@@ -122,7 +122,6 @@ pub struct UpdateDeployment {
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
#[error(serror::Error)]
pub struct RenameDeployment {
/// The id of the deployment to rename.
pub id: String,
@@ -109,7 +109,6 @@ pub type UpdateProcedureResponse = Procedure;
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
#[error(serror::Error)]
pub struct RenameProcedure {
/// The id or name of the Procedure to rename.
pub id: String,
-1
View File
@@ -101,7 +101,6 @@ pub struct UpdateRepo {
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
#[error(serror::Error)]
pub struct RenameRepo {
/// The id or name of the Repo to rename.
pub id: String,
-2
View File
@@ -79,7 +79,6 @@ pub struct UpdateServer {
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
#[error(serror::Error)]
pub struct RenameServer {
/// The id or name of the Server to rename.
pub id: String,
@@ -100,7 +99,6 @@ pub struct RenameServer {
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
#[error(serror::Error)]
pub struct CreateNetwork {
/// Server Id or name
pub server: String,
@@ -97,7 +97,6 @@ pub struct UpdateServerTemplate {
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
#[error(serror::Error)]
pub struct RenameServerTemplate {
/// The id or name of the ServerTemplate to rename.
pub id: String,
-2
View File
@@ -100,7 +100,6 @@ pub struct UpdateStack {
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
#[error(serror::Error)]
pub struct RenameStack {
/// The id of the stack to rename.
pub id: String,
@@ -118,7 +117,6 @@ pub struct RenameStack {
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
#[error(serror::Error)]
pub struct WriteStackFileContents {
/// The name or id of the target Stack.
#[serde(alias = "id", alias = "name")]
-3
View File
@@ -99,7 +99,6 @@ pub struct UpdateResourceSync {
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
#[error(serror::Error)]
pub struct RenameResourceSync {
/// The id or name of the ResourceSync to rename.
pub id: String,
@@ -133,7 +132,6 @@ pub struct RefreshResourceSyncPending {
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
#[error(serror::Error)]
pub struct WriteSyncFileContents {
/// The name or id of the target Sync.
#[serde(alias = "id", alias = "name")]
@@ -166,7 +164,6 @@ pub struct WriteSyncFileContents {
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
#[error(serror::Error)]
pub struct CommitSync {
/// Id or name
#[serde(alias = "id", alias = "name")]
+1
View File
@@ -63,6 +63,7 @@ pub type _PartialActionConfig = PartialActionConfig;
#[partial(skip_serializing_none, from, diff)]
pub struct ActionConfig {
/// Typescript file contents using pre-initialized `komodo` client.
/// Supports variable / secret interpolation.
#[serde(default, deserialize_with = "file_contents_deserializer")]
#[partial_attr(serde(
default,
+28
View File
@@ -111,6 +111,9 @@ pub enum AlerterEndpoint {
/// Send alert to a Discord app
Discord(DiscordAlerterEndpoint),
/// Send alert to Ntfy
Ntfy(NtfyAlerterEndpoint),
}
impl Default for AlerterEndpoint {
@@ -195,6 +198,31 @@ fn default_discord_url() -> String {
)
}
/// Configuration for a Ntfy alerter.
#[typeshare]
#[derive(
Debug, Clone, PartialEq, Serialize, Deserialize, Builder,
)]
pub struct NtfyAlerterEndpoint {
/// The ntfy topic URL
#[serde(default = "default_ntfy_url")]
#[builder(default = "default_ntfy_url()")]
pub url: String,
}
impl Default for NtfyAlerterEndpoint {
fn default() -> Self {
Self {
url: default_ntfy_url(),
}
}
}
fn default_ntfy_url() -> String {
String::from("http://localhost:8080/komodo")
}
// QUERY
#[typeshare]
+47 -11
View File
@@ -32,26 +32,31 @@ pub type BuildListItem = ResourceListItem<BuildListItemInfo>;
#[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuildListItemInfo {
/// State of the build. Reflects whether most recent build successful.
pub state: BuildState,
/// Unix timestamp in milliseconds of last build
pub last_built_at: I64,
/// The current version of the build
pub version: Version,
/// The builder attached to build.
pub builder_id: String,
/// Whether build is in files on host mode.
pub files_on_host: bool,
/// The git provider domain
pub git_provider: String,
/// The image registry domain
pub image_registry_domain: String,
pub git_provider: Option<String>,
/// The repo used as the source of the build
pub repo: String,
pub repo: Option<String>,
/// The branch of the repo
pub branch: String,
/// State of the build. Reflects whether most recent build successful.
pub state: BuildState,
pub branch: Option<String>,
/// Latest built short commit hash, or null.
pub built_hash: Option<String>,
/// Latest short commit hash, or null. Only for repo based stacks
pub latest_hash: Option<String>,
/// The image registry domain
pub image_registry_domain: Option<String>,
}
#[typeshare]
@@ -73,11 +78,26 @@ pub enum BuildState {
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct BuildInfo {
/// The timestamp build was last built.
pub last_built_at: I64,
/// Latest built short commit hash, or null.
pub built_hash: Option<String>,
/// Latest built commit message, or null. Only for repo based stacks
pub built_message: Option<String>,
/// The last built dockerfile contents.
/// This is updated whenever Komodo successfully runs the build.
pub built_contents: Option<String>,
/// The absolute path to the file
pub remote_path: Option<String>,
/// The remote dockerfile contents, whether on host or in repo.
/// This is updated whenever Komodo refreshes the build cache.
/// It will be empty if the dockerfile is defined directly in the build config.
pub remote_contents: Option<String>,
/// If there was an error in getting the remote contents, it will be here.
pub remote_error: Option<String>,
/// Latest remote short commit hash, or null.
pub latest_hash: Option<String>,
/// Latest remote commit message, or null
@@ -121,8 +141,8 @@ pub struct BuildConfig {
#[builder(default)]
pub image_name: String,
/// An extra tag put before the build version, for the image pushed to the repository.
/// Eg. in image tag of `aarch64` would push to moghtech/komodo:1.13.2-aarch64.
/// An extra tag put after the build version, for the image pushed to the repository.
/// Eg. in image tag of `aarch64` would push to moghtech/komodo-core:1.13.2-aarch64.
/// If this is empty, the image tag will just be the build version.
///
/// Can be used in conjunction with `image_name` to direct multiple builds
@@ -192,10 +212,13 @@ pub struct BuildConfig {
#[builder(default)]
pub webhook_secret: String,
/// The optional command run after repo clone and before docker build.
/// If this is checked, the build will source the files on the host.
/// Use `build_path` and `dockerfile_path` to specify the path on the host.
/// This is useful for those who wish to setup their files on the host,
/// rather than defining the contents in UI or in a git repo.
#[serde(default)]
#[builder(default)]
pub pre_build: SystemCommand,
pub files_on_host: bool,
/// The path of the docker build context relative to the root of the repo.
/// Default: "." (the root of the repo).
@@ -238,6 +261,17 @@ pub struct BuildConfig {
#[builder(default)]
pub extra_args: Vec<String>,
/// The optional command run after repo clone and before docker build.
#[serde(default)]
#[builder(default)]
pub pre_build: SystemCommand,
/// UI defined dockerfile contents.
/// Supports variable / secret interpolation.
#[serde(default)]
#[builder(default)]
pub dockerfile: String,
/// Docker build arguments.
///
/// These values are visible in the final image by running `docker inspect`.
@@ -338,6 +372,8 @@ impl Default for BuildConfig {
image_registry: Default::default(),
webhook_enabled: default_webhook_enabled(),
webhook_secret: Default::default(),
dockerfile: Default::default(),
files_on_host: Default::default(),
}
}
}
@@ -44,6 +44,8 @@ pub struct Env {
pub komodo_host: Option<String>,
/// Override `port`
pub komodo_port: Option<u16>,
/// Override `bind_ip`
pub komodo_bind_ip: Option<String>,
/// Override `passkey`
pub komodo_passkey: Option<String>,
/// Override `passkey` with file
@@ -256,6 +258,11 @@ pub struct CoreConfig {
#[serde(default = "default_core_port")]
pub port: u16,
/// IP address the core server binds to.
/// Default: [::].
#[serde(default = "default_core_bind_ip")]
pub bind_ip: String,
/// Sent in auth header with req to periphery.
/// Should be some secure hash, maybe 20-40 chars.
pub passkey: String,
@@ -521,6 +528,10 @@ fn default_core_port() -> u16 {
9120
}
fn default_core_bind_ip() -> String {
"[::]".to_string()
}
fn default_frontend_path() -> String {
"/app/frontend".to_string()
}
@@ -571,6 +582,7 @@ impl CoreConfig {
title: config.title,
host: config.host,
port: config.port,
bind_ip: config.bind_ip,
passkey: empty_or_redacted(&config.passkey),
first_server: config.first_server,
frontend_path: config.frontend_path,
@@ -114,10 +114,14 @@ pub struct Env {
/// Override `port`
pub periphery_port: Option<u16>,
/// Override `bind_ip`
pub periphery_bind_ip: Option<String>,
/// Override `repo_dir`
pub periphery_repo_dir: Option<PathBuf>,
/// Override `stack_dir`
pub periphery_stack_dir: Option<PathBuf>,
/// Override `build_dir`
pub periphery_build_dir: Option<PathBuf>,
/// Override `stats_polling_rate`
pub periphery_stats_polling_rate: Option<Timelength>,
/// Override `legacy_compose_cli`
@@ -162,6 +166,11 @@ pub struct PeripheryConfig {
#[serde(default = "default_periphery_port")]
pub port: u16,
/// IP address the periphery server binds to.
/// Default: [::].
#[serde(default = "default_periphery_bind_ip")]
pub bind_ip: String,
/// The system directory where Komodo managed repos will be cloned.
/// Default: `/etc/komodo/repos`
#[serde(default = "default_repo_dir")]
@@ -172,6 +181,11 @@ pub struct PeripheryConfig {
#[serde(default = "default_stack_dir")]
pub stack_dir: PathBuf,
/// The system directory where builds will managed.
/// Default: `/etc/komodo/builds`
#[serde(default = "default_build_dir")]
pub build_dir: PathBuf,
/// The rate at which the system stats will be polled to update the cache.
/// Default: `5-sec`
#[serde(default = "default_stats_polling_rate")]
@@ -244,6 +258,10 @@ fn default_periphery_port() -> u16 {
8120
}
fn default_periphery_bind_ip() -> String {
"[::]".to_string()
}
fn default_repo_dir() -> PathBuf {
"/etc/komodo/repos".parse().unwrap()
}
@@ -252,6 +270,10 @@ fn default_stack_dir() -> PathBuf {
"/etc/komodo/stacks".parse().unwrap()
}
fn default_build_dir() -> PathBuf {
"/etc/komodo/builds".parse().unwrap()
}
fn default_stats_polling_rate() -> Timelength {
Timelength::FiveSeconds
}
@@ -272,8 +294,10 @@ impl Default for PeripheryConfig {
fn default() -> Self {
Self {
port: default_periphery_port(),
bind_ip: default_periphery_bind_ip(),
repo_dir: default_repo_dir(),
stack_dir: default_stack_dir(),
build_dir: default_build_dir(),
stats_polling_rate: default_stats_polling_rate(),
legacy_compose_cli: Default::default(),
logging: Default::default(),
@@ -295,8 +319,10 @@ impl PeripheryConfig {
pub fn sanitized(&self) -> PeripheryConfig {
PeripheryConfig {
port: self.port,
bind_ip: self.bind_ip.clone(),
repo_dir: self.repo_dir.clone(),
stack_dir: self.stack_dir.clone(),
build_dir: self.build_dir.clone(),
stats_polling_rate: self.stats_polling_rate,
legacy_compose_cli: self.legacy_compose_cli,
logging: self.logging.clone(),
+11 -3
View File
@@ -118,11 +118,12 @@ pub fn all_logs_success(logs: &[update::Log]) -> bool {
true
}
pub fn optional_string(string: &str) -> Option<String> {
pub fn optional_string(string: impl Into<String>) -> Option<String> {
let string = string.into();
if string.is_empty() {
None
} else {
Some(string.to_string())
Some(string)
}
}
@@ -394,6 +395,8 @@ pub struct CloneArgs {
pub branch: String,
/// Specific commit hash. Optional
pub commit: Option<String>,
/// Use PERIPHERY_BUILD_DIR as the parent folder for the clone.
pub is_build: bool,
/// The clone destination path
pub destination: Option<String>,
/// Command to run after the repo has been cloned
@@ -459,8 +462,9 @@ impl From<&self::build::Build> for CloneArgs {
branch: optional_string(&build.config.branch)
.unwrap_or_else(|| String::from("main")),
commit: optional_string(&build.config.commit),
is_build: true,
destination: None,
on_clone: build.config.pre_build.clone().into_option(),
on_clone: None,
on_pull: None,
https: build.config.git_https,
account: optional_string(&build.config.git_account),
@@ -478,6 +482,7 @@ impl From<&self::repo::Repo> for CloneArgs {
branch: optional_string(&repo.config.branch)
.unwrap_or_else(|| String::from("main")),
commit: optional_string(&repo.config.commit),
is_build: false,
destination: optional_string(&repo.config.path),
on_clone: repo.config.on_clone.clone().into_option(),
on_pull: repo.config.on_pull.clone().into_option(),
@@ -497,6 +502,7 @@ impl From<&self::sync::ResourceSync> for CloneArgs {
branch: optional_string(&sync.config.branch)
.unwrap_or_else(|| String::from("main")),
commit: optional_string(&sync.config.commit),
is_build: false,
destination: None,
on_clone: None,
on_pull: None,
@@ -516,6 +522,7 @@ impl From<&self::stack::Stack> for CloneArgs {
branch: optional_string(&stack.config.branch)
.unwrap_or_else(|| String::from("main")),
commit: optional_string(&stack.config.commit),
is_build: false,
destination: None,
on_clone: None,
on_pull: None,
@@ -713,6 +720,7 @@ pub enum Operation {
DeleteBuild,
RunBuild,
CancelBuild,
WriteDockerfile,
// repo
CreateRepo,
+15
View File
@@ -62,6 +62,21 @@ pub struct Resource<Config: Default, Info: Default = ()> {
pub base_permission: PermissionLevel,
}
impl<C: Default, I: Default> Default for Resource<C, I> {
fn default() -> Self {
Self {
id: String::new(),
name: String::from("temp-resource"),
description: String::new(),
updated_at: 0,
tags: Vec::new(),
info: I::default(),
config: C::default(),
base_permission: PermissionLevel::None,
}
}
}
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ResourceListItem<Info> {
+8
View File
@@ -77,6 +77,13 @@ pub struct ServerConfig {
#[partial_default(default_timeout_seconds())]
pub timeout_seconds: I64,
/// An optional override passkey to use
/// to authenticate with periphery agent.
/// If this is empty, will use passkey in core config.
#[serde(default)]
#[builder(default)]
pub passkey: String,
/// Sometimes the system stats reports a mount path that is not desired.
/// Use this field to filter it out from the report.
#[serde(default, deserialize_with = "string_list_deserializer")]
@@ -240,6 +247,7 @@ impl Default for ServerConfig {
send_mem_alerts: default_send_alerts(),
send_disk_alerts: default_send_alerts(),
region: Default::default(),
passkey: Default::default(),
cpu_warning: default_cpu_warning(),
cpu_critical: default_cpu_critical(),
mem_warning: default_mem_warning(),
+42 -41
View File
@@ -269,47 +269,6 @@ pub struct StackConfig {
#[builder(default)]
pub skip_secret_interp: bool,
/// If this is checked, the stack will source the files on the host.
/// Use `run_directory` and `file_paths` to specify the path on the host.
/// This is useful for those who wish to setup their files on the host using SSH or similar,
/// rather than defining the contents in UI or in a git repo.
#[serde(default)]
#[builder(default)]
pub files_on_host: bool,
/// Directory to change to (`cd`) before running `docker compose up -d`.
#[serde(default)]
#[builder(default)]
pub run_directory: String,
/// Add paths to compose files, relative to the run path.
/// If this is empty, will use file `compose.yaml`.
#[serde(default, deserialize_with = "string_list_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_string_list_deserializer"
))]
#[builder(default)]
pub file_paths: Vec<String>,
/// The name of the written environment file before `docker compose up`.
/// Relative to the run directory root.
/// Default: .env
#[serde(default = "default_env_file_path")]
#[builder(default = "default_env_file_path()")]
#[partial_default(default_env_file_path())]
pub env_file_path: String,
/// Add additional env files to attach with `--env-file`.
/// Relative to the run directory root.
#[serde(default, deserialize_with = "string_list_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_string_list_deserializer"
))]
#[builder(default)]
pub additional_env_files: Vec<String>,
/// The git provider domain. Default: github.com
#[serde(default = "default_git_provider")]
#[builder(default = "default_git_provider()")]
@@ -373,6 +332,47 @@ pub struct StackConfig {
#[builder(default)]
pub webhook_force_deploy: bool,
/// If this is checked, the stack will source the files on the host.
/// Use `run_directory` and `file_paths` to specify the path on the host.
/// This is useful for those who wish to setup their files on the host,
/// rather than defining the contents in UI or in a git repo.
#[serde(default)]
#[builder(default)]
pub files_on_host: bool,
/// Directory to change to (`cd`) before running `docker compose up -d`.
#[serde(default)]
#[builder(default)]
pub run_directory: String,
/// Add paths to compose files, relative to the run path.
/// If this is empty, will use file `compose.yaml`.
#[serde(default, deserialize_with = "string_list_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_string_list_deserializer"
))]
#[builder(default)]
pub file_paths: Vec<String>,
/// The name of the written environment file before `docker compose up`.
/// Relative to the run directory root.
/// Default: .env
#[serde(default = "default_env_file_path")]
#[builder(default = "default_env_file_path()")]
#[partial_default(default_env_file_path())]
pub env_file_path: String,
/// Add additional env files to attach with `--env-file`.
/// Relative to the run directory root.
#[serde(default, deserialize_with = "string_list_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_string_list_deserializer"
))]
#[builder(default)]
pub additional_env_files: Vec<String>,
/// Whether to send StackStateChange alerts for this stack.
#[serde(default = "default_send_alerts")]
#[builder(default = "default_send_alerts()")]
@@ -434,6 +434,7 @@ pub struct StackConfig {
/// The contents of the file directly, for management in the UI.
/// If this is empty, it will fall back to checking git config for
/// repo based compose file.
/// Supports variable / secret interpolation.
#[serde(default, deserialize_with = "file_contents_deserializer")]
#[partial_attr(serde(
default,
+1 -1
View File
@@ -1,5 +1,5 @@
//! # Komodo
//! *A system to build and deploy software across many servers*
//! *A system to build and deploy software across many servers*. [**https://komo.do**](https://komo.do)
//!
//! This is a client library for the Komodo Core API.
//! It contains:
+2 -2
View File
@@ -1,6 +1,6 @@
# Komodo
_A system to build and deploy software across many servers_
_A system to build and deploy software across many servers_. [https://komo.do](https://komo.do)
```sh
npm install komodo_client
@@ -18,7 +18,7 @@ import { KomodoClient, Types } from "komodo_client";
const komodo = KomodoClient("https://demo.komo.do", {
type: "api-key",
params: {
api_key: "your_key",
key: "your_key",
secret: "your secret",
},
});
+2 -1
View File
@@ -1,9 +1,10 @@
{
"name": "komodo_client",
"version": "1.16.13",
"version": "1.17.1",
"description": "Komodo client package",
"homepage": "https://komo.do",
"main": "dist/lib.js",
"type": "module",
"license": "GPL-3.0",
"publishConfig": {
"access": "public"
+3
View File
@@ -220,6 +220,9 @@ export function KomodoClient(url: string, options: InitOptions) {
// Sleep for a bit before checking for websocket closed
await new Promise((resolve) => setTimeout(resolve, 500));
}
// Sleep for a bit before retrying connection to avoid spam.
await new Promise((resolve) => setTimeout(resolve, retry_timeout_ms));
} catch (error) {
console.error(error);
// Sleep for a bit before retrying, maybe Komodo Core is down temporarily.
+2
View File
@@ -61,6 +61,7 @@ export type ReadResponses = {
GetServer: Types.GetServerResponse;
GetServerState: Types.GetServerStateResponse;
GetPeripheryVersion: Types.GetPeripheryVersionResponse;
GetDockerContainersSummary: Types.GetDockerContainersSummaryResponse;
ListDockerContainers: Types.ListDockerContainersResponse;
ListAllDockerContainers: Types.ListAllDockerContainersResponse;
InspectDockerContainer: Types.InspectDockerContainerResponse;
@@ -227,6 +228,7 @@ export type WriteResponses = {
DeleteBuild: Types.Build;
UpdateBuild: Types.Build;
RenameBuild: Types.Update;
WriteBuildFileContents: Types.Update;
RefreshBuildCache: Types.NoData;
CreateBuildWebhook: Types.CreateBuildWebhookResponse;
DeleteBuildWebhook: Types.DeleteBuildWebhookResponse;
+125 -43
View File
@@ -52,7 +52,10 @@ export interface Resource<Config, Info> {
}
export interface ActionConfig {
/** Typescript file contents using pre-initialized `komodo` client. */
/**
* Typescript file contents using pre-initialized `komodo` client.
* Supports variable / secret interpolation.
*/
file_contents?: string;
/** Whether incoming webhooks actually trigger action. */
webhook_enabled: boolean;
@@ -130,7 +133,9 @@ export type AlerterEndpoint =
/** Send alert to a Slack app */
| { type: "Slack", params: SlackAlerterEndpoint }
/** Send alert to a Discord app */
| { type: "Discord", params: DiscordAlerterEndpoint };
| { type: "Discord", params: DiscordAlerterEndpoint }
/** Send alert to Ntfy */
| { type: "Ntfy", params: NtfyAlerterEndpoint };
/** Used to reference a specific resource across all resource types */
export type ResourceTarget =
@@ -210,11 +215,6 @@ export interface Version {
patch: number;
}
export interface SystemCommand {
path?: string;
command?: string;
}
/** Configuration for an image registry */
export interface ImageRegistryConfig {
/**
@@ -231,6 +231,11 @@ export interface ImageRegistryConfig {
organization?: string;
}
export interface SystemCommand {
path?: string;
command?: string;
}
/** The build configuration. */
export interface BuildConfig {
/** Which builder is used to build the image. */
@@ -252,8 +257,8 @@ export interface BuildConfig {
*/
image_name?: string;
/**
* An extra tag put before the build version, for the image pushed to the repository.
* Eg. in image tag of `aarch64` would push to moghtech/komodo:1.13.2-aarch64.
* An extra tag put after the build version, for the image pushed to the repository.
* Eg. in image tag of `aarch64` would push to moghtech/komodo-core:1.13.2-aarch64.
* If this is empty, the image tag will just be the build version.
*
* Can be used in conjunction with `image_name` to direct multiple builds
@@ -292,8 +297,13 @@ export interface BuildConfig {
* If its an empty string, use the default secret from the config.
*/
webhook_secret?: string;
/** The optional command run after repo clone and before docker build. */
pre_build?: SystemCommand;
/**
* If this is checked, the build will source the files on the host.
* Use `build_path` and `dockerfile_path` to specify the path on the host.
* This is useful for those who wish to setup their files on the host,
* rather than defining the contents in UI or in a git repo.
*/
files_on_host?: boolean;
/**
* The path of the docker build context relative to the root of the repo.
* Default: "." (the root of the repo).
@@ -309,6 +319,13 @@ export interface BuildConfig {
use_buildx?: boolean;
/** Any extra docker cli arguments to be included in the build command */
extra_args?: string[];
/** The optional command run after repo clone and before docker build. */
pre_build?: SystemCommand;
/**
* UI defined dockerfile contents.
* Supports variable / secret interpolation.
*/
dockerfile?: string;
/**
* Docker build arguments.
*
@@ -333,11 +350,27 @@ export interface BuildConfig {
}
export interface BuildInfo {
/** The timestamp build was last built. */
last_built_at: I64;
/** Latest built short commit hash, or null. */
built_hash?: string;
/** Latest built commit message, or null. Only for repo based stacks */
built_message?: string;
/**
* The last built dockerfile contents.
* This is updated whenever Komodo successfully runs the build.
*/
built_contents?: string;
/** The absolute path to the file */
remote_path?: string;
/**
* The remote dockerfile contents, whether on host or in repo.
* This is updated whenever Komodo refreshes the build cache.
* It will be empty if the dockerfile is defined directly in the build config.
*/
remote_contents?: string;
/** If there was an error in getting the remote contents, it will be here. */
remote_error?: string;
/** Latest remote short commit hash, or null. */
latest_hash?: string;
/** Latest remote commit message, or null */
@@ -358,26 +391,28 @@ export enum BuildState {
}
export interface BuildListItemInfo {
/** State of the build. Reflects whether most recent build successful. */
state: BuildState;
/** Unix timestamp in milliseconds of last build */
last_built_at: I64;
/** The current version of the build */
version: Version;
/** The builder attached to build. */
builder_id: string;
/** Whether build is in files on host mode. */
files_on_host: boolean;
/** The git provider domain */
git_provider: string;
/** The image registry domain */
image_registry_domain: string;
git_provider?: string;
/** The repo used as the source of the build */
repo: string;
repo?: string;
/** The branch of the repo */
branch: string;
/** State of the build. Reflects whether most recent build successful. */
state: BuildState;
branch?: string;
/** Latest built short commit hash, or null. */
built_hash?: string;
/** Latest short commit hash, or null. Only for repo based stacks */
latest_hash?: string;
/** The image registry domain */
image_registry_domain?: string;
}
export type BuildListItem = ResourceListItem<BuildListItemInfo>;
@@ -1520,6 +1555,12 @@ export interface ServerConfig {
* default: 2
*/
timeout_seconds: I64;
/**
* An optional override passkey to use
* to authenticate with periphery agent.
* If this is empty, will use passkey in core config.
*/
passkey?: string;
/**
* Sometimes the system stats reports a mount path that is not desired.
* Use this field to filter it out from the report.
@@ -1633,31 +1674,6 @@ export interface StackConfig {
destroy_before_deploy?: boolean;
/** Whether to skip secret interpolation into the stack environment variables. */
skip_secret_interp?: boolean;
/**
* If this is checked, the stack will source the files on the host.
* Use `run_directory` and `file_paths` to specify the path on the host.
* This is useful for those who wish to setup their files on the host using SSH or similar,
* rather than defining the contents in UI or in a git repo.
*/
files_on_host?: boolean;
/** Directory to change to (`cd`) before running `docker compose up -d`. */
run_directory?: string;
/**
* Add paths to compose files, relative to the run path.
* If this is empty, will use file `compose.yaml`.
*/
file_paths?: string[];
/**
* The name of the written environment file before `docker compose up`.
* Relative to the run directory root.
* Default: .env
*/
env_file_path: string;
/**
* Add additional env files to attach with `--env-file`.
* Relative to the run directory root.
*/
additional_env_files?: string[];
/** The git provider domain. Default: github.com */
git_provider: string;
/**
@@ -1697,6 +1713,31 @@ export interface StackConfig {
* If this option is enabled, will always run `DeployStack` without diffing.
*/
webhook_force_deploy?: boolean;
/**
* If this is checked, the stack will source the files on the host.
* Use `run_directory` and `file_paths` to specify the path on the host.
* This is useful for those who wish to setup their files on the host,
* rather than defining the contents in UI or in a git repo.
*/
files_on_host?: boolean;
/** Directory to change to (`cd`) before running `docker compose up -d`. */
run_directory?: string;
/**
* Add paths to compose files, relative to the run path.
* If this is empty, will use file `compose.yaml`.
*/
file_paths?: string[];
/**
* The name of the written environment file before `docker compose up`.
* Relative to the run directory root.
* Default: .env
*/
env_file_path: string;
/**
* Add additional env files to attach with `--env-file`.
* Relative to the run directory root.
*/
additional_env_files?: string[];
/** Whether to send StackStateChange alerts for this stack. */
send_alerts: boolean;
/** Used with `registry_account` to login to a registry before docker compose up. */
@@ -1728,6 +1769,7 @@ export interface StackConfig {
* The contents of the file directly, for management in the UI.
* If this is empty, it will fall back to checking git config for
* repo based compose file.
* Supports variable / secret interpolation.
*/
file_contents?: string;
/**
@@ -2053,6 +2095,7 @@ export enum Operation {
DeleteBuild = "DeleteBuild",
RunBuild = "RunBuild",
CancelBuild = "CancelBuild",
WriteDockerfile = "WriteDockerfile",
CreateRepo = "CreateRepo",
UpdateRepo = "UpdateRepo",
RenameRepo = "RenameRepo",
@@ -4007,6 +4050,8 @@ export interface CloneArgs {
branch: string;
/** Specific commit hash. Optional */
commit?: string;
/** Use PERIPHERY_BUILD_DIR as the parent folder for the clone. */
is_build: boolean;
/** The clone destination path */
destination?: string;
/** Command to run after the repo has been cloned */
@@ -5169,6 +5214,27 @@ export interface GetDeploymentsSummaryResponse {
unknown: I64;
}
/**
* Gets a summary of data relating to all containers.
* Response: [GetDockerContainersSummaryResponse].
*/
export interface GetDockerContainersSummary {
}
/** Response for [GetDockerContainersSummary] */
export interface GetDockerContainersSummaryResponse {
/** The total number of Containers */
total: number;
/** The number of Containers with Running state */
running: number;
/** The number of Containers with Stopped or Paused or Created state */
stopped: number;
/** The number of Containers with Restarting or Dead state */
unhealthy: number;
/** The number of Containers with Unknown state */
unknown: number;
}
/**
* Get a specific docker registry account.
* Response: [GetDockerRegistryAccountResponse].
@@ -6354,6 +6420,12 @@ export interface NameAndId {
id: string;
}
/** Configuration for a Ntfy alerter. */
export interface NtfyAlerterEndpoint {
/** The ntfy topic URL */
url: string;
}
/** Pauses all containers on the target server. Response: [Update] */
export interface PauseAllContainers {
/** Name or id */
@@ -7498,6 +7570,14 @@ export interface UrlBuilderConfig {
passkey?: string;
}
/** Update dockerfile contents in Files on Server or Git Repo mode. Response: [Update]. */
export interface WriteBuildFileContents {
/** The name or id of the target Build. */
build: string;
/** The dockerfile contents to write. */
contents: string;
}
/** Update file contents in Files on Server or Git Repo mode. Response: [Update]. */
export interface WriteStackFileContents {
/** The name or id of the target Stack. */
@@ -7648,6 +7728,7 @@ export type ReadRequest =
| { type: "InspectDockerImage", params: InspectDockerImage }
| { type: "ListDockerImageHistory", params: ListDockerImageHistory }
| { type: "InspectDockerVolume", params: InspectDockerVolume }
| { type: "GetDockerContainersSummary", params: GetDockerContainersSummary }
| { type: "ListAllDockerContainers", params: ListAllDockerContainers }
| { type: "ListDockerContainers", params: ListDockerContainers }
| { type: "ListDockerNetworks", params: ListDockerNetworks }
@@ -7763,6 +7844,7 @@ export type WriteRequest =
| { type: "DeleteBuild", params: DeleteBuild }
| { type: "UpdateBuild", params: UpdateBuild }
| { type: "RenameBuild", params: RenameBuild }
| { type: "WriteBuildFileContents", params: WriteBuildFileContents }
| { type: "RefreshBuildCache", params: RefreshBuildCache }
| { type: "CreateBuildWebhook", params: CreateBuildWebhook }
| { type: "DeleteBuildWebhook", params: DeleteBuildWebhook }
+37 -1
View File
@@ -1,4 +1,4 @@
use komodo_client::entities::update::Log;
use komodo_client::entities::{FileContents, update::Log};
use resolver_api::Resolve;
use serde::{Deserialize, Serialize};
@@ -21,6 +21,42 @@ pub type BuildResponse = Vec<Log>;
//
/// Get the dockerfile contents on the host, for builds using
/// `files_on_host`.
#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]
#[response(GetDockerfileContentsOnHostResponse)]
#[error(serror::Error)]
pub struct GetDockerfileContentsOnHost {
/// The name of the build
pub name: String,
/// The build path for the build.
pub build_path: String,
/// The dockerfile path for the build, relative to the build_path
pub dockerfile_path: String,
}
pub type GetDockerfileContentsOnHostResponse = FileContents;
//
/// Write the dockerfile contents to the file on the host, for build using
/// `files_on_host`.
#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]
#[response(Log)]
#[error(serror::Error)]
pub struct WriteDockerfileContentsToHost {
/// The name of the build
pub name: String,
/// The build path for the build.
pub build_path: String,
/// The dockerfile path for the build, relative to the build_path
pub dockerfile_path: String,
/// The contents to write.
pub contents: String,
}
//
#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]
#[response(Log)]
#[error(serror::Error)]
+2
View File
@@ -106,4 +106,6 @@ pub struct RenameRepo {
#[error(serror::Error)]
pub struct DeleteRepo {
pub name: String,
/// Clears
pub is_build: bool,
}
+7 -1
View File
@@ -122,13 +122,19 @@ PERIPHERY_PASSKEYS=${KOMODO_PASSKEY}
## Specify the root directory used by Periphery agent.
PERIPHERY_ROOT_DIRECTORY=/etc/komodo
PERIPHERY_REPO_DIR=$PERIPHERY_ROOT_DIRECTORY/repos
PERIPHERY_STACK_DIR=$PERIPHERY_ROOT_DIRECTORY/stacks
PERIPHERY_BUILD_DIR=$PERIPHERY_ROOT_DIRECTORY/builds
## Enable SSL using self signed certificates.
## Connect to Periphery at https://address:8120.
PERIPHERY_SSL_ENABLED=true
PERIPHERY_SSL_KEY_FILE=$PERIPHERY_ROOT_DIRECTORY/ssl/key.pem
PERIPHERY_SSL_CERT_FILE=$PERIPHERY_ROOT_DIRECTORY/ssl/cert.pem
## If the disk size is overreporting, can use one of these to
## whitelist / blacklist the disks to filter them, whichever is easier.
## Accepts comma separated list of paths.
## Usually whitelisting just /etc/hostname gives correct size.
PERIPHERY_INCLUDE_DISK_MOUNTS=/etc/hostname
# PERIPHERY_EXCLUDE_DISK_MOUNTS=/snap,/etc/repos
# PERIPHERY_EXCLUDE_DISK_MOUNTS=/snap,/etc/repos
+3 -17
View File
@@ -16,8 +16,6 @@ services:
restart: unless-stopped
logging:
driver: ${COMPOSE_LOGGING_DRIVER:-local}
networks:
- default
# ports:
# - 27017:27017
volumes:
@@ -28,7 +26,7 @@ services:
MONGO_INITDB_ROOT_PASSWORD: ${KOMODO_DB_PASSWORD}
core:
image: ghcr.io/moghtech/komodo:${COMPOSE_KOMODO_IMAGE_TAG:-latest}
image: ghcr.io/moghtech/komodo-core:${COMPOSE_KOMODO_IMAGE_TAG:-latest}
labels:
komodo.skip: # Prevent Komodo from stopping with StopAllContainers
restart: unless-stopped
@@ -36,8 +34,6 @@ services:
- mongo
logging:
driver: ${COMPOSE_LOGGING_DRIVER:-local}
networks:
- default
ports:
- 9120:9120
env_file: ./compose.env
@@ -61,20 +57,13 @@ services:
## or deploy the Periphery binary with systemd using
## https://github.com/moghtech/komodo/tree/main/scripts
periphery:
image: ghcr.io/moghtech/periphery:${COMPOSE_KOMODO_IMAGE_TAG:-latest}
image: ghcr.io/moghtech/komodo-periphery:${COMPOSE_KOMODO_IMAGE_TAG:-latest}
labels:
komodo.skip: # Prevent Komodo from stopping with StopAllContainers
restart: unless-stopped
logging:
driver: ${COMPOSE_LOGGING_DRIVER:-local}
networks:
- default
env_file: ./compose.env
environment:
PERIPHERY_REPO_DIR: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/repos
PERIPHERY_STACK_DIR: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/stacks
PERIPHERY_SSL_KEY_FILE: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/ssl/key.pem
PERIPHERY_SSL_CERT_FILE: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/ssl/cert.pem
volumes:
## Mount external docker socket
- /var/run/docker.sock:/var/run/docker.sock
@@ -91,7 +80,4 @@ volumes:
mongo-data:
mongo-config:
# Core
repo-cache:
networks:
default: {}
repo-cache:
+46
View File
@@ -0,0 +1,46 @@
####################################
# 🦎 KOMODO COMPOSE - PERIPHERY 🦎 #
####################################
## This compose file will deploy:
## 1. Komodo Periphery
services:
periphery:
image: ghcr.io/moghtech/komodo-periphery:${COMPOSE_KOMODO_IMAGE_TAG:-latest}
labels:
komodo.skip: # Prevent Komodo from stopping with StopAllContainers
restart: unless-stopped
logging:
driver: ${COMPOSE_LOGGING_DRIVER:-local}
## https://komo.do/docs/connect-servers#configuration
environment:
PERIPHERY_REPO_DIR: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/repos
PERIPHERY_STACK_DIR: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/stacks
PERIPHERY_BUILD_DIR: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/builds
PERIPHERY_SSL_KEY_FILE: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/ssl/key.pem
PERIPHERY_SSL_CERT_FILE: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/ssl/cert.pem
## Pass the same passkey as used by the Komodo Core connecting to this Periphery agent.
PERIPHERY_PASSKEYS: abc123
## If the disk size is overreporting, can use one of these to
## whitelist / blacklist the disks to filter them, whichever is easier.
## Accepts comma separated list of paths.
## Usually whitelisting just /etc/hostname gives correct size for single root disk.
PERIPHERY_INCLUDE_DISK_MOUNTS: /etc/hostname
# PERIPHERY_EXCLUDE_DISK_MOUNTS: /snap,/etc/repos
volumes:
## Mount external docker socket
- /var/run/docker.sock:/var/run/docker.sock
## Allow Periphery to see processes outside of container
- /proc:/proc
## Specify the Periphery agent root directory.
## Must be the same inside and outside the container,
## or docker will get confused. See https://github.com/moghtech/komodo/discussions/180.
## Default: /etc/komodo.
- ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}:${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}
## If periphery is being run remote from the core server, ports need
## to be exposed on the host.
# ports:
# - 8120:8120
## If you want to use a custom periphery config file, use command to pass it to periphery.
# command: periphery --config-path ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/periphery.config.toml
+3 -19
View File
@@ -15,8 +15,6 @@ services:
restart: unless-stopped
logging:
driver: ${COMPOSE_LOGGING_DRIVER:-local}
networks:
- default
# ports:
# - 5432:5432
volumes:
@@ -35,15 +33,13 @@ services:
- postgres
logging:
driver: ${COMPOSE_LOGGING_DRIVER:-local}
networks:
- default
# ports:
# - 27017:27017
environment:
- FERRETDB_POSTGRESQL_URL=postgres://postgres:5432/${KOMODO_DATABASE_DB_NAME:-komodo}
core:
image: ghcr.io/moghtech/komodo:${COMPOSE_KOMODO_IMAGE_TAG:-latest}
image: ghcr.io/moghtech/komodo-core:${COMPOSE_KOMODO_IMAGE_TAG:-latest}
labels:
komodo.skip: # Prevent Komodo from stopping with StopAllContainers
restart: unless-stopped
@@ -51,8 +47,6 @@ services:
- ferretdb
logging:
driver: ${COMPOSE_LOGGING_DRIVER:-local}
networks:
- default
ports:
- 9120:9120
env_file: ./compose.env
@@ -74,20 +68,13 @@ services:
## or deploy the Periphery binary with systemd using
## https://github.com/moghtech/komodo/tree/main/scripts
periphery:
image: ghcr.io/moghtech/periphery:${COMPOSE_KOMODO_IMAGE_TAG:-latest}
image: ghcr.io/moghtech/komodo-periphery:${COMPOSE_KOMODO_IMAGE_TAG:-latest}
labels:
komodo.skip: # Prevent Komodo from stopping with StopAllContainers
restart: unless-stopped
logging:
driver: ${COMPOSE_LOGGING_DRIVER:-local}
networks:
- default
env_file: ./compose.env
environment:
PERIPHERY_REPO_DIR: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/repos
PERIPHERY_STACK_DIR: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/stacks
PERIPHERY_SSL_KEY_FILE: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/ssl/key.pem
PERIPHERY_SSL_CERT_FILE: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/ssl/cert.pem
volumes:
## Mount external docker socket
- /var/run/docker.sock:/var/run/docker.sock
@@ -103,7 +90,4 @@ volumes:
# Postgres
pg-data:
# Core
repo-cache:
networks:
default: {}
repo-cache:
+3 -17
View File
@@ -15,8 +15,6 @@ services:
restart: unless-stopped
logging:
driver: ${COMPOSE_LOGGING_DRIVER:-local}
networks:
- default
# ports:
# - 27017:27017
volumes:
@@ -25,7 +23,7 @@ services:
- FERRETDB_HANDLER=sqlite
core:
image: ghcr.io/moghtech/komodo:${COMPOSE_KOMODO_IMAGE_TAG:-latest}
image: ghcr.io/moghtech/komodo-core:${COMPOSE_KOMODO_IMAGE_TAG:-latest}
labels:
komodo.skip: # Prevent Komodo from stopping with StopAllContainers
restart: unless-stopped
@@ -33,8 +31,6 @@ services:
- ferretdb
logging:
driver: ${COMPOSE_LOGGING_DRIVER:-local}
networks:
- default
ports:
- 9120:9120
env_file: ./compose.env
@@ -56,20 +52,13 @@ services:
## or deploy the Periphery binary with systemd using
## https://github.com/moghtech/komodo/tree/main/scripts
periphery:
image: ghcr.io/moghtech/periphery:${COMPOSE_KOMODO_IMAGE_TAG:-latest}
image: ghcr.io/moghtech/komodo-periphery:${COMPOSE_KOMODO_IMAGE_TAG:-latest}
labels:
komodo.skip: # Prevent Komodo from stopping with StopAllContainers
restart: unless-stopped
logging:
driver: ${COMPOSE_LOGGING_DRIVER:-local}
networks:
- default
env_file: ./compose.env
environment:
PERIPHERY_REPO_DIR: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/repos
PERIPHERY_STACK_DIR: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/stacks
PERIPHERY_SSL_KEY_FILE: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/ssl/key.pem
PERIPHERY_SSL_CERT_FILE: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/ssl/cert.pem
volumes:
## Mount external docker socket
- /var/run/docker.sock:/var/run/docker.sock
@@ -85,7 +74,4 @@ volumes:
# Sqlite
sqlite-data:
# Core
repo-cache:
networks:
default: {}
repo-cache:
+6
View File
@@ -33,6 +33,12 @@ host = "https://demo.komo.do"
## Default: 9120
port = 9120
## The IP address the core server will bind to.
## The default will allow it to accept external IPv4 and IPv6 connections.
## Env: KOMODO_BIND_IP
## Default: [::]
bind_ip = "[::]"
## This is the token used to authenticate core requests to periphery.
## Ensure this matches a passkey in the connected periphery configs.
## If the periphery servers don't have passkeys configured, this doesn't need to be changed.
+12
View File
@@ -18,6 +18,12 @@
## Default: 8120
port = 8120
## The IP address the periphery server will bind to.
## The default will allow it to accept external IPv4 and IPv6 connections.
## Env: PERIPHERY_BIND_IP
## Default: [::]
bind_ip = "[::]"
## The directory periphery will use to manage repos.
## The periphery user must have write access to this directory.
## Env: PERIPHERY_REPO_DIR
@@ -30,6 +36,12 @@ repo_dir = "/etc/komodo/repos"
## Default: /etc/komodo/stacks
stack_dir = "/etc/komodo/stacks"
## The directory periphery will use to manage builds.
## The periphery user must have write access to this directory.
## Env: PERIPHERY_BUILD_DIR
## Default: /etc/komodo/builds
build_dir = "/etc/komodo/builds"
## How often Periphery polls the host for system stats,
## like CPU / memory usage.
## Env: PERIPHERY_STATS_POLLING_RATE
+22 -54
View File
@@ -1,5 +1,9 @@
# Connect More Servers
```mdx-code-block
import RemoteCodeFile from "@site/src/components/RemoteCodeFile";
```
Connecting a server to Komodo has 2 steps:
1. Install the Periphery agent on the server (either binary or container).
@@ -13,7 +17,7 @@ You can install Periphery as a systemd managed process, run it as a [docker cont
Allowing unintended access to the Periphery agent API is a security risk.
Ensure to take appropriate measures to block access to the Periphery API, such as firewall rules on port `8120`.
Additionally, you can whitelist your Komodo Core IP address in the [Periphery config](https://github.com/moghtech/komodo/blob/main/config/periphery.config.toml#L46),
and configure it to [only accept requests matching including your Core passkey](https://github.com/moghtech/komodo/blob/main/config/periphery.config.toml#L51).
and configure it to [only accept requests including your Core passkey](https://github.com/moghtech/komodo/blob/main/config/periphery.config.toml#L51).
:::
### Install the Periphery agent - systemd
@@ -37,53 +41,14 @@ This script can be run multiple times without issue, and it won't change existin
### Install the Periphery agent - container
You can use a docker compose file like this:
```yaml
services:
periphery:
image: ghcr.io/mbecker20/periphery:latest
# image: ghcr.io/mbecker20/periphery:latest-aarch64 # use for arm support
labels:
komodo.skip: # Prevent Komodo from stopping with StopAllContainers
logging:
driver: local
ports:
- 8120:8120
volumes:
## Mount external docker socket
- /var/run/docker.sock:/var/run/docker.sock
## Allow Periphery to see processes outside of container
- /proc:/proc
## use self signed certs in docker volume,
## or mount your own signed certs.
- ssl-certs:/etc/komodo/ssl
## manage repos in a docker volume,
## or change it to an accessible host directory.
- repos:/etc/komodo/repos
## manage stack files in a docker volume,
## or change it to an accessible host directory.
- stacks:/etc/komodo/stacks
## Optionally mount a path to store compose files
# - /path/to/compose:/host/compose
environment:
## Full list: `https://github.com/moghtech/komodo/blob/main/config/periphery.config.toml`
## Configure the same passkey given to Komodo Core (KOMODO_PASSKEY)
PERIPHERY_PASSKEYS: your_core_passkey # Alt: PERIPHERY_PASSKEYS_FILE
## Adding IP here will ensure calling IP is in the list. (optional)
PERIPHERY_ALLOWED_IPS:
## Enable HTTPS server
PERIPHERY_SSL_ENABLED: true
## If the disk size is overreporting, can use one of these to
## whitelist / blacklist the disks to filter them, whichever is easier.
## Accepts comma separated list of paths.
## Usually whitelisting /etc/hostname gives correct size.
PERIPHERY_INCLUDE_DISK_MOUNTS: /etc/hostname
# PERIPHERY_EXCLUDE_DISK_MOUNTS: /snap,/etc/repos
You can use a docker compose file:
volumes:
ssl-certs:
repos:
stacks:
```mdx-code-block
<RemoteCodeFile
title="https://github.com/moghtech/komodo/blob/main/compose/periphery.compose.yaml"
url="https://raw.githubusercontent.com/moghtech/komodo/main/compose/periphery.compose.yaml"
language="yaml"
/>
```
### Manual install steps - binaries
@@ -112,7 +77,7 @@ Ensure that the user which periphery is run as has access to the docker group wi
```
periphery \
--config-path /path/to/periphery.config.base.toml \
--config-path /other_path/to/overide-periphery-config-directory \
--config-path /other_path/to/override-periphery-config-directory \
--config-keyword periphery \
--config-keyword config \
--merge-nested-config true
@@ -122,21 +87,26 @@ periphery \
You can run `periphery --help` to see the manual.
:::
When running periphery in docker, use [command](https://docs.docker.com/reference/compose-file/services/#command) to pass in additional arguments.
```
command: periphery --config-path /path/in/container/to/periphery.config.base.toml
```
### Passing config files
Either file paths or directory paths can be passed to `--config-path`.
When using directories, the file entries can be filtered by name with the `--config-keyword` argument, which can be passed multiple times to add more keywords. If passed, then only config files with file names that contain all keywords will be merged.
When passing multiple config files, later --config-path given in the command will always overide previous ones. Directory config files are merged in alphabetical order by name, so `config_b.toml` will overide `config_a.toml`.
When passing multiple config files, later --config-path given in the command will always override previous ones. Directory config files are merged in alphabetical order by name, so `config_b.toml` will override `config_a.toml`.
There are two ways to merge config files. The default behavior is to completely replace any base fields with whatever fields are present in the overide config. So if you pass `allowed_ips = []` in your overide config, the final allowed_ips will be an empty list as well.
There are two ways to merge config files. The default behavior is to completely replace any base fields with whatever fields are present in the override config. So if you pass `allowed_ips = []` in your override config, the final allowed_ips will be an empty list as well.
`--merge-nested-config true` will merge config fields recursively and extend config array fields.
For example, with `--merge-nested-config true` you can specify an allowed ip in the base config, and another in the overide config, they will both be present in the final config.
For example, with `--merge-nested-config true` you can specify an allowed ip in the base config, and another in the override config, they will both be present in the final config.
Similarly, you can specify a base docker / github account pair, and extend them with additional accounts in the overide config.
Similarly, you can specify a base docker / github account pair, and extend them with additional accounts in the override config.
## Configuration
@@ -146,8 +116,6 @@ wget -P komodo https://raw.githubusercontent.com/moghtech/komodo/main/config/per
```
```mdx-code-block
import RemoteCodeFile from "@site/src/components/RemoteCodeFile";
<RemoteCodeFile
title="https://github.com/moghtech/komodo/blob/main/config/periphery.config.toml"
url="https://raw.githubusercontent.com/moghtech/komodo/main/config/periphery.config.toml"
+1 -1
View File
@@ -39,7 +39,7 @@ Komodo Periphery is a small stateless web server that runs on all connected serv
## Core API
Komodo exposes powerful functionality over the Core's REST and Websocket API, enabling infrastructure engineers to manage their infrastructure programmatically. There is a [rust crate](https://crates.io/crates/komodo_client) to simplify programmatic interaction with the API, but in general this can be accomplished using any programming language that can make REST requests.
Komodo exposes powerful functionality over the Core's REST and Websocket API, enabling infrastructure engineers to manage their infrastructure programmatically. There is a [rust crate](https://crates.io/crates/komodo_client) and [npm package](https://www.npmjs.com/package/komodo_client) to simplify programmatic interaction with the API, but in general this can be accomplished using any programming language that can make REST requests.
## Permissioning
+15
View File
@@ -0,0 +1,15 @@
# Other Resources
### Posts and Guides
- [Migrating to Komodo](https://blog.foxxmd.dev/posts/migrating-to-komodo) by [FoxxMD](https://github.com/FoxxMD)
- [FAQ, Tips, and Tricks](https://blog.foxxmd.dev/posts/komodo-tips-tricks) by [FoxxMD](https://github.com/FoxxMD)
- [Compose Environments Explained](https://blog.foxxmd.dev/posts/compose-envs-explained) by [FoxxMD](https://github.com/FoxxMD)
- [How To: Automate version updates for your self-hosted Docker containers with Gitea, Renovate, and Komodo](https://nickcunningh.am/blog/how-to-automate-version-updates-for-your-self-hosted-docker-containers-with-gitea-renovate-and-komodo) by [TheNickOfTime](https://github.com/TheNickOfTime)
### Community Alerters
These provide alerting implementations which can be used with the `Custom` Alerter type.
- [Discord](https://github.com/FoxxMD/deploy-discord-alerter) by [FoxxMD](https://github.com/FoxxMD)
- [Telegram](https://github.com/mattsmallman/komodo-alert-to-telgram) by [mattsmallman](https://github.com/mattsmallman)
- [Ntfy](https://github.com/FoxxMD/deploy-ntfy-alerter) by [FoxxMD](https://github.com/FoxxMD)
- [Gotify](https://github.com/FoxxMD/deploy-gotify-alerter) by [FoxxMD](https://github.com/FoxxMD)
- [Apprise](https://github.com/FoxxMD/deploy-apprise-alerter) by [FoxxMD](https://github.com/FoxxMD)
+2 -2
View File
@@ -1,7 +1,7 @@
# Sqlite
:::info
FerritDB + Sqlite authentication support is [still experimental](https://docs.ferretdb.io/security/authentication/#experimental-authentication-mode),
FerretDB + Sqlite authentication support is [still experimental](https://docs.ferretdb.io/security/authentication/#experimental-authentication-mode),
and is not enabled in this database configuration. Because of this, the values for `DB_USERNAME` and `DB_PASSWORD`
are ignored when using Sqlite, and the database port is not exposed publicly by default.
:::
@@ -22,4 +22,4 @@ docker compose -p komodo -f komodo/sqlite.compose.yaml --env-file komodo/compose
import ComposeAndEnv from "@site/src/components/ComposeAndEnv";
<ComposeAndEnv file_name="sqlite.compose.yaml" />
```
```
+10 -4
View File
@@ -1,17 +1,23 @@
# Sync Resources
Komodo is able to create, update, delete, and deploy resources declared in TOML files by diffing them against the existing resources,
and apply updates based on the diffs. Push the files to a remote git repo and create a `ResourceSync` pointing to the repo,
and the core backend will poll for any updates (you can also manually trigger an update poll / execution in the UI).
and apply updates based on the diffs. Similar to Stacks, the files can be configured in UI, in a local file, or in files pushed to a remote git repo.
The Komodo Core backend will poll the files for for any updates, and alert about pending changes when diffs are detected.
File detection is additive and recursive, so you can spread out your resource declarations across any number of files
and use any nesting of folders to organize resources inside a root folder. Additionally, you can create multiple `ResourceSyncs`
You can spread out your resource declarations across any number of files
and use any nesting of folders to organize resources inside a root folder.
Additionally, you can create multiple `ResourceSyncs` and configure `Match Tags` to filter down which resources are synced,
and each sync will be handled independently. This allows different syncs to manage resources on a "per-project" basis.
The UI will display the computed sync actions and only execute them upon manual confirmation.
Or the sync execution git webhook may be configured on the git repo to
automatically execute syncs upon pushes to the configured branch.
## Commit to Syncs
If the Sync is pointing to just a single file, you can enable "Managed Mode" to allow Core to write the updates you made in UI _back to the file_.
This works no matter where the files are located, and will create a commit to your git repository for repo based files.
## Example Declarations
### Server
+2 -1
View File
@@ -65,7 +65,8 @@ const sidebars: SidebarsConfig = {
"webhooks",
"version-upgrades",
"api",
"development"
"development",
"other-resources"
],
};
+9001
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,4 +1,4 @@
FROM node:20.12-alpine as builder
FROM node:20.12-alpine AS builder
WORKDIR /builder
@@ -7,7 +7,7 @@ COPY ./client/core/ts ./client
# Optionally specify a specific Komodo host.
ARG VITE_KOMODO_HOST=""
ENV VITE_KOMODO_HOST=${VITE_KOMODO_HOST}
ENV VITE_KOMODO_HOST=$VITE_KOMODO_HOST
# Build and link the client
RUN cd client && yarn && yarn build && yarn link
+2
View File
@@ -117,6 +117,8 @@ export function KomodoClient(url, options) {
// Sleep for a bit before checking for websocket closed
await new Promise((resolve) => setTimeout(resolve, 500));
}
// Sleep for a bit before retrying to avoid spam.
await new Promise((resolve) => setTimeout(resolve, retry_timeout_ms));
}
catch (error) {
console.error(error);
+2
View File
@@ -46,6 +46,7 @@ export type ReadResponses = {
GetServer: Types.GetServerResponse;
GetServerState: Types.GetServerStateResponse;
GetPeripheryVersion: Types.GetPeripheryVersionResponse;
GetDockerContainersSummary: Types.GetDockerContainersSummaryResponse;
ListDockerContainers: Types.ListDockerContainersResponse;
ListAllDockerContainers: Types.ListAllDockerContainersResponse;
InspectDockerContainer: Types.InspectDockerContainerResponse;
@@ -168,6 +169,7 @@ export type WriteResponses = {
DeleteBuild: Types.Build;
UpdateBuild: Types.Build;
RenameBuild: Types.Update;
WriteBuildFileContents: Types.Update;
RefreshBuildCache: Types.NoData;
CreateBuildWebhook: Types.CreateBuildWebhookResponse;
DeleteBuildWebhook: Types.DeleteBuildWebhookResponse;
+126 -41
View File
@@ -43,7 +43,10 @@ export interface Resource<Config, Info> {
base_permission?: PermissionLevel;
}
export interface ActionConfig {
/** Typescript file contents using pre-initialized `komodo` client. */
/**
* Typescript file contents using pre-initialized `komodo` client.
* Supports variable / secret interpolation.
*/
file_contents?: string;
/** Whether incoming webhooks actually trigger action. */
webhook_enabled: boolean;
@@ -119,6 +122,11 @@ export type AlerterEndpoint =
| {
type: "Discord";
params: DiscordAlerterEndpoint;
}
/** Send alert to Ntfy */
| {
type: "Ntfy";
params: NtfyAlerterEndpoint;
};
/** Used to reference a specific resource across all resource types */
export type ResourceTarget = {
@@ -216,10 +224,6 @@ export interface Version {
minor: number;
patch: number;
}
export interface SystemCommand {
path?: string;
command?: string;
}
/** Configuration for an image registry */
export interface ImageRegistryConfig {
/**
@@ -235,6 +239,10 @@ export interface ImageRegistryConfig {
*/
organization?: string;
}
export interface SystemCommand {
path?: string;
command?: string;
}
/** The build configuration. */
export interface BuildConfig {
/** Which builder is used to build the image. */
@@ -256,8 +264,8 @@ export interface BuildConfig {
*/
image_name?: string;
/**
* An extra tag put before the build version, for the image pushed to the repository.
* Eg. in image tag of `aarch64` would push to moghtech/komodo:1.13.2-aarch64.
* An extra tag put after the build version, for the image pushed to the repository.
* Eg. in image tag of `aarch64` would push to moghtech/komodo-core:1.13.2-aarch64.
* If this is empty, the image tag will just be the build version.
*
* Can be used in conjunction with `image_name` to direct multiple builds
@@ -296,8 +304,13 @@ export interface BuildConfig {
* If its an empty string, use the default secret from the config.
*/
webhook_secret?: string;
/** The optional command run after repo clone and before docker build. */
pre_build?: SystemCommand;
/**
* If this is checked, the build will source the files on the host.
* Use `build_path` and `dockerfile_path` to specify the path on the host.
* This is useful for those who wish to setup their files on the host,
* rather than defining the contents in UI or in a git repo.
*/
files_on_host?: boolean;
/**
* The path of the docker build context relative to the root of the repo.
* Default: "." (the root of the repo).
@@ -313,6 +326,13 @@ export interface BuildConfig {
use_buildx?: boolean;
/** Any extra docker cli arguments to be included in the build command */
extra_args?: string[];
/** The optional command run after repo clone and before docker build. */
pre_build?: SystemCommand;
/**
* UI defined dockerfile contents.
* Supports variable / secret interpolation.
*/
dockerfile?: string;
/**
* Docker build arguments.
*
@@ -336,11 +356,27 @@ export interface BuildConfig {
labels?: string;
}
export interface BuildInfo {
/** The timestamp build was last built. */
last_built_at: I64;
/** Latest built short commit hash, or null. */
built_hash?: string;
/** Latest built commit message, or null. Only for repo based stacks */
built_message?: string;
/**
* The last built dockerfile contents.
* This is updated whenever Komodo successfully runs the build.
*/
built_contents?: string;
/** The absolute path to the file */
remote_path?: string;
/**
* The remote dockerfile contents, whether on host or in repo.
* This is updated whenever Komodo refreshes the build cache.
* It will be empty if the dockerfile is defined directly in the build config.
*/
remote_contents?: string;
/** If there was an error in getting the remote contents, it will be here. */
remote_error?: string;
/** Latest remote short commit hash, or null. */
latest_hash?: string;
/** Latest remote commit message, or null */
@@ -358,26 +394,28 @@ export declare enum BuildState {
Unknown = "Unknown"
}
export interface BuildListItemInfo {
/** State of the build. Reflects whether most recent build successful. */
state: BuildState;
/** Unix timestamp in milliseconds of last build */
last_built_at: I64;
/** The current version of the build */
version: Version;
/** The builder attached to build. */
builder_id: string;
/** Whether build is in files on host mode. */
files_on_host: boolean;
/** The git provider domain */
git_provider: string;
/** The image registry domain */
image_registry_domain: string;
git_provider?: string;
/** The repo used as the source of the build */
repo: string;
repo?: string;
/** The branch of the repo */
branch: string;
/** State of the build. Reflects whether most recent build successful. */
state: BuildState;
branch?: string;
/** Latest built short commit hash, or null. */
built_hash?: string;
/** Latest short commit hash, or null. Only for repo based stacks */
latest_hash?: string;
/** The image registry domain */
image_registry_domain?: string;
}
export type BuildListItem = ResourceListItem<BuildListItemInfo>;
export interface BuildQuerySpecifics {
@@ -1622,6 +1660,12 @@ export interface ServerConfig {
* default: 2
*/
timeout_seconds: I64;
/**
* An optional override passkey to use
* to authenticate with periphery agent.
* If this is empty, will use passkey in core config.
*/
passkey?: string;
/**
* Sometimes the system stats reports a mount path that is not desired.
* Use this field to filter it out from the report.
@@ -1732,31 +1776,6 @@ export interface StackConfig {
destroy_before_deploy?: boolean;
/** Whether to skip secret interpolation into the stack environment variables. */
skip_secret_interp?: boolean;
/**
* If this is checked, the stack will source the files on the host.
* Use `run_directory` and `file_paths` to specify the path on the host.
* This is useful for those who wish to setup their files on the host using SSH or similar,
* rather than defining the contents in UI or in a git repo.
*/
files_on_host?: boolean;
/** Directory to change to (`cd`) before running `docker compose up -d`. */
run_directory?: string;
/**
* Add paths to compose files, relative to the run path.
* If this is empty, will use file `compose.yaml`.
*/
file_paths?: string[];
/**
* The name of the written environment file before `docker compose up`.
* Relative to the run directory root.
* Default: .env
*/
env_file_path: string;
/**
* Add additional env files to attach with `--env-file`.
* Relative to the run directory root.
*/
additional_env_files?: string[];
/** The git provider domain. Default: github.com */
git_provider: string;
/**
@@ -1796,6 +1815,31 @@ export interface StackConfig {
* If this option is enabled, will always run `DeployStack` without diffing.
*/
webhook_force_deploy?: boolean;
/**
* If this is checked, the stack will source the files on the host.
* Use `run_directory` and `file_paths` to specify the path on the host.
* This is useful for those who wish to setup their files on the host,
* rather than defining the contents in UI or in a git repo.
*/
files_on_host?: boolean;
/** Directory to change to (`cd`) before running `docker compose up -d`. */
run_directory?: string;
/**
* Add paths to compose files, relative to the run path.
* If this is empty, will use file `compose.yaml`.
*/
file_paths?: string[];
/**
* The name of the written environment file before `docker compose up`.
* Relative to the run directory root.
* Default: .env
*/
env_file_path: string;
/**
* Add additional env files to attach with `--env-file`.
* Relative to the run directory root.
*/
additional_env_files?: string[];
/** Whether to send StackStateChange alerts for this stack. */
send_alerts: boolean;
/** Used with `registry_account` to login to a registry before docker compose up. */
@@ -1827,6 +1871,7 @@ export interface StackConfig {
* The contents of the file directly, for management in the UI.
* If this is empty, it will fall back to checking git config for
* repo based compose file.
* Supports variable / secret interpolation.
*/
file_contents?: string;
/**
@@ -2137,6 +2182,7 @@ export declare enum Operation {
DeleteBuild = "DeleteBuild",
RunBuild = "RunBuild",
CancelBuild = "CancelBuild",
WriteDockerfile = "WriteDockerfile",
CreateRepo = "CreateRepo",
UpdateRepo = "UpdateRepo",
RenameRepo = "RenameRepo",
@@ -3869,6 +3915,8 @@ export interface CloneArgs {
branch: string;
/** Specific commit hash. Optional */
commit?: string;
/** Use PERIPHERY_BUILD_DIR as the parent folder for the clone. */
is_build: boolean;
/** The clone destination path */
destination?: string;
/** Command to run after the repo has been cloned */
@@ -4925,6 +4973,25 @@ export interface GetDeploymentsSummaryResponse {
/** The number of Deployments with Unknown state */
unknown: I64;
}
/**
* Gets a summary of data relating to all containers.
* Response: [GetDockerContainersSummaryResponse].
*/
export interface GetDockerContainersSummary {
}
/** Response for [GetDockerContainersSummary] */
export interface GetDockerContainersSummaryResponse {
/** The total number of Containers */
total: number;
/** The number of Containers with Running state */
running: number;
/** The number of Containers with Stopped or Paused or Created state */
stopped: number;
/** The number of Containers with Restarting or Dead state */
unhealthy: number;
/** The number of Containers with Unknown state */
unknown: number;
}
/**
* Get a specific docker registry account.
* Response: [GetDockerRegistryAccountResponse].
@@ -5988,6 +6055,11 @@ export interface NameAndId {
name: string;
id: string;
}
/** Configuration for a Ntfy alerter. */
export interface NtfyAlerterEndpoint {
/** The ntfy topic URL */
url: string;
}
/** Pauses all containers on the target server. Response: [Update] */
export interface PauseAllContainers {
/** Name or id */
@@ -7031,6 +7103,13 @@ export interface UrlBuilderConfig {
/** A custom passkey to use. Otherwise, use the default passkey. */
passkey?: string;
}
/** Update dockerfile contents in Files on Server or Git Repo mode. Response: [Update]. */
export interface WriteBuildFileContents {
/** The name or id of the target Build. */
build: string;
/** The dockerfile contents to write. */
contents: string;
}
/** Update file contents in Files on Server or Git Repo mode. Response: [Update]. */
export interface WriteStackFileContents {
/** The name or id of the target Stack. */
@@ -7401,6 +7480,9 @@ export type ReadRequest = {
} | {
type: "InspectDockerVolume";
params: InspectDockerVolume;
} | {
type: "GetDockerContainersSummary";
params: GetDockerContainersSummary;
} | {
type: "ListAllDockerContainers";
params: ListAllDockerContainers;
@@ -7736,6 +7818,9 @@ export type WriteRequest = {
} | {
type: "RenameBuild";
params: RenameBuild;
} | {
type: "WriteBuildFileContents";
params: WriteBuildFileContents;
} | {
type: "RefreshBuildCache";
params: RefreshBuildCache;
+1
View File
@@ -236,6 +236,7 @@ export var Operation;
Operation["DeleteBuild"] = "DeleteBuild";
Operation["RunBuild"] = "RunBuild";
Operation["CancelBuild"] = "CancelBuild";
Operation["WriteDockerfile"] = "WriteDockerfile";
Operation["CreateRepo"] = "CreateRepo";
Operation["UpdateRepo"] = "UpdateRepo";
Operation["RenameRepo"] = "RenameRepo";
@@ -105,7 +105,7 @@ export const ActionConfig = ({ id }: { id: string }) => {
},
{
label: "Webhook",
description: `Configure your ${webhook_integration}-style repo provider to send webhooks to Komodo`,
description: `Copy the webhook given here, and configure your ${webhook_integration}-style repo provider to send webhooks to Komodo`,
components: {
["Builder" as any]: () => (
<WebhookBuilder git_provider={ACTION_GIT_PROVIDER}>
@@ -13,6 +13,7 @@ const ENDPOINT_TYPES: Types.AlerterEndpoint["type"][] = [
"Custom",
"Discord",
"Slack",
"Ntfy",
];
export const EndpointConfig = ({
@@ -67,5 +68,7 @@ const default_url = (type: Types.AlerterEndpoint["type"]) => {
? "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX"
: type === "Discord"
? "https://discord.com/api/webhooks/XXXXXXXXXXXX/XXXX-XXXXXXXXXX"
: type === "Ntfy"
? "https://ntfy.sh/komodo"
: "";
};
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,11 @@
import { Section } from "@components/layouts";
import { useInvalidate, useRead, useUser, useWrite } from "@lib/hooks";
import {
useInvalidate,
useLocalStorage,
useRead,
useUser,
useWrite,
} from "@lib/hooks";
import { RequiredResourceComponents } from "@types";
import { Factory, FolderGit, Hammer, Loader2, RefreshCcw } from "lucide-react";
import { BuildConfig } from "./config";
@@ -13,7 +19,6 @@ import {
stroke_color_class_by_intention,
} from "@lib/color";
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";
@@ -27,6 +32,7 @@ import { useBuilder } from "../builder";
import { RenameResource } from "@components/config/util";
import { GroupActions } from "@components/group-actions";
import { Tooltip, TooltipContent, TooltipTrigger } from "@ui/tooltip";
import { BuildInfo } from "./info";
export const useBuild = (id?: string) =>
useRead("ListBuilds", {}, { refetchInterval: 10_000 }).data?.find(
@@ -42,8 +48,11 @@ const BuildIcon = ({ id, size }: { id?: string; size: number }) => {
return <Hammer className={cn(`w-${size} h-${size}`, state && color)} />;
};
const ConfigOrDeployments = ({ id }: { id: string }) => {
const [view, setView] = useState("Config");
const ConfigInfoDeployments = ({ id }: { id: string }) => {
const [view, setView] = useLocalStorage<"Config" | "Info" | "Deployments">(
"build-tabs-v1",
"Config"
);
const deployments = useRead("ListDeployments", {}).data?.filter(
(deployment) => deployment.info.build_id === id
);
@@ -53,6 +62,9 @@ const ConfigOrDeployments = ({ id }: { id: string }) => {
<TabsTrigger value="Config" className="w-[110px]">
Config
</TabsTrigger>
<TabsTrigger value="Info" className="w-[110px]">
Info
</TabsTrigger>
<TabsTrigger
value="Deployments"
className="w-[110px]"
@@ -64,13 +76,16 @@ const ConfigOrDeployments = ({ id }: { id: string }) => {
);
return (
<Tabs
value={deploymentsDisabled ? "Config" : view}
onValueChange={setView}
value={deploymentsDisabled && view === "Deployments" ? "Config" : view}
onValueChange={setView as any}
className="grid gap-4"
>
<TabsContent value="Config">
<BuildConfig id={id} titleOther={titleOther} />
</TabsContent>
<TabsContent value="Info">
<BuildInfo id={id} titleOther={titleOther} />
</TabsContent>
<TabsContent value="Deployments">
<Section
titleOther={titleOther}
@@ -265,7 +280,7 @@ export const BuildComponents: RequiredResourceComponents = {
Page: {},
Config: ConfigOrDeployments,
Config: ConfigInfoDeployments,
DangerZone: ({ id }) => (
<>
@@ -0,0 +1,226 @@
import { Section } from "@components/layouts";
import { ReactNode, useState } from "react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@ui/card";
import { useFullBuild } from ".";
import { cn, updateLogToHtml } from "@lib/utils";
import { MonacoEditor } from "@components/monaco";
import { useEditPermissions } from "@pages/resource";
import { ConfirmUpdate } from "@components/config/util";
import { useLocalStorage, useRead, useWrite } from "@lib/hooks";
import { Button } from "@ui/button";
import { Clock, FilePlus, History } from "lucide-react";
import { useToast } from "@ui/use-toast";
import { ConfirmButton, ShowHideButton } from "@components/util";
import { DEFAULT_BUILD_DOCKERFILE_CONTENTS } from "./config";
import { fmt_duration } from "@lib/formatting";
export const BuildInfo = ({
id,
titleOther,
}: {
id: string;
titleOther: ReactNode;
}) => {
const [edits, setEdits] = useLocalStorage<{ contents: string | undefined }>(
`build-${id}-edits`,
{ contents: undefined }
);
const [showContents, setShowContents] = useState(true);
const { canWrite } = useEditPermissions({ type: "Build", id });
const { toast } = useToast();
const { mutateAsync, isPending } = useWrite("WriteBuildFileContents", {
onSuccess: (res) => {
toast({
title: res.success ? "Contents written." : "Failed to write contents.",
variant: res.success ? undefined : "destructive",
});
},
});
const build = useFullBuild(id);
const recent_builds = useRead("ListUpdates", {
query: { "target.type": "Build", "target.id": id, operation: "RunBuild" },
}).data;
const _last_build = recent_builds?.updates[0];
const last_build = useRead(
"GetUpdate",
{
id: _last_build?.id!,
},
{ enabled: !!_last_build }
).data;
const file_on_host = build?.config?.files_on_host ?? false;
const git_repo = build?.config?.repo ? true : false;
const canEdit = canWrite && (file_on_host || git_repo);
const remote_path = build?.info?.remote_path;
const remote_contents = build?.info?.remote_contents;
const remote_error = build?.info?.remote_error;
return (
<Section titleOther={titleOther}>
{/* Errors */}
{remote_error && remote_error.length > 0 && (
<Card className="flex flex-col gap-4">
<CardHeader className="flex flex-row justify-between items-center pb-0">
<div className="font-mono flex gap-2">
{remote_path && (
<>
<div className="text-muted-foreground">Path:</div>
{remote_path}
</>
)}
</div>
{canEdit && (
<ConfirmButton
title="Initialize File"
icon={<FilePlus className="w-4 h-4" />}
onClick={() => {
if (build) {
mutateAsync({
build: build.name,
contents: DEFAULT_BUILD_DOCKERFILE_CONTENTS,
});
}
}}
loading={isPending}
/>
)}
</CardHeader>
<CardContent className="pr-8">
<pre
dangerouslySetInnerHTML={{
__html: updateLogToHtml(remote_error),
}}
className="max-h-[500px] overflow-y-auto"
/>
</CardContent>
</Card>
)}
{/* Update latest contents */}
{remote_contents && remote_contents.length > 0 && (
<Card className="flex flex-col gap-4">
<CardHeader
className={cn(
"flex flex-row justify-between items-center",
showContents && "pb-0"
)}
>
{remote_path && (
<CardTitle className="font-mono flex gap-2">
<div className="text-muted-foreground">Path:</div>
{remote_path}
</CardTitle>
)}
<div className="flex items-center gap-2">
{canEdit && (
<>
<Button
variant="outline"
onClick={() => setEdits({ contents: undefined })}
className="flex items-center gap-2"
disabled={!edits.contents}
>
<History className="w-4 h-4" />
Reset
</Button>
<ConfirmUpdate
previous={{ contents: remote_contents }}
content={{ contents: edits.contents }}
onConfirm={async () => {
if (build) {
return await mutateAsync({
build: build.name,
contents: edits.contents!,
}).then(() => setEdits({ contents: undefined }));
}
}}
disabled={!edits.contents}
language="dockerfile"
loading={isPending}
/>
</>
)}
<ShowHideButton show={showContents} setShow={setShowContents} />
</div>
</CardHeader>
{showContents && (
<CardContent className="pr-8">
<MonacoEditor
value={edits.contents ?? remote_contents}
language="dockerfile"
readOnly={!canEdit}
onValueChange={(contents) => setEdits({ contents })}
/>
</CardContent>
)}
</Card>
)}
{/* Last build output */}
{last_build && last_build.logs.length > 0 && (
<code className="font-bold">Last Build Logs</code>
)}
{last_build &&
last_build.logs.length > 0 &&
last_build.logs?.map((log, i) => (
<Card key={i}>
<CardHeader className="flex-col">
<CardTitle>{log.stage}</CardTitle>
<CardDescription className="flex gap-2">
<span>
Stage {i + 1} of {last_build.logs.length}
</span>
<span>|</span>
<span className="flex items-center gap-2">
<Clock className="w-4 h-4" />
{fmt_duration(log.start_ts, log.end_ts)}
</span>
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-2">
{log.command && (
<div>
<CardDescription>command</CardDescription>
<pre className="max-h-[500px] overflow-y-auto">
{log.command}
</pre>
</div>
)}
{log.stdout && (
<div>
<CardDescription>stdout</CardDescription>
<pre
dangerouslySetInnerHTML={{
__html: updateLogToHtml(log.stdout),
}}
className="max-h-[500px] overflow-y-auto"
/>
</div>
)}
{log.stderr && (
<div>
<CardDescription>stderr</CardDescription>
<pre
dangerouslySetInnerHTML={{
__html: updateLogToHtml(log.stderr),
}}
className="max-h-[500px] overflow-y-auto"
/>
</div>
)}
</CardContent>
</Card>
))}
</Section>
);
};
@@ -27,10 +27,19 @@ export const BuildTable = ({ builds }: { builds: Types.BuildListItem[] }) => {
size: 200,
},
{
accessorKey: "info.repo",
header: ({ column }) => (
<SortableHeader column={column} title="Repo" />
),
// header: ({ column }) => (
// <SortableHeader column={column} title="Source" />
// ),
header: "Source",
accessorFn: ({ info: { files_on_host, repo } }) => {
if (files_on_host) {
return "Files on Server";
} else if (repo) {
return repo;
} else {
return "UI Defined";
}
},
size: 200,
},
{
@@ -222,7 +222,9 @@ const ProcedureConfigInner = ({
<CardHeader>
<CardTitle>Webhook</CardTitle>
<CardDescription>
Trigger this Procedure with a webhook.
Copy the webhook given here, and configure your
{webhook_integration}-style repo provider to send webhooks to
Komodo
</CardDescription>
</CardHeader>
<CardContent>
@@ -238,8 +238,7 @@ export const RepoConfig = ({ id }: { id: string }) => {
},
{
label: "Webhooks",
description:
"Configure your repo provider to send webhooks to Komodo",
description: `Copy the webhook given here, and configure your ${webhook_integration}-style repo provider to send webhooks to Komodo`,
components: {
["Guard" as any]: () => {
if (update.branch ?? config.branch) {
@@ -90,7 +90,7 @@ export const ResourceSyncConfig = ({
const disabled = global_disabled || perms !== Types.PermissionLevel.Write;
const git_provider = update.git_provider ?? config.git_provider;
const integration = getWebhookIntegration(integrations, git_provider);
const webhook_integration = getWebhookIntegration(integrations, git_provider);
const mode = getSyncMode(update, config);
const managed = update.managed ?? config.managed ?? false;
@@ -138,6 +138,7 @@ export const ResourceSyncConfig = ({
<ConfigItem
label="Choose Mode"
description="Will the file contents be defined in UI, stored on the server, or pulled from a git repo?"
boldLabel
>
<Select
value={mode}
@@ -331,8 +332,7 @@ export const ResourceSyncConfig = ({
match_tags,
{
label: "Git Webhooks",
description:
"Configure your repo provider to send webhooks to Komodo",
description: `Copy the webhook given here, and configure your ${webhook_integration}-style repo provider to send webhooks to Komodo`,
contentHidden: !show.webhooks,
actions: (
<ShowHideButton
@@ -360,7 +360,7 @@ export const ResourceSyncConfig = ({
description="Trigger an update of the pending sync cache, to display the changes in the UI on push."
>
<CopyWebhook
integration={integration}
integration={webhook_integration}
path={`/sync/${id_or_name === "Id" ? id : name}/refresh`}
/>
</ConfigItem>
@@ -371,7 +371,7 @@ export const ResourceSyncConfig = ({
description="Trigger an execution of the sync on push."
>
<CopyWebhook
integration={integration}
integration={webhook_integration}
path={`/sync/${id_or_name === "Id" ? id : name}/sync`}
/>
</ConfigItem>
@@ -164,6 +164,7 @@ export const StackConfig = ({
<ConfigItem
label="Choose Mode"
description="Will the file contents be defined in UI, stored on the server, or pulled from a git repo?"
boldLabel
>
<Select
value={mode}
@@ -493,12 +494,10 @@ export const StackConfig = ({
server_component,
{
label: "Files",
labelHidden: true,
components: {
run_directory: {
label: "Run Directory",
description:
"Set the working directory when running the compose up command. Usually is the parent folder of the compose files.",
description: `Set the working directory when running the 'compose up' command. Can be absolute path, or relative to $PERIPHERY_STACK_DIR/${stack.name}`,
placeholder: "/path/to/folder",
},
file_paths: (value, set) => (
@@ -583,13 +582,11 @@ export const StackConfig = ({
},
{
label: "Files",
labelHidden: true,
components: {
run_directory: {
label: "Run Directory",
description:
"Set the working directory when running the compose up command, relative to the stack root. ($periphery_stack_dir/$stack_name/$run_directory)",
placeholder: "./path/to/folder",
"Set the working directory when running the compose up command, relative to the root of the repo.",
placeholder: "path/to/folder",
},
file_paths: (value, set) => (
<ConfigList
@@ -607,8 +604,7 @@ export const StackConfig = ({
...general_common,
{
label: "Webhooks",
description:
"Configure your repo provider to send webhooks to Komodo",
description: `Copy the webhook given here, and configure your ${webhook_integration}-style repo provider to send webhooks to Komodo`,
actions: (
<ShowHideButton
show={show.webhooks}
@@ -846,7 +842,7 @@ export const StackConfig = ({
);
};
export const DEFAULT_STACK_FILE_CONTENTS = `## 🦎 Hello Komodo 🦎
export const DEFAULT_STACK_FILE_CONTENTS = `## Add your compose file here
services:
hello_world:
image: hello-world
+8 -11
View File
@@ -1,5 +1,5 @@
import {
tagsAtom,
useTags,
useInvalidate,
useRead,
useShiftKeyListener,
@@ -19,7 +19,6 @@ import {
} from "@ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@ui/popover";
import { useToast } from "@ui/use-toast";
import { useAtom } from "jotai";
import { MinusCircle, PlusCircle, SearchX, Tag, X } from "lucide-react";
import { ReactNode, useEffect, useState } from "react";
import { tag_background_class } from "@lib/color";
@@ -29,27 +28,24 @@ type TargetExcludingSystem = Exclude<Types.ResourceTarget, { type: "System" }>;
export const TagsFilter = () => {
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
const [tags, setTags] = useAtom<string[]>(tagsAtom);
const { tags, add_tag, remove_tag, clear_tags } = useTags();
const all_tags = useRead("ListTags", {}).data;
const filtered = filterBySplit(all_tags, search, (item) => item.name);
useShiftKeyListener("T", () => setOpen(true));
useShiftKeyListener("C", () => setTags([]));
useShiftKeyListener("C", () => clear_tags());
return (
<div className="flex gap-3 items-center">
{tags.length > 0 && (
<Button
variant="destructive"
className="px-2 py-1.5 h-fit"
onClick={() => setTags([])}
onClick={() => clear_tags()}
>
<X className="w-4 h-4" />
</Button>
)}
<TagsFilterTags
tag_ids={tags}
onBadgeClick={(tag_id) => setTags(tags.filter((id) => id !== tag_id))}
/>
<TagsFilterTags tag_ids={tags} onBadgeClick={remove_tag} />
<Popover
open={open}
@@ -89,7 +85,7 @@ export const TagsFilter = () => {
<CommandItem
key={tag.name}
onSelect={() => {
setTags([...tags, tag._id!.$oid]);
add_tag(tag._id!.$oid);
setSearch("");
setOpen(false);
}}
@@ -228,9 +224,10 @@ export const TagsWithBadge = ({
};
export const TableTags = ({ tag_ids }: { tag_ids: string[] }) => {
const { toggle_tag } = useTags();
return (
<div className="flex gap-1 flex-wrap">
<TagsWithBadge tag_ids={tag_ids} />
<TagsWithBadge tag_ids={tag_ids} onBadgeClick={toggle_tag} />
</div>
);
};
+25 -1
View File
@@ -343,7 +343,31 @@ export const atomWithStorage = <T>(key: string, init: T) => {
);
};
export const tagsAtom = atomWithStorage<string[]>("tags-v0", []);
const tagsAtom = atomWithStorage<string[]>("tags-v0", []);
export const useTags = () => {
const [tags, setTags] = useAtom<string[]>(tagsAtom);
const add_tag = (tag_id: string) => setTags([...tags, tag_id]);
const remove_tag = (tag_id: string) =>
setTags(tags.filter((id) => id !== tag_id));
const toggle_tag = (tag_id: string) => {
if (tags.includes(tag_id)) {
remove_tag(tag_id);
} else {
add_tag(tag_id);
}
};
const clear_tags = () => setTags([]);
return {
tags,
add_tag,
remove_tag,
toggle_tag,
clear_tags,
};
};
export const useTagsFilter = () => {
const [tags] = useAtom<string[]>(tagsAtom);
-147
View File
@@ -1,147 +0,0 @@
import * as monaco from "monaco-editor";
export const dockerfile_conf: monaco.languages.LanguageConfiguration = {
brackets: [
["{", "}"],
["[", "]"],
["(", ")"],
],
autoClosingPairs: [
{ open: "{", close: "}" },
{ open: "[", close: "]" },
{ open: "(", close: ")" },
{ open: '"', close: '"' },
{ open: "'", close: "'" },
],
surroundingPairs: [
{ open: "{", close: "}" },
{ open: "[", close: "]" },
{ open: "(", close: ")" },
{ open: '"', close: '"' },
{ open: "'", close: "'" },
],
};
export const dockerfile_language = <monaco.languages.IMonarchLanguage>{
defaultToken: "",
tokenPostfix: ".dockerfile",
variable: /\${?[\w]+}?/,
tokenizer: {
root: [
{ include: "@whitespace" },
{ include: "@comment" },
[/(ONBUILD)(\s+)/, ["keyword", ""]],
[
/(ENV)(\s+)([\w]+)/,
["keyword", "", { token: "variable", next: "@arguments" }],
],
[
/(FROM|MAINTAINER|RUN|EXPOSE|ENV|ADD|ARG|VOLUME|LABEL|USER|WORKDIR|COPY|CMD|STOPSIGNAL|SHELL|HEALTHCHECK|ENTRYPOINT)/,
{ token: "keyword", next: "@arguments" },
],
],
arguments: [
{ include: "@whitespace" },
{ include: "@strings" },
[
/(@variable)/,
{
cases: {
"@eos": { token: "variable", next: "@popall" },
"@default": "variable",
},
},
],
[
/\\/,
{
cases: {
"@eos": "",
"@default": "",
},
},
],
[
/./,
{
cases: {
"@eos": { token: "", next: "@popall" },
"@default": "",
},
},
],
],
// Deal with white space, including comments
whitespace: [
[
/\s+/,
{
cases: {
"@eos": { token: "", next: "@popall" },
"@default": "",
},
},
],
],
comment: [[/(^#.*$)/, "comment", "@popall"]],
// Recognize strings, including those broken across lines with \ (but not without)
strings: [
[/\\'$/, "", "@popall"], // \' leaves @arguments at eol
[/\\'/, ""], // \' is not a string
[/'$/, "string", "@popall"],
[/'/, "string", "@stringBody"],
[/"$/, "string", "@popall"],
[/"/, "string", "@dblStringBody"],
],
stringBody: [
[
/[^\\\$']/,
{
cases: {
"@eos": { token: "string", next: "@popall" },
"@default": "string",
},
},
],
[/\\./, "string.escape"],
[/'$/, "string", "@popall"],
[/'/, "string", "@pop"],
[/(@variable)/, "variable"],
[/\\$/, "string"],
[/$/, "string", "@popall"],
],
dblStringBody: [
[
/[^\\\$"]/,
{
cases: {
"@eos": { token: "string", next: "@popall" },
"@default": "string",
},
},
],
[/\\./, "string.escape"],
[/"$/, "string", "@popall"],
[/"/, "string", "@pop"],
[/(@variable)/, "variable"],
[/\\$/, "string"],
[/$/, "string", "@popall"],
],
},
};
monaco.languages.register({ id: "dockerfile" });
monaco.languages.setLanguageConfiguration("dockerfile", dockerfile_conf);
monaco.languages.setMonarchTokensProvider("dockerfile", dockerfile_language);
+1 -3
View File
@@ -31,8 +31,6 @@ import "./theme";
// Load the parsers
import "./yaml";
import "./toml";
import "./shell"
import "./key_value";
import "./string_list";
import "./shell";
import "./dockerfile";
import "./rust";
-370
View File
@@ -1,370 +0,0 @@
import * as monaco from "monaco-editor";
export const rust_conf: monaco.languages.LanguageConfiguration = {
comments: {
lineComment: "//",
blockComment: ["/*", "*/"],
},
brackets: [
["{", "}"],
["[", "]"],
["(", ")"],
],
autoClosingPairs: [
{ open: "[", close: "]" },
{ open: "{", close: "}" },
{ open: "(", close: ")" },
{ open: '"', close: '"', notIn: ["string"] },
],
surroundingPairs: [
{ open: "{", close: "}" },
{ open: "[", close: "]" },
{ open: "(", close: ")" },
{ open: '"', close: '"' },
{ open: "'", close: "'" },
],
folding: {
markers: {
start: new RegExp("^\\s*#pragma\\s+region\\b"),
end: new RegExp("^\\s*#pragma\\s+endregion\\b"),
},
},
};
export const rust_language = <monaco.languages.IMonarchLanguage>{
tokenPostfix: ".rust",
defaultToken: "invalid",
keywords: [
"as",
"async",
"await",
"box",
"break",
"const",
"continue",
"crate",
"dyn",
"else",
"enum",
"extern",
"false",
"fn",
"for",
"if",
"impl",
"in",
"let",
"loop",
"match",
"mod",
"move",
"mut",
"pub",
"ref",
"return",
"self",
"static",
"struct",
"super",
"trait",
"true",
"try",
"type",
"unsafe",
"use",
"where",
"while",
"catch",
"default",
"union",
"static",
"abstract",
"alignof",
"become",
"do",
"final",
"macro",
"offsetof",
"override",
"priv",
"proc",
"pure",
"sizeof",
"typeof",
"unsized",
"virtual",
"yield",
],
typeKeywords: [
"Self",
"m32",
"m64",
"m128",
"f80",
"f16",
"f128",
"int",
"uint",
"float",
"char",
"bool",
"u8",
"u16",
"u32",
"u64",
"f32",
"f64",
"i8",
"i16",
"i32",
"i64",
"str",
"Option",
"Either",
"c_float",
"c_double",
"c_void",
"FILE",
"fpos_t",
"DIR",
"dirent",
"c_char",
"c_schar",
"c_uchar",
"c_short",
"c_ushort",
"c_int",
"c_uint",
"c_long",
"c_ulong",
"size_t",
"ptrdiff_t",
"clock_t",
"time_t",
"c_longlong",
"c_ulonglong",
"intptr_t",
"uintptr_t",
"off_t",
"dev_t",
"ino_t",
"pid_t",
"mode_t",
"ssize_t",
],
constants: ["true", "false", "Some", "None", "Left", "Right", "Ok", "Err"],
supportConstants: [
"EXIT_FAILURE",
"EXIT_SUCCESS",
"RAND_MAX",
"EOF",
"SEEK_SET",
"SEEK_CUR",
"SEEK_END",
"_IOFBF",
"_IONBF",
"_IOLBF",
"BUFSIZ",
"FOPEN_MAX",
"FILENAME_MAX",
"L_tmpnam",
"TMP_MAX",
"O_RDONLY",
"O_WRONLY",
"O_RDWR",
"O_APPEND",
"O_CREAT",
"O_EXCL",
"O_TRUNC",
"S_IFIFO",
"S_IFCHR",
"S_IFBLK",
"S_IFDIR",
"S_IFREG",
"S_IFMT",
"S_IEXEC",
"S_IWRITE",
"S_IREAD",
"S_IRWXU",
"S_IXUSR",
"S_IWUSR",
"S_IRUSR",
"F_OK",
"R_OK",
"W_OK",
"X_OK",
"STDIN_FILENO",
"STDOUT_FILENO",
"STDERR_FILENO",
],
supportMacros: [
"format!",
"print!",
"println!",
"panic!",
"format_args!",
"unreachable!",
"write!",
"writeln!",
],
operators: [
"!",
"!=",
"%",
"%=",
"&",
"&=",
"&&",
"*",
"*=",
"+",
"+=",
"-",
"-=",
"->",
".",
"..",
"...",
"/",
"/=",
":",
";",
"<<",
"<<=",
"<",
"<=",
"=",
"==",
"=>",
">",
">=",
">>",
">>=",
"@",
"^",
"^=",
"|",
"|=",
"||",
"_",
"?",
"#",
],
escapes: /\\([nrt0\"''\\]|x\h{2}|u\{\h{1,6}\})/,
delimiters: /[,]/,
symbols: /[\#\!\%\&\*\+\-\.\/\:\;\<\=\>\@\^\|_\?]+/,
intSuffixes: /[iu](8|16|32|64|128|size)/,
floatSuffixes: /f(32|64)/,
tokenizer: {
root: [
// Raw string literals
[
/r(#*)"/,
{ token: "string.quote", bracket: "@open", next: "@stringraw.$1" },
],
[
/[a-zA-Z][a-zA-Z0-9_]*!?|_[a-zA-Z0-9_]+/,
{
cases: {
"@typeKeywords": "keyword.type",
"@keywords": "keyword",
"@supportConstants": "keyword",
"@supportMacros": "keyword",
"@constants": "keyword",
"@default": "identifier",
},
},
],
// Designator
[/\$/, "identifier"],
// Lifetime annotations
[/'[a-zA-Z_][a-zA-Z0-9_]*(?=[^\'])/, "identifier"],
// Byte literal
[/'(\S|@escapes)'/, "string.byteliteral"],
// Strings
[/"/, { token: "string.quote", bracket: "@open", next: "@string" }],
{ include: "@numbers" },
// Whitespace + comments
{ include: "@whitespace" },
[
/@delimiters/,
{
cases: {
"@keywords": "keyword",
"@default": "delimiter",
},
},
],
[/[{}()\[\]<>]/, "@brackets"],
[/@symbols/, { cases: { "@operators": "operator", "@default": "" } }],
],
whitespace: [
[/[ \t\r\n]+/, "white"],
[/\/\*/, "comment", "@comment"],
[/\/\/.*$/, "comment"],
],
comment: [
[/[^\/*]+/, "comment"],
[/\/\*/, "comment", "@push"],
["\\*/", "comment", "@pop"],
[/[\/*]/, "comment"],
],
string: [
[/[^\\"]+/, "string"],
[/@escapes/, "string.escape"],
[/\\./, "string.escape.invalid"],
[/"/, { token: "string.quote", bracket: "@close", next: "@pop" }],
],
stringraw: [
[/[^"#]+/, { token: "string" }],
[
/"(#*)/,
{
cases: {
"$1==$S2": {
token: "string.quote",
bracket: "@close",
next: "@pop",
},
"@default": { token: "string" },
},
},
],
[/["#]/, { token: "string" }],
],
numbers: [
//Octal
[/(0o[0-7_]+)(@intSuffixes)?/, { token: "number" }],
//Binary
[/(0b[0-1_]+)(@intSuffixes)?/, { token: "number" }],
//Exponent
[
/[\d][\d_]*(\.[\d][\d_]*)?[eE][+-][\d_]+(@floatSuffixes)?/,
{ token: "number" },
],
//Float
[/\b(\d\.?[\d_]*)(@floatSuffixes)?\b/, { token: "number" }],
//Hexadecimal
[/(0x[\da-fA-F]+)_?(@intSuffixes)?/, { token: "number" }],
//Integer
[/[\d][\d_]*(@intSuffixes?)?/, { token: "number" }],
],
},
};
monaco.languages.register({ id: "rust" });
monaco.languages.setLanguageConfiguration("rust", rust_conf);
monaco.languages.setMonarchTokensProvider("rust", rust_language);
+3 -3
View File
@@ -64,13 +64,13 @@ docker compose -p komodo-dev -f dev.compose.yaml build"""
[dev-docsite]
description = "starts the documentation site (https://komo.do) in dev mode"
path = "docsite"
cmd = "yarn start"
cmd = "yarn && yarn start"
[deploy-docsite]
description = "deploys the documentation site (https://komo.do) to github pages"
path = "docsite"
cmd = "yarn deploy"
cmd = "yarn && yarn deploy"
[dev-rustdoc]
description = "starts the rustdoc site (https://docs.rs/komodo_client/latest/komodo_client/) in dev mode"
cmd = "cargo watch -s 'cargo doc --no-deps -p komodo_client' & http --quiet target/doc"
cmd = "cargo doc --no-deps -p komodo_client && http-server -p 8050 target/doc"

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