Compare commits

..
107 Commits
Author SHA1 Message Date
mbecker20 f458106263 batch Build builds 2025-03-13 00:55:29 -04:00
mbecker20 f022e83414 use Tooltip component instead of HoverCard for mobile compatibility 2025-03-13 00:01:26 -04:00
mbecker20 93ccc1ce7f update some deps 2025-03-12 23:39:39 -04:00
mbecker20 d69b7a74e7 isolate stacks / deployments with pending updates 2025-03-12 22:57:34 -04:00
wlaticandmbecker20 96cfaf3355 Update config.tsx (#358) 2025-03-12 22:21:06 -04:00
komodoandmbecker20 b7587e5426 soften tag opacity a bit 2025-03-12 22:21:06 -04:00
komodoandmbecker20 ab874211ed default new tag colors to grey 2025-03-12 22:21:06 -04:00
komodoandmbecker20 7094c4e3c5 fix sync summary count ok 2025-03-12 22:21:06 -04:00
komodoandmbecker20 386d463a0a colored tags 2025-03-12 22:21:05 -04:00
komodoandmbecker20 65fa969686 improve variable value table overflow 2025-03-12 22:21:05 -04:00
komodoandmbecker20 92099f311f use jsonwebtoken 2025-03-12 22:21:05 -04:00
komodoandmbecker20 9186d5fd50 resource sync toggle resource / variable / user group inclusion independantly 2025-03-12 22:21:05 -04:00
komodoandmbecker20 54e766f48b update deps 2025-03-12 22:21:05 -04:00
komodoandmbecker20 a1dd895b19 add KOMODO_LOCK_LOGIN_CREDENTIALS_FOR in config doc 2025-03-12 22:21:05 -04:00
komodoandmbecker20 3f767ed42e cycle the oidc client on interval to ensure up to date JWKs 2025-03-12 22:21:05 -04:00
komodoandmbecker20 4c14c33339 update docs on OIDC and client secret 2025-03-12 22:21:05 -04:00
komodoandmbecker20 ac43572acb OIDC: Support PKCE auth (secret optional) 2025-03-12 22:21:05 -04:00
komodoandmbecker20 c5451bed8e use png in topbar logo, svg quality sometimes bad 2025-03-12 22:21:05 -04:00
komodoandmbecker20 8f568ea8d1 update .devcontainer / dev docs for updated runfile 2025-03-12 22:21:05 -04:00
komodoandmbecker20 dc1062bb57 rename test.compose.yaml to dev.compose.yaml, and update runfile 2025-03-12 22:21:05 -04:00
komodoandmbecker20 a12f21ca7a rust 2024 and fmt 2025-03-12 22:21:05 -04:00
komodoandmbecker20 a0e5a86c89 add update_available query parameter to filter for only stacks /deployments with available update 2025-03-12 22:21:05 -04:00
komodoandmbecker20 2f9cf82cee Fix actions when core on https 2025-03-12 22:21:05 -04:00
komodoandmbecker20 a118696d54 add yarn install to runfile 2025-03-12 22:21:05 -04:00
mbecker20 d6abda063a stack edits on localstorage and show last deployed config 2025-03-12 22:21:05 -04:00
mbecker20 8fb4bea790 store sync edits on localstorage 2025-03-12 22:21:05 -04:00
mbecker20 8cc1d0106a rust 1.85 2025-03-12 22:21:05 -04:00
mbecker20 c0b213dfd4 clean up service_args 2025-03-12 22:21:05 -04:00
mbecker20 5ad00a41ca dev-5 fix the stack service executions 2025-03-12 22:21:05 -04:00
mbecker20 c91e107245 auto update all service deploy option 2025-03-12 22:21:05 -04:00
mbecker20 66a80c1262 Stacks executions take list of services -- Auto update only redeploys services with update 2025-03-12 22:21:05 -04:00
mbecker20 dd0d99dabc resource sync only add escaping on toml between the """ 2025-03-12 22:21:05 -04:00
mbecker20 13a0c5a2fa dev-4 2025-03-12 22:21:05 -04:00
mbecker20 177ce9dd45 update openidconnect dependency, and use reqwest rustls-tls-native-roots 2025-03-12 22:21:05 -04:00
mbecker20 d09a864453 update most deps 2025-03-12 22:21:05 -04:00
mbecker20 29be281f11 don't prune images if server not enabled 2025-03-12 22:21:05 -04:00
mbecker20 adb1a49305 make sure parent directories exist whenever writing files 2025-03-12 22:21:05 -04:00
mbecker20 014d3b2c13 ResourceSync state resolution refinement 2025-03-12 22:21:05 -04:00
mbecker20 42b1f7b6c8 revert to login screen whenever the call to check login fails 2025-03-12 22:21:05 -04:00
mbecker20 6ee707576b lock certain users username / password, prevent demo creds from being changed. 2025-03-12 22:21:05 -04:00
mbecker20 999ad9a4ce fix rand 2025-03-12 22:21:05 -04:00
mbecker20 94afb432f3 fix all clippy lints 2025-03-12 22:21:05 -04:00
mbecker20 48e871d400 standardize running commands with interpolation / output sanitizations 2025-03-12 22:21:05 -04:00
mbecker20 723853e92d Improve resource sync Execute / Pending view selector 2025-03-12 22:21:05 -04:00
mbecker20 24b2a8ab75 ResourceSync: pending view toggle between "Execute" vs "Commit" sync direction 2025-03-12 22:21:05 -04:00
mbecker20 d541c4c202 set branch on git init folder 2025-03-12 22:21:05 -04:00
mbecker20 a09ced896f init sync file new repo 2025-03-12 22:21:05 -04:00
mbecker20 d0890436c3 Stack: Fix git repo new compose file initialization 2025-03-12 22:21:05 -04:00
mbecker20 54447fe56b show provider usernames from config file 2025-03-12 22:21:05 -04:00
mbecker20 625295d50b filters wrap 2025-03-12 22:21:05 -04:00
mbecker20 e23e37ac92 give server stat charts labels 2025-03-12 22:21:05 -04:00
mbecker20 d4e058a532 improve WriteComposeContentsToHost instrument fields 2025-03-12 22:21:05 -04:00
mbecker20 3890a287d1 ServerTemplate description 2025-03-12 22:21:05 -04:00
mbecker20 e169bbbd31 dev-3 2025-03-12 22:21:05 -04:00
mbecker20 d2cb543c76 use komodo_client.subscribe_to_update_websocket, and click indicator to reconnect 2025-03-12 22:21:05 -04:00
mbecker20 59da3812a9 Fix unclear ComposePull log re #244 2025-03-12 22:21:05 -04:00
mbecker20 ce10cbe684 improve pull to git init on existing folder without .git 2025-03-12 22:21:05 -04:00
unsyncandmbecker20 dd901f7369 feature: improve tables quick actions on mobile (#312)
* feature: improve tables quick actions on mobile

* review: fix gap4

* review: use flex-wrap
2025-03-12 22:21:05 -04:00
mbecker20 7908149226 choose which stack services to include in logs 2025-03-12 22:21:05 -04:00
mbecker20 0283930207 fix api name chnage 2025-03-12 22:21:05 -04:00
mbecker20 487664a25a 1.17.0-dev-2 2025-03-12 22:21:05 -04:00
mbecker20 130ba1f54f Add all services stack log 2025-03-12 22:21:05 -04:00
mbecker20 2758b91e31 improve update indicator style and also put on home screen 2025-03-12 22:21:05 -04:00
mbecker20 d240b5c959 requery alerts more often 2025-03-12 22:21:05 -04:00
mbecker20 f8b8f76569 FIx PullStack re #302 and record docker compose config on stack deploy 2025-03-12 22:21:05 -04:00
mbecker20 d2b27294be improve First Login docs 2025-03-12 22:21:05 -04:00
unsyncandmbecker20 c44313a9f1 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
2025-03-12 22:21:05 -04:00
mbecker20 1cc967f215 add save button to config bottom 2025-03-12 22:21:05 -04:00
mbecker20 53fcd899a4 add config save button in desktop sidebar navigator 2025-03-12 22:21:05 -04:00
mbecker20 b296971c1a add donate button docsite 2025-03-12 22:21:05 -04:00
mbecker20 6a4c88f8f4 typescript subscribe_to_update_websocket 2025-03-12 22:21:05 -04:00
mbecker20 51992c477d docs new organization 2025-03-12 22:21:05 -04:00
mbecker20 2c98c6ea40 fix new compose images 2025-03-12 22:21:05 -04:00
mbecker20 14658ba722 more legible favicon 2025-03-12 22:21:05 -04:00
mbecker20 192073a12c fix login screen logo 2025-03-12 22:21:05 -04:00
mbecker20 275f204a30 dev-1 2025-03-12 22:21:05 -04:00
mbecker20 a196e1ff7f remove example from cargo toml workspace 2025-03-12 22:21:05 -04:00
mbecker20 3ef5367ed1 mbecker20 -> moghtech 2025-03-12 22:21:05 -04:00
ca2c728bf3 Remove .git from remote_url (#299)
Remove .git from remote_url

Co-authored-by: Deon Marshall <dmarshall@ccp.com.au>
2025-03-12 22:21:05 -04:00
unsyncandmbecker20 f0d22642b2 feature: interpolate secrets in custom alerter (#289)
* feature: interpolate secrets in custom alerter

* fix rust warning

* review: sanitize errors

* review: sanitize error message
2025-03-12 22:21:05 -04:00
unsyncandmbecker20 5d54876fff feature: add post_deploy command (#288)
* feature: add post_deploy command

* review: do not run post_deploy if deploy failed
2025-03-12 22:21:05 -04:00
mbecker20 c3c2f57db4 1.17.0-dev 2025-03-12 22:21:05 -04:00
unsyncandmbecker20 30043e32a4 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()
2025-03-12 22:21:05 -04:00
mbecker20 33a2897c2c update available deployment table 2025-03-12 22:21:05 -04:00
mbecker20 2ae1313170 show update available stack table 2025-03-12 22:21:05 -04:00
mbecker20 b1e38714cf finish oidc comment 2025-03-12 22:21:05 -04:00
mbecker20 43fe613f85 clean up rust client websocket subscription 2025-03-12 22:21:05 -04:00
mbecker20 6a8f46ee7a escape incoming sync backslashes (BREAKING) 2025-03-12 22:21:05 -04:00
mbecker20 cbb323fce5 rename Test Alerter button 2025-03-12 22:21:05 -04:00
mbecker20 4d85c601d3 simplify network stats 2025-03-12 22:21:05 -04:00
mbecker20 b03a4db3ce komodo-logo 2025-03-12 22:21:05 -04:00
mbecker20 a19f2afc74 higher quality / colored icons 2025-03-12 22:21:05 -04:00
mbecker20 b55e5c584b Add test alerter button 2025-03-12 22:21:05 -04:00
mbecker20 4d1a975c79 fix last axum updates 2025-03-12 22:21:05 -04:00
mbecker20 f45fb6f635 axum update :param to {param} syntax 2025-03-12 22:21:05 -04:00
mbecker20 5c6e09a48c rust 1.84.0 2025-03-12 22:21:05 -04:00
mbecker20 6ffb421662 test alert implementation 2025-03-12 22:21:05 -04:00
mbecker20 85f1cc699c add entities / message for test alerter 2025-03-12 22:21:05 -04:00
mbecker20 3c4e0b23df the komodo env file should be highest priority over additional files 2025-03-12 22:21:05 -04:00
mbecker20 a524c3ac2d clean up cors 2025-03-12 22:21:05 -04:00
mbecker20 e89a5bface just make it 1.17.0 2025-03-12 22:21:05 -04:00
mbecker20 d67130f6ee bump aws deps 2025-03-12 22:21:05 -04:00
mbecker20 d76736b71e axum to 0.8 2025-03-12 22:21:05 -04:00
mbecker20 c3857e93b6 resource2 not really a benefit 2025-03-12 22:21:05 -04:00
mbecker20 c879c393be format 2025-03-12 22:21:05 -04:00
mbecker20 7bae90b661 fmt 2025-03-12 22:21:05 -04:00
mbecker20 bef9d9397c 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
2025-03-12 22:21:05 -04:00
177 changed files with 3287 additions and 18072 deletions
Generated
+671 -942
View File
File diff suppressed because it is too large Load Diff
+22 -31
View File
@@ -8,7 +8,7 @@ members = [
]
[workspace.package]
version = "1.17.4"
version = "1.17.0-dev-7"
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.4.0", package = "slack_client_rs", default-features = false, features = ["rustls"] }
slack = { version = "0.3.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,19 +44,19 @@ mungos = "3.2.0"
svi = "1.0.1"
# ASYNC
reqwest = { version = "0.12.15", default-features = false, features = ["json", "stream", "rustls-tls-native-roots"] }
tokio = { version = "1.44.1", features = ["full"] }
tokio-util = { version = "0.7.14", features = ["io", "codec"] }
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"
futures = "0.3.31"
futures-util = "0.3.31"
arc-swap = "1.7.1"
# SERVER
tokio-tungstenite = { version = "0.26.2", features = ["rustls-tls-native-roots"] }
axum-extra = { version = "0.10.0", features = ["typed-header"] }
tower-http = { version = "0.6.2", features = ["fs", "cors"] }
axum-server = { version = "0.7.2", features = ["tls-rustls"] }
axum-server = { version = "0.7.1", features = ["tls-rustls"] }
axum = { version = "0.8.1", features = ["ws", "json", "macros"] }
tokio-tungstenite = "0.26.2"
# SER/DE
ordered_hash_map = { version = "0.4.0", features = ["serde"] }
@@ -64,64 +64,55 @@ serde = { version = "1.0.219", features = ["derive"] }
strum = { version = "0.27.1", features = ["derive"] }
serde_json = "1.0.140"
serde_yaml = "0.9.34"
serde_qs = "0.14.0"
toml = "0.8.20"
# ERROR
anyhow = "1.0.98"
anyhow = "1.0.97"
thiserror = "2.0.12"
# LOGGING
opentelemetry-otlp = { version = "0.29.0", features = ["tls-roots", "reqwest-rustls"] }
opentelemetry_sdk = { version = "0.29.0", features = ["rt-tokio"] }
opentelemetry-otlp = { version = "0.28.0", features = ["tls-roots", "reqwest-rustls"] }
opentelemetry_sdk = { version = "0.28.0", features = ["rt-tokio"] }
tracing-subscriber = { version = "0.3.19", features = ["json"] }
opentelemetry-semantic-conventions = "0.29.0"
tracing-opentelemetry = "0.30.0"
opentelemetry = "0.29.0"
opentelemetry-semantic-conventions = "0.28.0"
tracing-opentelemetry = "0.29.0"
opentelemetry = "0.28.0"
tracing = "0.1.41"
# CONFIG
clap = { version = "4.5.37", features = ["derive"] }
clap = { version = "4.5.32", features = ["derive"] }
dotenvy = "0.15.7"
envy = "0.4.2"
# CRYPTO / AUTH
uuid = { version = "1.16.0", features = ["v4", "fast-rng", "serde"] }
uuid = { version = "1.15.1", 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.26"
rustls = "0.23.23"
hmac = "0.12.1"
sha2 = "0.10.8"
rand = "0.9.1"
rand = "0.9.0"
hex = "0.4.3"
# SYSTEM
portable-pty = "0.9.0"
bollard = "0.18.1"
sysinfo = "0.34.2"
sysinfo = "0.33.1"
# CLOUD
aws-config = "1.6.1"
aws-sdk-ec2 = "1.121.1"
aws-config = "1.6.0"
aws-sdk-ec2 = "1.117.0"
aws-credential-types = "1.2.2"
## CRON
english-to-cron = "0.1.4"
chrono-tz = "0.10.3"
chrono = "0.4.40"
croner = "2.1.0"
# MISC
derive_builder = "0.20.2"
typeshare = "1.0.4"
octorust = "0.10.0"
octorust = "0.9.0"
dashmap = "6.1.0"
wildcard = "0.3.0"
colored = "3.0.0"
regex = "1.11.1"
bytes = "1.10.1"
bson = "2.14.0"
bson = "2.13.0"
+2 -2
View File
@@ -1,7 +1,7 @@
## Builds the Komodo Core and Periphery binaries
## for a specific architecture.
FROM rust:1.86.0-bullseye AS builder
FROM rust:1.85.0-bullseye AS builder
WORKDIR /builder
COPY Cargo.toml Cargo.lock ./
@@ -23,5 +23,5 @@ COPY --from=builder /builder/target/release/core /core
COPY --from=builder /builder/target/release/periphery /periphery
LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo
LABEL org.opencontainers.image.description="Komodo Binaries"
LABEL org.opencontainers.image.description="Komodo Periphery"
LABEL org.opencontainers.image.licenses=GPL-3.0
+1 -6
View File
@@ -38,11 +38,8 @@ slack.workspace = true
svi.workspace = true
# external
aws-credential-types.workspace = true
tokio-tungstenite.workspace = true
ordered_hash_map.workspace = true
english-to-cron.workspace = true
openidconnect.workspace = true
jsonwebtoken.workspace = true
axum-server.workspace = true
urlencoding.workspace = true
aws-sdk-ec2.workspace = true
@@ -53,7 +50,6 @@ tower-http.workspace = true
serde_json.workspace = true
serde_yaml.workspace = true
typeshare.workspace = true
chrono-tz.workspace = true
octorust.workspace = true
wildcard.workspace = true
arc-swap.workspace = true
@@ -64,8 +60,6 @@ futures.workspace = true
nom_pem.workspace = true
dotenvy.workspace = true
anyhow.workspace = true
croner.workspace = true
chrono.workspace = true
bcrypt.workspace = true
base64.workspace = true
rustls.workspace = true
@@ -79,4 +73,5 @@ envy.workspace = true
rand.workspace = true
hmac.workspace = true
sha2.workspace = true
jsonwebtoken.workspace = true
hex.workspace = true
+5 -4
View File
@@ -1,7 +1,7 @@
## All in one, multi stage compile + runtime Docker build for your architecture.
# Build Core
FROM rust:1.86.0-bullseye AS core-builder
FROM rust:1.85.0-bullseye AS core-builder
WORKDIR /builder
COPY Cargo.toml Cargo.lock ./
@@ -24,9 +24,10 @@ RUN cd frontend && yarn link komodo_client && yarn && yarn build
# Final Image
FROM debian:bullseye-slim
COPY ./bin/core/starship.toml /config/starship.toml
COPY ./bin/core/debian-deps.sh .
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
# Install Deps
RUN apt update && \
apt install -y git ca-certificates && \
rm -rf /var/lib/apt/lists/*
# Setup an application directory
WORKDIR /app
-14
View File
@@ -1,14 +0,0 @@
#!/bin/bash
## Core deps installer
apt-get update
apt-get install -y git curl ca-certificates
rm -rf /var/lib/apt/lists/*
# Starship prompt
curl -sS https://starship.rs/install.sh | sh -s -- --yes --bin-dir /usr/local/bin
echo 'export STARSHIP_CONFIG=/config/starship.toml' >> /root/.bashrc
echo 'eval "$(starship init bash)"' >> /root/.bashrc
+4 -3
View File
@@ -15,9 +15,10 @@ FROM ${FRONTEND_IMAGE} AS frontend
# Final Image
FROM debian:bullseye-slim
COPY ./bin/core/starship.toml /config/starship.toml
COPY ./bin/core/debian-deps.sh .
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
# Install Deps
RUN apt update && \
apt install -y git ca-certificates && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
+4 -3
View File
@@ -16,9 +16,10 @@ RUN cd frontend && yarn link komodo_client && yarn && yarn build
FROM debian:bullseye-slim
COPY ./bin/core/starship.toml /config/starship.toml
COPY ./bin/core/debian-deps.sh .
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
# Install Deps
RUN apt update && \
apt install -y git ca-certificates && \
rm -rf /var/lib/apt/lists/*
# Copy
COPY ./config/core.config.toml /config/config.toml
+1 -43
View File
@@ -189,52 +189,10 @@ pub async fn send_alert(
let link = resource_link(ResourceTargetVariant::Repo, id);
format!("{level} | Repo build for **{name}** failed\n{link}")
}
AlertData::ProcedureFailed { id, name } => {
let link = resource_link(ResourceTargetVariant::Procedure, id);
format!("{level} | Procedure **{name}** failed\n{link}")
}
AlertData::ActionFailed { id, name } => {
let link = resource_link(ResourceTargetVariant::Action, id);
format!("{level} | Action **{name}** failed\n{link}")
}
AlertData::ScheduleRun {
resource_type,
id,
name,
} => {
let link = resource_link(*resource_type, id);
format!(
"{level} | **{name}** ({resource_type}) | Scheduled run started 🕝\n{link}"
)
}
AlertData::None {} => Default::default(),
};
if !content.is_empty() {
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
))
})?;
send_message(url, &content).await?;
}
Ok(())
}
-18
View File
@@ -18,8 +18,6 @@ use crate::helpers::query::get_variables_and_secrets;
use crate::{config::core_config, state::db_client};
mod discord;
mod ntfy;
mod pushover;
mod slack;
#[instrument(level = "debug")]
@@ -130,22 +128,6 @@ 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
)
})
}
AlerterEndpoint::Pushover(PushoverAlerterEndpoint { url }) => {
pushover::send_alert(url, alert).await.with_context(|| {
format!(
"Failed to send alert to Pushover Alerter {}",
alerter.name
)
})
}
}
}
-266
View File
@@ -1,266 +0,0 @@
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::ProcedureFailed { id, name } => {
let link = resource_link(ResourceTargetVariant::Procedure, id);
format!("{level} | Procedure {name} failed\n{link}")
}
AlertData::ActionFailed { id, name } => {
let link = resource_link(ResourceTargetVariant::Action, id);
format!("{level} | Action {name} failed\n{link}")
}
AlertData::ScheduleRun {
resource_type,
id,
name,
} => {
let link = resource_link(*resource_type, id);
format!(
"{level} | {name} ({resource_type}) | Scheduled run started 🕝\n{link}"
)
}
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)
}
-270
View File
@@ -1,270 +0,0 @@
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 {name} failed\nversion: v{version}\n{link}",
)
}
AlertData::RepoBuildFailed { id, name } => {
let link = resource_link(ResourceTargetVariant::Repo, id);
format!("{level} | Repo build for {} failed\n{link}", name,)
}
AlertData::ProcedureFailed { id, name } => {
let link = resource_link(ResourceTargetVariant::Procedure, id);
format!("{level} | Procedure {name} failed\n{link}")
}
AlertData::ActionFailed { id, name } => {
let link = resource_link(ResourceTargetVariant::Action, id);
format!("{level} | Action {name} failed\n{link}")
}
AlertData::ScheduleRun {
resource_type,
id,
name,
} => {
let link = resource_link(*resource_type, id);
format!(
"{level} | {name} ({resource_type}) | Scheduled run started 🕝\n{link}"
)
}
AlertData::None {} => Default::default(),
};
if !content.is_empty() {
send_message(url, content).await?;
}
Ok(())
}
async fn send_message(
url: &str,
content: String,
) -> anyhow::Result<()> {
// pushover needs all information to be encoded in the URL. At minimum they need
// the user key, the application token, and the message (url encoded).
// other optional params here: https://pushover.net/api (just add them to the
// webhook url along with the application token and the user key).
let content = [("message", content)];
let response = http_client()
.post(url)
.form(&content)
.send()
.await
.context("Failed to send message")?;
let status = response.status();
if status.is_success() {
debug!("pushover alert sent successfully: {}", status);
Ok(())
} else {
let text = response.text().await.with_context(|| {
format!(
"Failed to send message to pushover | {} | failed to get response text",
status
)
})?;
Err(anyhow!(
"Failed to send message to pushover | {} | {}",
status,
text
))
}
}
fn http_client() -> &'static reqwest::Client {
static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
CLIENT.get_or_init(reqwest::Client::new)
}
+6 -61
View File
@@ -373,7 +373,9 @@ pub async fn send_alert(
let text = format!("{level} | Build {name} has failed");
let blocks = vec![
Block::header(text.clone()),
Block::section(format!("version: *v{version}*",)),
Block::section(format!(
"build name: *{name}*\nversion: *v{version}*",
)),
Block::section(resource_link(
ResourceTargetVariant::Build,
id,
@@ -386,6 +388,7 @@ pub async fn send_alert(
format!("{level} | Repo build for *{name}* has *failed*");
let blocks = vec![
Block::header(text.clone()),
Block::section(format!("repo name: *{name}*",)),
Block::section(resource_link(
ResourceTargetVariant::Repo,
id,
@@ -393,69 +396,11 @@ pub async fn send_alert(
];
(text, blocks.into())
}
AlertData::ProcedureFailed { id, name } => {
let text = format!("{level} | Procedure *{name}* has *failed*");
let blocks = vec![
Block::header(text.clone()),
Block::section(resource_link(
ResourceTargetVariant::Procedure,
id,
)),
];
(text, blocks.into())
}
AlertData::ActionFailed { id, name } => {
let text = format!("{level} | Action *{name}* has *failed*");
let blocks = vec![
Block::header(text.clone()),
Block::section(resource_link(
ResourceTargetVariant::Action,
id,
)),
];
(text, blocks.into())
}
AlertData::ScheduleRun {
resource_type,
id,
name,
} => {
let text = format!(
"{level} | *{name}* ({resource_type}) | Scheduled run started 🕝"
);
let blocks = vec![
Block::header(text.clone()),
Block::section(resource_link(*resource_type, id)),
];
(text, blocks.into())
}
AlertData::None {} => Default::default(),
};
if !text.is_empty() {
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
))
})?;
let slack = ::slack::Client::new(url);
slack.send_message(text, blocks).await?;
}
Ok(())
}
+2 -28
View File
@@ -13,13 +13,8 @@ use komodo_client::{
user::{CreateApiKey, CreateApiKeyResponse, DeleteApiKey},
},
entities::{
action::Action,
alert::{Alert, AlertData, SeverityLevel},
config::core::CoreConfig,
komodo_timestamp,
permission::PermissionLevel,
update::Update,
user::action_user,
action::Action, config::core::CoreConfig,
permission::PermissionLevel, update::Update, user::action_user,
},
};
use mungos::{by_id::update_one_by_id, mongodb::bson::to_document};
@@ -27,7 +22,6 @@ use resolver_api::Resolve;
use tokio::fs;
use crate::{
alert::send_alerts,
api::{execute::ExecuteRequest, user::UserArgs},
config::core_config,
helpers::{
@@ -184,26 +178,6 @@ impl Resolve<ExecuteArgs> for RunAction {
update_update(update.clone()).await?;
if !update.success && action.config.failure_alert {
warn!("action unsuccessful, alerting...");
let target = update.target.clone();
tokio::spawn(async move {
let alert = Alert {
id: Default::default(),
target,
ts: komodo_timestamp(),
resolved_ts: Some(komodo_timestamp()),
resolved: true,
level: SeverityLevel::Warning,
data: AlertData::ActionFailed {
id: action.id,
name: action.name,
},
};
send_alerts(&[alert]).await
});
}
Ok(update)
}
}
+89 -87
View File
@@ -86,13 +86,7 @@ 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());
@@ -116,6 +110,14 @@ 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,
@@ -175,6 +177,7 @@ impl Resolve<ExecuteArgs> for RunBuild {
});
// GET BUILDER PERIPHERY
let (periphery, cleanup_data) = match get_builder_periphery(
build.name.clone(),
Some(build.config.version),
@@ -200,8 +203,9 @@ impl Resolve<ExecuteArgs> for RunBuild {
}
};
// INTERPOLATE VARIABLES
// CLONE REPO
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();
@@ -212,34 +216,6 @@ 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,
@@ -251,57 +227,84 @@ impl Resolve<ExecuteArgs> for RunBuild {
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
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) {
// RUN BUILD
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 res = tokio::select! {
res = periphery
.request(api::build::Build {
@@ -318,7 +321,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(cleanup_data, &mut update)
cleanup_builder_instance(periphery, cleanup_data, &mut update)
.await;
return handle_early_return(update, build.id, build.name, true).await
},
@@ -362,9 +365,8 @@ impl Resolve<ExecuteArgs> for RunBuild {
// stop the cancel listening task from going forever
cancel.cancel();
// If building on temporary cloud server (AWS),
// this will terminate the server.
cleanup_builder_instance(cleanup_data, &mut update).await;
cleanup_builder_instance(periphery, cleanup_data, &mut update)
.await;
// Need to manually update the update before cache refresh,
// and before broadcast with add_update.
+2 -27
View File
@@ -6,12 +6,8 @@ use komodo_client::{
BatchExecutionResponse, BatchRunProcedure, RunProcedure,
},
entities::{
alert::{Alert, AlertData, SeverityLevel},
komodo_timestamp,
permission::PermissionLevel,
procedure::Procedure,
update::Update,
user::User,
permission::PermissionLevel, procedure::Procedure,
update::Update, user::User,
},
};
use mungos::{by_id::update_one_by_id, mongodb::bson::to_document};
@@ -19,7 +15,6 @@ use resolver_api::Resolve;
use tokio::sync::Mutex;
use crate::{
alert::send_alerts,
helpers::{procedure::execute_procedure, update::update_update},
resource::{self, refresh_procedure_state_cache},
state::{action_states, db_client},
@@ -142,26 +137,6 @@ fn resolve_inner(
update_update(update.clone()).await?;
if !update.success && procedure.config.failure_alert {
warn!("procedure unsuccessful, alerting...");
let target = update.target.clone();
tokio::spawn(async move {
let alert = Alert {
id: Default::default(),
target,
ts: komodo_timestamp(),
resolved_ts: Some(komodo_timestamp()),
resolved: true,
level: SeverityLevel::Warning,
data: AlertData::ProcedureFailed {
id: procedure.id,
name: procedure.name,
},
};
send_alerts(&[alert]).await
});
}
Ok(update)
})
}
+3 -4
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(cleanup_data, &mut update)
cleanup_builder_instance(periphery, cleanup_data, &mut update)
.await;
info!("builder cleaned up");
return handle_builder_early_return(update, repo.id, repo.name, true).await
@@ -506,9 +506,8 @@ impl Resolve<ExecuteArgs> for BuildRepo {
// stop the cancel listening task from going forever
cancel.cancel();
// If building on temporary cloud server (AWS),
// this will terminate the server.
cleanup_builder_instance(cleanup_data, &mut update).await;
cleanup_builder_instance(periphery, cleanup_data, &mut update)
.await;
// Need to manually update the update before cache refresh,
// and before broadcast with add_update.
-2
View File
@@ -116,14 +116,12 @@ enum ReadRequest {
InspectDockerImage(InspectDockerImage),
ListDockerImageHistory(ListDockerImageHistory),
InspectDockerVolume(InspectDockerVolume),
GetDockerContainersSummary(GetDockerContainersSummary),
ListAllDockerContainers(ListAllDockerContainers),
ListDockerContainers(ListDockerContainers),
ListDockerNetworks(ListDockerNetworks),
ListDockerImages(ListDockerImages),
ListDockerVolumes(ListDockerVolumes),
ListComposeProjects(ListComposeProjects),
ListTerminals(ListTerminals),
// ==== DEPLOYMENT ====
GetDeploymentsSummary(GetDeploymentsSummary),
+31 -112
View File
@@ -14,18 +14,14 @@ use komodo_client::{
ResourceTarget,
deployment::Deployment,
docker::{
container::{
Container, ContainerListItem, ContainerStateStatusEnum,
},
container::{Container, ContainerListItem},
image::{Image, ImageHistoryResponseItem},
network::Network,
volume::Volume,
},
komodo_timestamp,
permission::PermissionLevel,
server::{
Server, ServerActionState, ServerListItem, ServerState,
TerminalInfo,
},
stack::{Stack, StackServiceNames},
stats::{SystemInformation, SystemProcess},
@@ -47,10 +43,7 @@ use resolver_api::Resolve;
use tokio::sync::Mutex;
use crate::{
helpers::{
periphery_client,
query::{get_all_tags, get_system_info},
},
helpers::{periphery_client, query::get_all_tags},
resource,
stack::compose_container_match_regex,
state::{action_states, db_client, server_status_cache},
@@ -203,6 +196,16 @@ impl Resolve<ReadArgs> for GetServerActionState {
}
}
// This protects the peripheries from spam requests
const SYSTEM_INFO_EXPIRY: u128 = FIFTEEN_SECONDS_MS;
type SystemInfoCache =
Mutex<HashMap<String, Arc<(SystemInformation, u128)>>>;
fn system_info_cache() -> &'static SystemInfoCache {
static SYSTEM_INFO_CACHE: OnceLock<SystemInfoCache> =
OnceLock::new();
SYSTEM_INFO_CACHE.get_or_init(Default::default)
}
impl Resolve<ReadArgs> for GetSystemInformation {
async fn resolve(
self,
@@ -214,7 +217,25 @@ impl Resolve<ReadArgs> for GetSystemInformation {
PermissionLevel::Read,
)
.await?;
get_system_info(&server).await.map_err(Into::into)
let mut lock = system_info_cache().lock().await;
let res = match lock.get(&server.id) {
Some(cached) if cached.1 > unix_timestamp_ms() => {
cached.0.clone()
}
_ => {
let stats = periphery_client(&server)?
.request(periphery::stats::GetSystemInformation {})
.await?;
lock.insert(
server.id,
(stats.clone(), unix_timestamp_ms() + SYSTEM_INFO_EXPIRY)
.into(),
);
stats
}
};
Ok(res)
}
}
@@ -392,45 +413,6 @@ 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,
@@ -789,66 +771,3 @@ impl Resolve<ReadArgs> for ListComposeProjects {
}
}
}
#[derive(Default)]
struct TerminalCacheItem {
list: Vec<TerminalInfo>,
ttl: i64,
}
const TERMINAL_CACHE_TIMEOUT: i64 = 30_000;
#[derive(Default)]
struct TerminalCache(
std::sync::Mutex<
HashMap<String, Arc<tokio::sync::Mutex<TerminalCacheItem>>>,
>,
);
impl TerminalCache {
fn get_or_insert(
&self,
server_id: String,
) -> Arc<tokio::sync::Mutex<TerminalCacheItem>> {
if let Some(cached) =
self.0.lock().unwrap().get(&server_id).cloned()
{
return cached;
}
let to_cache =
Arc::new(tokio::sync::Mutex::new(TerminalCacheItem::default()));
self.0.lock().unwrap().insert(server_id, to_cache.clone());
to_cache
}
}
fn terminals_cache() -> &'static TerminalCache {
static TERMINALS: OnceLock<TerminalCache> = OnceLock::new();
TERMINALS.get_or_init(Default::default)
}
impl Resolve<ReadArgs> for ListTerminals {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListTerminalsResponse> {
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read,
)
.await?;
let cache = terminals_cache().get_or_insert(server.id.clone());
let mut cache = cache.lock().await;
if self.fresh || komodo_timestamp() > cache.ttl {
cache.list = periphery_client(&server)?
.request(periphery_client::api::terminal::ListTerminals {})
.await
.context("Failed to get fresh terminal list")?;
cache.ttl = komodo_timestamp() + TERMINAL_CACHE_TIMEOUT;
Ok(cache.list.clone())
} else {
Ok(cache.list.clone())
}
}
}
+36 -336
View File
@@ -1,17 +1,12 @@
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, FileContents, NoData, Operation, all_logs_success,
CloneArgs, NoData,
build::{Build, BuildInfo, PartialBuildConfig},
builder::{Builder, BuilderConfig},
config::core::CoreConfig,
permission::PermissionLevel,
server::ServerState,
update::Update,
},
};
@@ -20,22 +15,11 @@ 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, periphery_client,
query::get_server_with_state,
update::{add_update, make_update},
},
helpers::git_token,
resource,
state::{db_client, github_client},
};
@@ -104,184 +88,6 @@ 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",
@@ -301,104 +107,55 @@ impl Resolve<WriteArgs> for RefreshBuildCache {
)
.await?;
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();
if build.config.repo.is_empty()
|| build.config.git_provider.is_empty()
{
// Nothing to do here
return Ok(NoData {});
}
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 config = core_config();
let access_token = if let Some(username) = &clone_args.account {
git_token(&clone_args.provider, username, |https| {
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| {
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
};
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)
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 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,
};
@@ -419,63 +176,6 @@ 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(_) => {
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(
+4 -4
View File
@@ -81,9 +81,6 @@ pub enum WriteRequest {
UpdateServer(UpdateServer),
RenameServer(RenameServer),
CreateNetwork(CreateNetwork),
CreateTerminal(CreateTerminal),
DeleteTerminal(DeleteTerminal),
DeleteAllTerminals(DeleteAllTerminals),
// ==== DEPLOYMENT ====
CreateDeployment(CreateDeployment),
@@ -99,7 +96,6 @@ pub enum WriteRequest {
DeleteBuild(DeleteBuild),
UpdateBuild(UpdateBuild),
RenameBuild(RenameBuild),
WriteBuildFileContents(WriteBuildFileContents),
RefreshBuildCache(RefreshBuildCache),
CreateBuildWebhook(CreateBuildWebhook),
DeleteBuildWebhook(DeleteBuildWebhook),
@@ -211,6 +207,10 @@ async fn handler(
.await
.context("failure in spawned task");
if let Err(e) = &res {
warn!("/write request {req_id} spawn error: {e:#}");
}
res?
}
+1 -80
View File
@@ -1,9 +1,8 @@
use anyhow::Context;
use formatting::format_serror;
use komodo_client::{
api::write::*,
entities::{
NoData, Operation,
Operation,
permission::PermissionLevel,
server::Server,
update::{Update, UpdateStatus},
@@ -102,81 +101,3 @@ impl Resolve<WriteArgs> for CreateNetwork {
Ok(update)
}
}
impl Resolve<WriteArgs> for CreateTerminal {
#[instrument(name = "CreateTerminal", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<NoData> {
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Write,
)
.await?;
let periphery = periphery_client(&server)?;
periphery
.request(api::terminal::CreateTerminal {
name: self.name,
command: self.command,
recreate: self.recreate,
})
.await
.context("Failed to create terminal on periphery")?;
Ok(NoData {})
}
}
impl Resolve<WriteArgs> for DeleteTerminal {
#[instrument(name = "DeleteTerminal", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<NoData> {
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Write,
)
.await?;
let periphery = periphery_client(&server)?;
periphery
.request(api::terminal::DeleteTerminal {
terminal: self.terminal,
})
.await
.context("Failed to delete terminal on periphery")?;
Ok(NoData {})
}
}
impl Resolve<WriteArgs> for DeleteAllTerminals {
#[instrument(name = "DeleteAllTerminals", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<NoData> {
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Write,
)
.await?;
let periphery = periphery_client(&server)?;
periphery
.request(api::terminal::DeleteAllTerminals {})
.await
.context("Failed to delete all terminals on periphery")?;
Ok(NoData {})
}
}
+1 -1
View File
@@ -211,7 +211,7 @@ impl Resolve<WriteArgs> for WriteStackFileContents {
}
update.finalize();
update.id = add_update(update.clone()).await?;
add_update(update.clone()).await?;
Ok(update)
}
+33 -36
View File
@@ -132,38 +132,6 @@ 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,
@@ -330,7 +298,7 @@ async fn write_sync_file_contents_git(
.await
{
update.push_error_log(
"Refresh sync pending",
"Refresh failed",
format_serror(&e.error.into()),
);
}
@@ -341,6 +309,37 @@ 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> {
@@ -829,9 +828,7 @@ impl Resolve<WriteArgs> for RefreshResourceSyncPending {
.context("failed to open existing pending resource sync updates alert")
.inspect_err(|e| warn!("{e:#}"))
.ok();
if sync.config.pending_alert {
send_alerts(&[alert]).await;
}
send_alerts(&[alert]).await;
}
// CLOSE ALERT
(Some(existing), false) => {
+1 -3
View File
@@ -5,8 +5,6 @@ pub mod hetzner;
#[derive(Debug)]
pub enum BuildCleanupData {
/// Nothing to clean up
Server,
/// Clean up AWS instance
Server { repo_name: String },
Aws { instance_id: String, region: String },
}
-2
View File
@@ -139,7 +139,6 @@ 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
@@ -194,7 +193,6 @@ pub fn core_config() -> &'static CoreConfig {
stdio: env
.komodo_logging_stdio
.unwrap_or(config.logging.stdio),
pretty: env.komodo_logging_pretty.unwrap_or(config.logging.pretty),
otlp_endpoint: env
.komodo_logging_otlp_endpoint
.unwrap_or(config.logging.otlp_endpoint),
+18 -5
View File
@@ -61,7 +61,12 @@ pub async fn get_builder_periphery(
.health_check()
.await
.context("Url Builder failed health check")?;
Ok((periphery, BuildCleanupData::Server))
Ok((
periphery,
BuildCleanupData::Server {
repo_name: resource_name,
},
))
}
BuilderConfig::Server(config) => {
if config.server_id.is_empty() {
@@ -69,7 +74,12 @@ pub async fn get_builder_periphery(
}
let server = resource::get::<Server>(&config.server_id).await?;
let periphery = periphery_client(&server)?;
Ok((periphery, BuildCleanupData::Server))
Ok((
periphery,
BuildCleanupData::Server {
repo_name: resource_name,
},
))
}
BuilderConfig::Aws(config) => {
get_aws_builder(&resource_name, version, config, update).await
@@ -169,14 +179,17 @@ async fn get_aws_builder(
)
}
#[instrument(skip(update))]
#[instrument(skip(periphery, update))]
pub async fn cleanup_builder_instance(
periphery: PeripheryClient,
cleanup_data: BuildCleanupData,
update: &mut Update,
) {
match cleanup_data {
BuildCleanupData::Server => {
// Nothing to clean up
BuildCleanupData::Server { repo_name } => {
let _ = periphery
.request(api::git::DeleteRepo { name: repo_name })
.await;
}
BuildCleanupData::Aws {
instance_id,
+2 -9
View File
@@ -78,9 +78,6 @@ 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 })
@@ -146,11 +143,7 @@ pub fn periphery_client(
let client = PeripheryClient::new(
&server.config.address,
if server.config.passkey.is_empty() {
&core_config().passkey
} else {
&server.config.passkey
},
&core_config().passkey,
Duration::from_secs(server.config.timeout_seconds as u64),
);
@@ -359,4 +352,4 @@ pub async fn ensure_first_server_and_builder() {
e.error
);
}
}
}
+1 -44
View File
@@ -1,11 +1,6 @@
use std::{
collections::HashMap,
str::FromStr,
sync::{Arc, OnceLock},
};
use std::{collections::HashMap, str::FromStr};
use anyhow::{Context, anyhow};
use async_timing_util::{ONE_MIN_MS, unix_timestamp_ms};
use komodo_client::entities::{
Operation, ResourceTarget, ResourceTargetVariant,
action::Action,
@@ -20,7 +15,6 @@ use komodo_client::entities::{
server::{Server, ServerState},
server_template::ServerTemplate,
stack::{Stack, StackServiceNames, StackState},
stats::SystemInformation,
sync::ResourceSync,
tag::Tag,
update::Update,
@@ -35,8 +29,6 @@ use mungos::{
options::FindOneOptions,
},
};
use periphery_client::api::stats;
use tokio::sync::Mutex;
use crate::{
config::core_config,
@@ -45,8 +37,6 @@ use crate::{
state::{db_client, deployment_status_cache, stack_status_cache},
};
use super::periphery_client;
// user: Id or username
#[instrument(level = "debug")]
pub async fn get_user(user: &str) -> anyhow::Result<User> {
@@ -392,36 +382,3 @@ pub async fn get_variables_and_secrets()
Ok(VariablesAndSecrets { variables, secrets })
}
// This protects the peripheries from spam requests
const SYSTEM_INFO_EXPIRY: u128 = ONE_MIN_MS;
type SystemInfoCache =
Mutex<HashMap<String, Arc<(SystemInformation, u128)>>>;
fn system_info_cache() -> &'static SystemInfoCache {
static SYSTEM_INFO_CACHE: OnceLock<SystemInfoCache> =
OnceLock::new();
SYSTEM_INFO_CACHE.get_or_init(Default::default)
}
pub async fn get_system_info(
server: &Server,
) -> anyhow::Result<SystemInformation> {
let mut lock = system_info_cache().lock().await;
let res = match lock.get(&server.id) {
Some(cached) if cached.1 > unix_timestamp_ms() => {
cached.0.clone()
}
_ => {
let stats = periphery_client(server)?
.request(stats::GetSystemInformation {})
.await?;
lock.insert(
server.id.clone(),
(stats.clone(), unix_timestamp_ms() + SYSTEM_INFO_EXPIRY)
.into(),
);
stats
}
};
Ok(res)
}
+3 -12
View File
@@ -23,7 +23,6 @@ mod helpers;
mod listener;
mod monitor;
mod resource;
mod schedule;
mod stack;
mod state;
mod sync;
@@ -34,12 +33,6 @@ async fn app() -> anyhow::Result<()> {
dotenvy::dotenv().ok();
let config = core_config();
logger::init(&config.logging)?;
if let Err(e) =
rustls::crypto::aws_lc_rs::default_provider().install_default()
{
error!("Failed to install default crypto provider | {e:?}");
std::process::exit(1);
};
info!("Komodo Core version: v{}", env!("CARGO_PKG_VERSION"));
info!("{:?}", config.sanitized());
@@ -66,7 +59,6 @@ async fn app() -> anyhow::Result<()> {
resource::spawn_repo_state_refresh_loop();
resource::spawn_procedure_state_refresh_loop();
resource::spawn_action_state_refresh_loop();
schedule::spawn_schedule_executor();
helpers::prune::spawn_prune_loop();
// Setup static frontend services
@@ -94,10 +86,9 @@ async fn app() -> anyhow::Result<()> {
)
.into_make_service();
let addr =
format!("{}:{}", core_config().bind_ip, core_config().port);
let socket_addr = SocketAddr::from_str(&addr)
.context("failed to parse listen address")?;
let socket_addr =
SocketAddr::from_str(&format!("0.0.0.0:{}", core_config().port))
.context("failed to parse socket addr")?;
if config.ssl_enabled {
info!("🔒 Core SSL Enabled");
+4 -20
View File
@@ -2,7 +2,7 @@ use std::time::Duration;
use anyhow::Context;
use komodo_client::entities::{
Operation, ResourceTarget, ResourceTargetVariant,
Operation, ResourceTargetVariant,
action::{
Action, ActionConfig, ActionConfigDiff, ActionInfo,
ActionListItem, ActionListItemInfo, ActionQuerySpecifics,
@@ -17,12 +17,7 @@ use mungos::{
mongodb::{Collection, bson::doc, options::FindOneOptions},
};
use crate::{
schedule::{
cancel_schedule, get_schedule_item_info, update_schedule,
},
state::{action_state_cache, action_states, db_client},
};
use crate::state::{action_state_cache, action_states, db_client};
impl super::KomodoResource for Action {
type Config = ActionConfig;
@@ -36,10 +31,6 @@ impl super::KomodoResource for Action {
ResourceTargetVariant::Action
}
fn resource_target(id: impl Into<String>) -> ResourceTarget {
ResourceTarget::Action(id.into())
}
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().actions
@@ -49,9 +40,6 @@ impl super::KomodoResource for Action {
action: Resource<Self::Config, Self::Info>,
) -> Self::ListItem {
let state = get_action_state(&action.id).await;
let (next_scheduled_run, schedule_error) = get_schedule_item_info(
&ResourceTarget::Action(action.id.clone()),
);
ActionListItem {
name: action.name,
id: action.id,
@@ -60,8 +48,6 @@ impl super::KomodoResource for Action {
info: ActionListItemInfo {
state,
last_run_at: action.info.last_run_at,
next_scheduled_run,
schedule_error,
},
}
}
@@ -97,10 +83,9 @@ impl super::KomodoResource for Action {
}
async fn post_create(
created: &Resource<Self::Config, Self::Info>,
_created: &Resource<Self::Config, Self::Info>,
_update: &mut Update,
) -> anyhow::Result<()> {
update_schedule(created);
refresh_action_state_cache().await;
Ok(())
}
@@ -146,10 +131,9 @@ impl super::KomodoResource for Action {
}
async fn post_delete(
resource: &Resource<Self::Config, Self::Info>,
_resource: &Resource<Self::Config, Self::Info>,
_update: &mut Update,
) -> anyhow::Result<()> {
cancel_schedule(&ResourceTarget::Action(resource.id.clone()));
Ok(())
}
}
+1 -5
View File
@@ -1,6 +1,6 @@
use derive_variants::ExtractVariant;
use komodo_client::entities::{
Operation, ResourceTarget, ResourceTargetVariant,
Operation, ResourceTargetVariant,
alerter::{
Alerter, AlerterConfig, AlerterConfigDiff, AlerterListItem,
AlerterListItemInfo, AlerterQuerySpecifics, PartialAlerterConfig,
@@ -25,10 +25,6 @@ impl super::KomodoResource for Alerter {
ResourceTargetVariant::Alerter
}
fn resource_target(id: impl Into<String>) -> ResourceTarget {
ResourceTarget::Alerter(id.into())
}
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().alerters
+18 -44
View File
@@ -1,32 +1,26 @@
use std::time::Duration;
use anyhow::Context;
use formatting::format_serror;
use komodo_client::{
api::write::RefreshBuildCache,
entities::{
Operation, ResourceTarget, 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},
use komodo_client::entities::{
Operation, ResourceTargetVariant,
build::{
Build, BuildConfig, BuildConfigDiff, BuildInfo, BuildListItem,
BuildListItemInfo, BuildQuerySpecifics, BuildState,
PartialBuildConfig,
},
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},
@@ -44,10 +38,6 @@ impl super::KomodoResource for Build {
ResourceTargetVariant::Build
}
fn resource_target(id: impl Into<String>) -> ResourceTarget {
ResourceTarget::Build(id.into())
}
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().builds
@@ -66,13 +56,10 @@ impl super::KomodoResource for Build {
last_built_at: build.info.last_built_at,
version: build.config.version,
builder_id: build.config.builder_id,
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,
),
git_provider: build.config.git_provider,
image_registry_domain: build.config.image_registry.domain,
repo: build.config.repo,
branch: build.config.branch,
built_hash: build.info.built_hash,
latest_hash: build.info.latest_hash,
state,
@@ -109,23 +96,10 @@ 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 -5
View File
@@ -1,6 +1,6 @@
use anyhow::Context;
use komodo_client::entities::{
MergePartial, Operation, ResourceTarget, ResourceTargetVariant,
MergePartial, Operation, ResourceTargetVariant,
builder::{
Builder, BuilderConfig, BuilderConfigDiff, BuilderConfigVariant,
BuilderListItem, BuilderListItemInfo, BuilderQuerySpecifics,
@@ -31,10 +31,6 @@ impl super::KomodoResource for Builder {
ResourceTargetVariant::Builder
}
fn resource_target(id: impl Into<String>) -> ResourceTarget {
ResourceTarget::Builder(id.into())
}
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().builders
+1 -5
View File
@@ -1,7 +1,7 @@
use anyhow::Context;
use formatting::format_serror;
use komodo_client::entities::{
Operation, ResourceTarget, ResourceTargetVariant,
Operation, ResourceTargetVariant,
build::Build,
deployment::{
Deployment, DeploymentConfig, DeploymentConfigDiff,
@@ -43,10 +43,6 @@ impl super::KomodoResource for Deployment {
ResourceTargetVariant::Deployment
}
fn resource_target(id: impl Into<String>) -> ResourceTarget {
ResourceTarget::Deployment(id.into())
}
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().deployments
+8 -35
View File
@@ -29,7 +29,7 @@ use mungos::{
options::FindOptions,
},
};
use partial_derive2::{Diff, MaybeNone, PartialDiff};
use partial_derive2::{Diff, FieldDiff, MaybeNone, PartialDiff};
use resolver_api::Resolve;
use serde::{Serialize, de::DeserializeOwned};
@@ -107,7 +107,6 @@ pub trait KomodoResource {
type QuerySpecifics: AddFilters + Default + std::fmt::Debug;
fn resource_type() -> ResourceTargetVariant;
fn resource_target(id: impl Into<String>) -> ResourceTarget;
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>;
@@ -694,17 +693,13 @@ pub async fn update<T: KomodoResource>(
return Ok(resource);
}
// Leave this Result unhandled for now
let prev_toml = ExportResourcesToToml {
targets: vec![T::resource_target(&resource.id)],
..Default::default()
let mut diff_log = String::from("diff");
for FieldDiff { field, from, to } in diff.iter_field_diffs() {
diff_log.push_str(&format!(
"\n\n<span class=\"text-muted-foreground\">field</span>: '{field}'\n<span class=\"text-muted-foreground\">from</span>: <span class=\"text-red-700 dark:text-red-400\">{from}</span>\n<span class=\"text-muted-foreground\">to</span>: <span class=\"text-green-700 dark:text-green-400\">{to}</span>",
));
}
.resolve(&ReadArgs {
user: system_user().to_owned(),
})
.await
.map_err(|e| e.error)
.context("Failed to export resource toml before update");
// This minimizes the update against the existing config
let config: T::PartialConfig = diff.into();
@@ -720,35 +715,13 @@ pub async fn update<T: KomodoResource>(
.await
.context("failed to update resource on database")?;
let curr_toml = ExportResourcesToToml {
targets: vec![T::resource_target(&id)],
..Default::default()
}
.resolve(&ReadArgs {
user: system_user().to_owned(),
})
.await
.map_err(|e| e.error)
.context("Failed to export resource toml after update");
let mut update = make_update(
resource_target::<T>(id),
T::update_operation(),
user,
);
match prev_toml {
Ok(res) => update.prev_toml = res.toml,
Err(e) => update
// These logs are pushed with success == true, so user still knows the update was succesful.
.push_simple_log("Failed export", format_serror(&e.into())),
}
match curr_toml {
Ok(res) => update.current_toml = res.toml,
Err(e) => update
// These logs are pushed with success == true, so user still knows the update was succesful.
.push_simple_log("Failed export", format_serror(&e.into())),
}
update.push_simple_log("update config", diff_log);
let updated = get::<T>(id_or_name).await?;
+3 -17
View File
@@ -4,7 +4,7 @@ use anyhow::{Context, anyhow};
use komodo_client::{
api::execute::Execution,
entities::{
Operation, ResourceTarget, ResourceTargetVariant,
Operation, ResourceTargetVariant,
action::Action,
alerter::Alerter,
build::Build,
@@ -31,9 +31,6 @@ use mungos::{
use crate::{
config::core_config,
schedule::{
cancel_schedule, get_schedule_item_info, update_schedule,
},
state::{action_states, db_client, procedure_state_cache},
};
@@ -49,10 +46,6 @@ impl super::KomodoResource for Procedure {
ResourceTargetVariant::Procedure
}
fn resource_target(id: impl Into<String>) -> ResourceTarget {
ResourceTarget::Procedure(id.into())
}
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().procedures
@@ -62,9 +55,6 @@ impl super::KomodoResource for Procedure {
procedure: Resource<Self::Config, Self::Info>,
) -> Self::ListItem {
let state = get_procedure_state(&procedure.id).await;
let (next_scheduled_run, schedule_error) = get_schedule_item_info(
&ResourceTarget::Procedure(procedure.id.clone()),
);
ProcedureListItem {
name: procedure.name,
id: procedure.id,
@@ -73,8 +63,6 @@ impl super::KomodoResource for Procedure {
info: ProcedureListItemInfo {
stages: procedure.config.stages.len() as i64,
state,
next_scheduled_run,
schedule_error,
},
}
}
@@ -106,10 +94,9 @@ impl super::KomodoResource for Procedure {
}
async fn post_create(
created: &Resource<Self::Config, Self::Info>,
_created: &Resource<Self::Config, Self::Info>,
_update: &mut Update,
) -> anyhow::Result<()> {
update_schedule(created);
refresh_procedure_state_cache().await;
Ok(())
}
@@ -155,10 +142,9 @@ impl super::KomodoResource for Procedure {
}
async fn post_delete(
resource: &Resource<Self::Config, Self::Info>,
_resource: &Resource<Self::Config, Self::Info>,
_update: &mut Update,
) -> anyhow::Result<()> {
cancel_schedule(&ResourceTarget::Procedure(resource.id.clone()));
Ok(())
}
}
+1 -6
View File
@@ -3,7 +3,7 @@ use std::time::Duration;
use anyhow::Context;
use formatting::format_serror;
use komodo_client::entities::{
Operation, ResourceTarget, ResourceTargetVariant,
Operation, ResourceTargetVariant,
builder::Builder,
permission::PermissionLevel,
repo::{
@@ -44,10 +44,6 @@ impl super::KomodoResource for Repo {
ResourceTargetVariant::Repo
}
fn resource_target(id: impl Into<String>) -> ResourceTarget {
ResourceTarget::Repo(id.into())
}
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().repos
@@ -174,7 +170,6 @@ impl super::KomodoResource for Repo {
} else {
repo.config.path.clone()
},
is_build: false,
})
.await
{
+1 -11
View File
@@ -1,6 +1,6 @@
use anyhow::Context;
use komodo_client::entities::{
Operation, ResourceTarget, ResourceTargetVariant, komodo_timestamp,
Operation, ResourceTargetVariant, komodo_timestamp,
resource::Resource,
server::{
PartialServerConfig, Server, ServerConfig, ServerConfigDiff,
@@ -13,7 +13,6 @@ use mungos::mongodb::{Collection, bson::doc};
use crate::{
config::core_config,
helpers::query::get_system_info,
monitor::update_cache_for_server,
state::{action_states, db_client, server_status_cache},
};
@@ -30,10 +29,6 @@ impl super::KomodoResource for Server {
ResourceTargetVariant::Server
}
fn resource_target(id: impl Into<String>) -> ResourceTarget {
ResourceTarget::Server(id.into())
}
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().servers
@@ -43,10 +38,6 @@ impl super::KomodoResource for Server {
server: Resource<Self::Config, Self::Info>,
) -> Self::ListItem {
let status = server_status_cache().get(&server.id).await;
let terminals_disabled = get_system_info(&server)
.await
.map(|i| i.terminals_disabled)
.unwrap_or(true);
ServerListItem {
name: server.name,
id: server.id,
@@ -62,7 +53,6 @@ impl super::KomodoResource for Server {
send_cpu_alerts: server.config.send_cpu_alerts,
send_mem_alerts: server.config.send_mem_alerts,
send_disk_alerts: server.config.send_disk_alerts,
terminals_disabled,
},
}
}
+1 -5
View File
@@ -1,5 +1,5 @@
use komodo_client::entities::{
MergePartial, Operation, ResourceTarget, ResourceTargetVariant,
MergePartial, Operation, ResourceTargetVariant,
resource::Resource,
server_template::{
PartialServerTemplateConfig, ServerTemplate,
@@ -29,10 +29,6 @@ impl super::KomodoResource for ServerTemplate {
ResourceTargetVariant::ServerTemplate
}
fn resource_target(id: impl Into<String>) -> ResourceTarget {
ResourceTarget::ServerTemplate(id.into())
}
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().server_templates
+2 -6
View File
@@ -3,7 +3,7 @@ use formatting::format_serror;
use komodo_client::{
api::write::RefreshStackCache,
entities::{
Operation, ResourceTarget, ResourceTargetVariant,
Operation, ResourceTargetVariant,
permission::PermissionLevel,
resource::Resource,
server::Server,
@@ -44,10 +44,6 @@ impl super::KomodoResource for Stack {
ResourceTargetVariant::Stack
}
fn resource_target(id: impl Into<String>) -> ResourceTarget {
ResourceTarget::Stack(id.into())
}
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().stacks
@@ -166,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 may be due to a misconfiguration of the Stack").into())
format_serror(&e.error.context("The stack cache has failed to refresh. This is likely due to a misconfiguration of the Stack").into())
);
};
if created.config.server_id.is_empty() {
+1 -6
View File
@@ -3,8 +3,7 @@ use formatting::format_serror;
use komodo_client::{
api::write::RefreshResourceSyncPending,
entities::{
Operation, ResourceTarget, ResourceTargetVariant,
komodo_timestamp,
Operation, ResourceTargetVariant, komodo_timestamp,
resource::Resource,
sync::{
PartialResourceSyncConfig, ResourceSync, ResourceSyncConfig,
@@ -37,10 +36,6 @@ impl super::KomodoResource for ResourceSync {
ResourceTargetVariant::ResourceSync
}
fn resource_target(id: impl Into<String>) -> ResourceTarget {
ResourceTarget::ResourceSync(id.into())
}
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().resource_syncs
-378
View File
@@ -1,378 +0,0 @@
use std::{
collections::HashMap,
sync::{OnceLock, RwLock},
};
use anyhow::{Context, anyhow};
use async_timing_util::Timelength;
use chrono::Local;
use formatting::format_serror;
use komodo_client::{
api::execute::{RunAction, RunProcedure},
entities::{
ResourceTarget, ResourceTargetVariant, ScheduleFormat,
action::Action,
alert::{Alert, AlertData, SeverityLevel},
komodo_timestamp,
procedure::Procedure,
user::{action_user, procedure_user},
},
};
use mungos::find::find_collect;
use resolver_api::Resolve;
use crate::{
alert::send_alerts,
api::execute::{ExecuteArgs, ExecuteRequest},
helpers::update::init_execution_update,
state::db_client,
};
pub fn spawn_schedule_executor() {
// Executor thread
tokio::spawn(async move {
loop {
let current_time = async_timing_util::wait_until_timelength(
Timelength::OneSecond,
0,
)
.await as i64;
let mut lock = schedules().write().unwrap();
let drained = lock.drain().collect::<Vec<_>>();
for (target, next_run) in drained {
match next_run {
Ok(next_run_time) if current_time >= next_run_time => {
tokio::spawn(async move {
match &target {
ResourceTarget::Action(id) => {
let action = match crate::resource::get::<Action>(
id,
)
.await
{
Ok(action) => action,
Err(e) => {
warn!(
"Scheduled action run on {id} failed | failed to get procedure | {e:?}"
);
return;
}
};
let request =
ExecuteRequest::RunAction(RunAction {
action: id.clone(),
});
let update = match init_execution_update(
&request,
action_user(),
)
.await
{
Ok(update) => update,
Err(e) => {
error!(
"Failed to make update for scheduled action run, action {id} is not being run | {e:#}"
);
return;
}
};
let ExecuteRequest::RunAction(request) = request
else {
unreachable!()
};
if let Err(e) = request
.resolve(&ExecuteArgs {
user: action_user().to_owned(),
update,
})
.await
{
warn!(
"Scheduled action run on {id} failed | {e:?}"
);
}
update_schedule(&action);
if action.config.schedule_alert {
let alert = Alert {
id: Default::default(),
target,
ts: komodo_timestamp(),
resolved_ts: Some(komodo_timestamp()),
resolved: true,
level: SeverityLevel::Ok,
data: AlertData::ScheduleRun {
resource_type: ResourceTargetVariant::Action,
id: action.id,
name: action.name,
},
};
send_alerts(&[alert]).await
}
}
ResourceTarget::Procedure(id) => {
let procedure = match crate::resource::get::<
Procedure,
>(id)
.await
{
Ok(procedure) => procedure,
Err(e) => {
warn!(
"Scheduled procedure run on {id} failed | failed to get procedure | {e:?}"
);
return;
}
};
let request =
ExecuteRequest::RunProcedure(RunProcedure {
procedure: id.clone(),
});
let update = match init_execution_update(
&request,
procedure_user(),
)
.await
{
Ok(update) => update,
Err(e) => {
error!(
"Failed to make update for scheduled procedure run, procedure {id} is not being run | {e:#}"
);
return;
}
};
let ExecuteRequest::RunProcedure(request) = request
else {
unreachable!()
};
if let Err(e) = request
.resolve(&ExecuteArgs {
user: procedure_user().to_owned(),
update,
})
.await
{
warn!(
"Scheduled procedure run on {id} failed | {e:?}"
);
}
update_schedule(&procedure);
if procedure.config.schedule_alert {
let alert = Alert {
id: Default::default(),
target,
ts: komodo_timestamp(),
resolved_ts: Some(komodo_timestamp()),
resolved: true,
level: SeverityLevel::Ok,
data: AlertData::ScheduleRun {
resource_type:
ResourceTargetVariant::Procedure,
id: procedure.id,
name: procedure.name,
},
};
send_alerts(&[alert]).await
}
}
_ => unreachable!(),
}
});
}
other => {
lock.insert(target, other);
continue;
}
};
}
}
});
// Updater thread
tokio::spawn(async move {
update_schedules().await;
loop {
async_timing_util::wait_until_timelength(
Timelength::FiveMinutes,
500,
)
.await;
update_schedules().await
}
});
}
type UnixTimestampMs = i64;
type Schedules =
HashMap<ResourceTarget, Result<UnixTimestampMs, String>>;
fn schedules() -> &'static RwLock<Schedules> {
static SCHEDULES: OnceLock<RwLock<Schedules>> = OnceLock::new();
SCHEDULES.get_or_init(Default::default)
}
pub fn get_schedule_item_info(
target: &ResourceTarget,
) -> (Option<i64>, Option<String>) {
match schedules().read().unwrap().get(target) {
Some(Ok(time)) => (Some(*time), None),
Some(Err(e)) => (None, Some(e.clone())),
None => (None, None),
}
}
pub fn cancel_schedule(target: &ResourceTarget) {
schedules().write().unwrap().remove(target);
}
pub async fn update_schedules() {
let (procedures, actions) = tokio::join!(
find_collect(&db_client().procedures, None, None),
find_collect(&db_client().actions, None, None),
);
let procedures = match procedures
.context("failed to get all procedures from db")
{
Ok(procedures) => procedures,
Err(e) => {
error!("failed to get procedures for schedule update | {e:#}");
Vec::new()
}
};
let actions =
match actions.context("failed to get all actions from db") {
Ok(actions) => actions,
Err(e) => {
error!("failed to get actions for schedule update | {e:#}");
Vec::new()
}
};
// clear out any schedules which don't match to existing resources
{
let mut lock = schedules().write().unwrap();
lock.retain(|target, _| match target {
ResourceTarget::Action(id) => {
actions.iter().any(|action| &action.id == id)
}
ResourceTarget::Procedure(id) => {
procedures.iter().any(|procedure| &procedure.id == id)
}
_ => unreachable!(),
});
}
for procedure in procedures {
update_schedule(&procedure);
}
for action in actions {
update_schedule(&action);
}
}
/// Re/spawns the schedule for the given procedure
pub fn update_schedule(schedule: impl HasSchedule) {
// Cancel any existing schedule for the procedure
cancel_schedule(&schedule.target());
if !schedule.enabled() || schedule.schedule().is_empty() {
return;
}
schedules().write().unwrap().insert(
schedule.target(),
find_next_occurrence(schedule)
.map_err(|e| format_serror(&e.into())),
);
}
/// Finds the next run occurence in UTC ms.
fn find_next_occurrence(
schedule: impl HasSchedule,
) -> anyhow::Result<i64> {
let cron = match schedule.format() {
ScheduleFormat::Cron => croner::Cron::new(schedule.schedule())
.with_seconds_required()
.with_dom_and_dow()
.parse()
.context("Failed to parse schedule CRON")?,
ScheduleFormat::English => {
let cron =
english_to_cron::str_cron_syntax(schedule.schedule())
.map_err(|e| {
anyhow!("Failed to parse english to cron | {e:?}")
})?
.split(' ')
// croner does not accept year
.take(6)
.collect::<Vec<_>>()
.join(" ");
croner::Cron::new(&cron)
.with_seconds_required()
.with_dom_and_dow()
.parse()
.with_context(|| {
format!("Failed to parse schedule CRON: {cron}")
})?
}
};
let next = if schedule.timezone().is_empty() {
let tz_time = chrono::Local::now().with_timezone(&Local);
cron
.find_next_occurrence(&tz_time, false)
.context("Failed to find next run time")?
.timestamp_millis()
} else {
let tz: chrono_tz::Tz = schedule
.timezone()
.parse()
.context("Failed to parse schedule timezone")?;
let tz_time = chrono::Local::now().with_timezone(&tz);
cron
.find_next_occurrence(&tz_time, false)
.context("Failed to find next run time")?
.timestamp_millis()
};
Ok(next)
}
pub trait HasSchedule {
fn target(&self) -> ResourceTarget;
fn enabled(&self) -> bool;
fn format(&self) -> ScheduleFormat;
fn schedule(&self) -> &str;
fn timezone(&self) -> &str;
}
impl HasSchedule for &Procedure {
fn target(&self) -> ResourceTarget {
ResourceTarget::Procedure(self.id.clone())
}
fn enabled(&self) -> bool {
self.config.schedule_enabled
}
fn format(&self) -> ScheduleFormat {
self.config.schedule_format
}
fn schedule(&self) -> &str {
&self.config.schedule
}
fn timezone(&self) -> &str {
&self.config.schedule_timezone
}
}
impl HasSchedule for &Action {
fn target(&self) -> ResourceTarget {
ResourceTarget::Action(self.id.clone())
}
fn enabled(&self) -> bool {
self.config.schedule_enabled
}
fn format(&self) -> ScheduleFormat {
self.config.schedule_format
}
fn schedule(&self) -> &str {
&self.config.schedule
}
fn timezone(&self) -> &str {
&self.config.schedule_timezone
}
}
+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))
}
+12 -9
View File
@@ -8,15 +8,18 @@ pub fn extract_services_from_stack(
stack: &Stack,
) -> Vec<StackServiceNames> {
if let Some(mut services) = stack.info.deployed_services.clone() {
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();
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();
}
}
services
} else {
+1 -1
View File
@@ -424,7 +424,7 @@ fn build_cache_for_deployment<'a>(
let deployed_version = status
.container
.as_ref()
.and_then(|c| c.image.as_ref()?.split(':').next_back())
.and_then(|c| c.image.as_ref()?.split(':').last())
.unwrap_or("0.0.0");
match build_version_cache.get(build_id) {
Some(version) if deployed_version != version => {
+3 -1
View File
@@ -2,7 +2,7 @@ use std::{collections::HashMap, str::FromStr};
use anyhow::anyhow;
use komodo_client::entities::{
ResourceTargetVariant,
ResourceTarget, ResourceTargetVariant,
action::Action,
alerter::Alerter,
build::Build,
@@ -55,6 +55,8 @@ pub struct ToUpdateItem<T: Default> {
}
pub trait ResourceSyncTrait: ToToml + Sized {
fn resource_target(id: String) -> ResourceTarget;
/// To exclude resource syncs with "file_contents" (they aren't compatible)
fn include_resource(
name: &String,
+45 -1
View File
@@ -4,7 +4,7 @@ use formatting::{Color, bold, colored, muted};
use komodo_client::{
api::execute::Execution,
entities::{
ResourceTargetVariant,
ResourceTarget, ResourceTargetVariant,
action::Action,
alerter::Alerter,
build::Build,
@@ -40,6 +40,10 @@ use super::{
};
impl ResourceSyncTrait for Server {
fn resource_target(id: String) -> ResourceTarget {
ResourceTarget::Server(id)
}
fn get_diff(
original: Self::Config,
update: Self::PartialConfig,
@@ -52,6 +56,10 @@ impl ResourceSyncTrait for Server {
impl ExecuteResourceSync for Server {}
impl ResourceSyncTrait for Deployment {
fn resource_target(id: String) -> ResourceTarget {
ResourceTarget::Deployment(id)
}
fn get_diff(
mut original: Self::Config,
update: Self::PartialConfig,
@@ -85,6 +93,10 @@ impl ResourceSyncTrait for Deployment {
impl ExecuteResourceSync for Deployment {}
impl ResourceSyncTrait for Stack {
fn resource_target(id: String) -> ResourceTarget {
ResourceTarget::Stack(id)
}
fn get_diff(
mut original: Self::Config,
update: Self::PartialConfig,
@@ -104,6 +116,10 @@ impl ResourceSyncTrait for Stack {
impl ExecuteResourceSync for Stack {}
impl ResourceSyncTrait for Build {
fn resource_target(id: String) -> ResourceTarget {
ResourceTarget::Build(id)
}
fn get_diff(
mut original: Self::Config,
update: Self::PartialConfig,
@@ -133,6 +149,10 @@ impl ResourceSyncTrait for Build {
impl ExecuteResourceSync for Build {}
impl ResourceSyncTrait for Repo {
fn resource_target(id: String) -> ResourceTarget {
ResourceTarget::Repo(id)
}
fn get_diff(
mut original: Self::Config,
update: Self::PartialConfig,
@@ -159,6 +179,10 @@ impl ResourceSyncTrait for Repo {
impl ExecuteResourceSync for Repo {}
impl ResourceSyncTrait for Alerter {
fn resource_target(id: String) -> ResourceTarget {
ResourceTarget::Alerter(id)
}
fn get_diff(
original: Self::Config,
update: Self::PartialConfig,
@@ -171,6 +195,10 @@ impl ResourceSyncTrait for Alerter {
impl ExecuteResourceSync for Alerter {}
impl ResourceSyncTrait for Builder {
fn resource_target(id: String) -> ResourceTarget {
ResourceTarget::Builder(id)
}
fn get_diff(
mut original: Self::Config,
update: Self::PartialConfig,
@@ -192,6 +220,10 @@ impl ResourceSyncTrait for Builder {
impl ExecuteResourceSync for Builder {}
impl ResourceSyncTrait for ServerTemplate {
fn resource_target(id: String) -> ResourceTarget {
ResourceTarget::ServerTemplate(id)
}
fn get_diff(
original: Self::Config,
update: Self::PartialConfig,
@@ -204,6 +236,10 @@ impl ResourceSyncTrait for ServerTemplate {
impl ExecuteResourceSync for ServerTemplate {}
impl ResourceSyncTrait for Action {
fn resource_target(id: String) -> ResourceTarget {
ResourceTarget::Action(id)
}
fn get_diff(
original: Self::Config,
update: Self::PartialConfig,
@@ -216,6 +252,10 @@ impl ResourceSyncTrait for Action {
impl ExecuteResourceSync for Action {}
impl ResourceSyncTrait for ResourceSync {
fn resource_target(id: String) -> ResourceTarget {
ResourceTarget::ResourceSync(id)
}
fn include_resource(
name: &String,
config: &Self::Config,
@@ -301,6 +341,10 @@ impl ResourceSyncTrait for ResourceSync {
impl ExecuteResourceSync for ResourceSync {}
impl ResourceSyncTrait for Procedure {
fn resource_target(id: String) -> ResourceTarget {
ResourceTarget::Procedure(id)
}
fn get_diff(
mut original: Self::Config,
update: Self::PartialConfig,
+209
View File
@@ -0,0 +1,209 @@
use anyhow::anyhow;
use axum::{
Router,
extract::{
WebSocketUpgrade,
ws::{Message, WebSocket},
},
response::IntoResponse,
routing::get,
};
use futures::{SinkExt, StreamExt};
use komodo_client::{
entities::{
ResourceTarget, permission::PermissionLevel, user::User,
},
ws::WsLoginMessage,
};
use serde_json::json;
use serror::serialize_error;
use tokio::select;
use tokio_util::sync::CancellationToken;
use crate::{
auth::{auth_api_key_check_enabled, auth_jwt_check_enabled},
helpers::{
channel::update_channel,
query::{get_user, get_user_permission_on_target},
},
};
pub fn router() -> Router {
Router::new().route("/update", get(ws_handler))
}
#[instrument(level = "debug")]
async fn ws_handler(ws: WebSocketUpgrade) -> impl IntoResponse {
// get a reveiver for internal update messages.
let mut receiver = update_channel().receiver.resubscribe();
// handle http -> ws updgrade
ws.on_upgrade(|socket| async move {
let Some((socket, user)) = ws_login(socket).await else {
return
};
let (mut ws_sender, mut ws_reciever) = socket.split();
let cancel = CancellationToken::new();
let cancel_clone = cancel.clone();
tokio::spawn(async move {
loop {
// poll for updates off the receiver / await cancel.
let update = select! {
_ = cancel_clone.cancelled() => break,
update = receiver.recv() => {update.expect("failed to recv update msg")}
};
// before sending every update, verify user is still valid.
// kill the connection is user if found to be invalid.
let user = check_user_valid(&user.id).await;
let user = match user {
Err(e) => {
let _ = ws_sender
.send(Message::text(json!({ "type": "INVALID_USER", "msg": serialize_error(&e) }).to_string()))
.await;
let _ = ws_sender.close().await;
return;
},
Ok(user) => user,
};
// Only send if user has permission on the target resource.
if user_can_see_update(&user, &update.target).await.is_ok() {
let _ = ws_sender
.send(Message::text(serde_json::to_string(&update).unwrap()))
.await;
}
}
});
// Handle messages from the client.
// After login, only handles close message.
while let Some(msg) = ws_reciever.next().await {
match msg {
Ok(msg) => {
if let Message::Close(_) = msg {
cancel.cancel();
return;
}
}
Err(_) => {
cancel.cancel();
return;
}
}
}
})
}
#[instrument(level = "debug")]
async fn ws_login(
mut socket: WebSocket,
) -> Option<(WebSocket, User)> {
let login_msg = match socket.recv().await {
Some(Ok(Message::Text(login_msg))) => {
LoginMessage::Ok(login_msg.to_string())
}
Some(Ok(msg)) => {
LoginMessage::Err(format!("invalid login message: {msg:?}"))
}
Some(Err(e)) => {
LoginMessage::Err(format!("failed to get login message: {e:?}"))
}
None => {
LoginMessage::Err("failed to get login message".to_string())
}
};
let login_msg = match login_msg {
LoginMessage::Ok(login_msg) => login_msg,
LoginMessage::Err(msg) => {
let _ = socket.send(Message::text(msg)).await;
let _ = socket.close().await;
return None;
}
};
match WsLoginMessage::from_json_str(&login_msg) {
// Login using a jwt
Ok(WsLoginMessage::Jwt { jwt }) => {
match auth_jwt_check_enabled(&jwt).await {
Ok(user) => {
let _ = socket.send(Message::text("LOGGED_IN")).await;
Some((socket, user))
}
Err(e) => {
let _ = socket
.send(Message::text(format!(
"failed to authenticate user using jwt | {e:#}"
)))
.await;
let _ = socket.close().await;
None
}
}
}
// login using api keys
Ok(WsLoginMessage::ApiKeys { key, secret }) => {
match auth_api_key_check_enabled(&key, &secret).await {
Ok(user) => {
let _ = socket.send(Message::text("LOGGED_IN")).await;
Some((socket, user))
}
Err(e) => {
let _ = socket
.send(Message::text(format!(
"failed to authenticate user using api keys | {e:#}"
)))
.await;
let _ = socket.close().await;
None
}
}
}
Err(e) => {
let _ = socket
.send(Message::text(format!(
"failed to parse login message: {e:#}"
)))
.await;
let _ = socket.close().await;
None
}
}
}
enum LoginMessage {
/// The text message
Ok(String),
/// The err message
Err(String),
}
#[instrument(level = "debug")]
async fn check_user_valid(user_id: &str) -> anyhow::Result<User> {
let user = get_user(user_id).await?;
if !user.enabled {
return Err(anyhow!("user not enabled"));
}
Ok(user)
}
#[instrument(level = "debug")]
async fn user_can_see_update(
user: &User,
update_target: &ResourceTarget,
) -> anyhow::Result<()> {
if user.admin {
return Ok(());
}
let permissions =
get_user_permission_on_target(user, update_target).await?;
if permissions > PermissionLevel::None {
Ok(())
} else {
Err(anyhow!(
"user does not have permissions on {update_target:?}"
))
}
}
-112
View File
@@ -1,112 +0,0 @@
use crate::{
auth::{auth_api_key_check_enabled, auth_jwt_check_enabled},
helpers::query::get_user,
};
use anyhow::anyhow;
use axum::{
Router,
extract::ws::{Message, WebSocket},
routing::get,
};
use futures::SinkExt;
use komodo_client::{entities::user::User, ws::WsLoginMessage};
mod terminal;
mod update;
pub fn router() -> Router {
Router::new()
.route("/update", get(update::handler))
.route("/terminal", get(terminal::handler))
}
#[instrument(level = "debug")]
async fn ws_login(
mut socket: WebSocket,
) -> Option<(WebSocket, User)> {
let login_msg = match socket.recv().await {
Some(Ok(Message::Text(login_msg))) => {
LoginMessage::Ok(login_msg.to_string())
}
Some(Ok(msg)) => {
LoginMessage::Err(format!("invalid login message: {msg:?}"))
}
Some(Err(e)) => {
LoginMessage::Err(format!("failed to get login message: {e:?}"))
}
None => {
LoginMessage::Err("failed to get login message".to_string())
}
};
let login_msg = match login_msg {
LoginMessage::Ok(login_msg) => login_msg,
LoginMessage::Err(msg) => {
let _ = socket.send(Message::text(msg)).await;
let _ = socket.close().await;
return None;
}
};
match WsLoginMessage::from_json_str(&login_msg) {
// Login using a jwt
Ok(WsLoginMessage::Jwt { jwt }) => {
match auth_jwt_check_enabled(&jwt).await {
Ok(user) => {
let _ = socket.send(Message::text("LOGGED_IN")).await;
Some((socket, user))
}
Err(e) => {
let _ = socket
.send(Message::text(format!(
"failed to authenticate user using jwt | {e:#}"
)))
.await;
let _ = socket.close().await;
None
}
}
}
// login using api keys
Ok(WsLoginMessage::ApiKeys { key, secret }) => {
match auth_api_key_check_enabled(&key, &secret).await {
Ok(user) => {
let _ = socket.send(Message::text("LOGGED_IN")).await;
Some((socket, user))
}
Err(e) => {
let _ = socket
.send(Message::text(format!(
"failed to authenticate user using api keys | {e:#}"
)))
.await;
let _ = socket.close().await;
None
}
}
}
Err(e) => {
let _ = socket
.send(Message::text(format!(
"failed to parse login message: {e:#}"
)))
.await;
let _ = socket.close().await;
None
}
}
}
enum LoginMessage {
/// The text message
Ok(String),
/// The err message
Err(String),
}
#[instrument(level = "debug")]
async fn check_user_valid(user_id: &str) -> anyhow::Result<User> {
let user = get_user(user_id).await?;
if !user.enabled {
return Err(anyhow!("user not enabled"));
}
Ok(user)
}
-200
View File
@@ -1,200 +0,0 @@
use axum::{
extract::{
Query, WebSocketUpgrade,
ws::{CloseFrame, Message, Utf8Bytes},
},
response::IntoResponse,
};
use futures::{SinkExt, StreamExt};
use komodo_client::{
api::terminal::ConnectTerminalQuery,
entities::{permission::PermissionLevel, server::Server},
};
use tokio_tungstenite::tungstenite;
use tokio_util::sync::CancellationToken;
use crate::{helpers::periphery_client, resource};
#[instrument(name = "ConnectTerminal", skip(ws))]
pub async fn handler(
Query(ConnectTerminalQuery {
server,
terminal,
init,
}): Query<ConnectTerminalQuery>,
ws: WebSocketUpgrade,
) -> impl IntoResponse {
ws.on_upgrade(|socket| async move {
let Some((mut socket, user)) = super::ws_login(socket).await
else {
return;
};
let server = match resource::get_check_permissions::<Server>(
&server,
&user,
PermissionLevel::Write,
)
.await
{
Ok(server) => server,
Err(e) => {
debug!("could not get server | {e:#}");
let _ =
socket.send(Message::text(format!("ERROR: {e:#}"))).await;
let _ = socket.close().await;
return;
}
};
let periphery = match periphery_client(&server) {
Ok(periphery) => periphery,
Err(e) => {
debug!("couldn't get periphery | {e:#}");
let _ =
socket.send(Message::text(format!("ERROR: {e:#}"))).await;
let _ = socket.close().await;
return;
}
};
trace!("connecting to periphery terminal");
let periphery_socket = match periphery
.connect_terminal(
terminal,
init,
)
.await
{
Ok(ws) => ws,
Err(e) => {
debug!("Failed connect to periphery terminal | {e:#}");
let _ =
socket.send(Message::text(format!("ERROR: {e:#}"))).await;
let _ = socket.close().await;
return;
}
};
trace!("connected to periphery terminal socket");
let (mut periphery_send, mut periphery_receive) =
periphery_socket.split();
let (mut core_send, mut core_receive) = socket.split();
let cancel = CancellationToken::new();
trace!("starting ws exchange");
let core_to_periphery = async {
loop {
let res = tokio::select! {
res = core_receive.next() => res,
_ = cancel.cancelled() => {
trace!("core to periphery read: cancelled from inside");
break;
}
};
match res {
Some(Ok(msg)) => {
if let Err(e) =
periphery_send.send(axum_to_tungstenite(msg)).await
{
debug!("Failed to send terminal message to {} | {e:?}", server.name);
cancel.cancel();
break;
};
}
Some(Err(_e)) => {
cancel.cancel();
break;
}
None => {
cancel.cancel();
break;
}
}
}
};
let periphery_to_core = async {
loop {
let res = tokio::select! {
res = periphery_receive.next() => res,
_ = cancel.cancelled() => {
trace!("periphery to core read: cancelled from inside");
break;
}
};
match res {
Some(Ok(msg)) => {
if let Err(e) =
core_send.send(tungstenite_to_axum(msg)).await
{
debug!("{e:?}");
cancel.cancel();
break;
};
}
Some(Err(e)) => {
let _ = core_send
.send(Message::text(format!(
"ERROR: Failed to receive message from periphery | {e:?}"
)))
.await;
cancel.cancel();
break;
}
None => {
let _ = core_send
.send(Message::text("STREAM EOF"))
.await;
cancel.cancel();
break;
}
}
}
};
tokio::join!(core_to_periphery, periphery_to_core);
})
}
fn axum_to_tungstenite(msg: Message) -> tungstenite::Message {
match msg {
Message::Text(text) => tungstenite::Message::Text(
tungstenite::Utf8Bytes::from(text.to_string()),
),
Message::Binary(bytes) => tungstenite::Message::Binary(bytes),
Message::Ping(bytes) => tungstenite::Message::Ping(bytes),
Message::Pong(bytes) => tungstenite::Message::Pong(bytes),
Message::Close(close_frame) => {
tungstenite::Message::Close(close_frame.map(|cf| {
tungstenite::protocol::CloseFrame {
code: cf.code.into(),
reason: tungstenite::Utf8Bytes::from(cf.reason.to_string()),
}
}))
}
}
}
fn tungstenite_to_axum(msg: tungstenite::Message) -> Message {
match msg {
tungstenite::Message::Text(text) => {
Message::Text(Utf8Bytes::from(text.to_string()))
}
tungstenite::Message::Binary(bytes) => Message::Binary(bytes),
tungstenite::Message::Ping(bytes) => Message::Ping(bytes),
tungstenite::Message::Pong(bytes) => Message::Pong(bytes),
tungstenite::Message::Close(close_frame) => {
Message::Close(close_frame.map(|cf| CloseFrame {
code: cf.code.into(),
reason: Utf8Bytes::from(cf.reason.to_string()),
}))
}
tungstenite::Message::Frame(_) => {
unreachable!()
}
}
}
-102
View File
@@ -1,102 +0,0 @@
use anyhow::anyhow;
use axum::{
extract::{WebSocketUpgrade, ws::Message},
response::IntoResponse,
};
use futures::{SinkExt, StreamExt};
use komodo_client::entities::{
ResourceTarget, permission::PermissionLevel, user::User,
};
use serde_json::json;
use serror::serialize_error;
use tokio::select;
use tokio_util::sync::CancellationToken;
use crate::helpers::{
channel::update_channel, query::get_user_permission_on_target,
};
#[instrument(level = "debug")]
pub async fn handler(ws: WebSocketUpgrade) -> impl IntoResponse {
// get a reveiver for internal update messages.
let mut receiver = update_channel().receiver.resubscribe();
// handle http -> ws updgrade
ws.on_upgrade(|socket| async move {
let Some((socket, user)) = super::ws_login(socket).await else {
return
};
let (mut ws_sender, mut ws_reciever) = socket.split();
let cancel = CancellationToken::new();
let cancel_clone = cancel.clone();
tokio::spawn(async move {
loop {
// poll for updates off the receiver / await cancel.
let update = select! {
_ = cancel_clone.cancelled() => break,
update = receiver.recv() => {update.expect("failed to recv update msg")}
};
// before sending every update, verify user is still valid.
// kill the connection is user if found to be invalid.
let user = super::check_user_valid(&user.id).await;
let user = match user {
Err(e) => {
let _ = ws_sender
.send(Message::text(json!({ "type": "INVALID_USER", "msg": serialize_error(&e) }).to_string()))
.await;
let _ = ws_sender.close().await;
return;
},
Ok(user) => user,
};
// Only send if user has permission on the target resource.
if user_can_see_update(&user, &update.target).await.is_ok() {
let _ = ws_sender
.send(Message::text(serde_json::to_string(&update).unwrap()))
.await;
}
}
});
// Handle messages from the client.
// After login, only handles close message.
while let Some(msg) = ws_reciever.next().await {
match msg {
Ok(msg) => {
if let Message::Close(_) = msg {
cancel.cancel();
return;
}
}
Err(_) => {
cancel.cancel();
return;
}
}
}
})
}
#[instrument(level = "debug")]
async fn user_can_see_update(
user: &User,
update_target: &ResourceTarget,
) -> anyhow::Result<()> {
if user.admin {
return Ok(());
}
let permissions =
get_user_permission_on_target(user, update_target).await?;
if permissions > PermissionLevel::None {
Ok(())
} else {
Err(anyhow!(
"user does not have permissions on {update_target:?}"
))
}
}
-67
View File
@@ -1,67 +0,0 @@
## This is used to customize the shell prompt in Periphery container for Terminals
"$schema" = 'https://starship.rs/config-schema.json'
add_newline = true
format = "$time$hostname$container$memory_usage$all"
[character]
success_symbol = "[❯](bright-blue bold)"
error_symbol = "[❯](bright-red bold)"
[package]
disabled = true
[time]
format = "[❯$time](white dimmed) "
time_format = "%l:%M %p"
utc_time_offset = '-5'
disabled = true
[username]
format = "[❯ $user]($style) "
style_user = "bright-green"
show_always = true
[hostname]
format = "[❯ $hostname]($style) "
style = "bright-blue"
ssh_only = false
[directory]
format = "[❯ $path]($style)[$read_only]($read_only_style) "
style = "bright-cyan"
[git_branch]
format = "[❯ $symbol$branch(:$remote_branch)]($style) "
style = "bright-purple"
[git_status]
style = "bright-purple"
[rust]
format = "[❯ $symbol($version )]($style)"
symbol = "rustc "
style = "bright-red"
[nodejs]
format = "[❯ $symbol($version )]($style)"
symbol = "nodejs "
style = "bright-red"
[memory_usage]
format = "[❯ mem ${ram} ${ram_pct}]($style) "
threshold = -1
style = "white"
[cmd_duration]
format = "[❯ $duration]($style)"
style = "bright-yellow"
[container]
format = "[❯ 🦎 core container ]($style)"
style = "bright-green"
[aws]
disabled = true
-4
View File
@@ -33,11 +33,9 @@ resolver_api.workspace = true
run_command.workspace = true
svi.workspace = true
# external
portable-pty.workspace = true
axum-server.workspace = true
serde_json.workspace = true
serde_yaml.workspace = true
tokio-util.workspace = true
futures.workspace = true
tracing.workspace = true
bollard.workspace = true
@@ -47,9 +45,7 @@ anyhow.workspace = true
rustls.workspace = true
tokio.workspace = true
serde.workspace = true
bytes.workspace = true
axum.workspace = true
clap.workspace = true
envy.workspace = true
uuid.workspace = true
rand.workspace = true
+1 -2
View File
@@ -1,6 +1,6 @@
## All in one, multi stage compile + runtime Docker build for your architecture.
FROM rust:1.86.0-bullseye AS builder
FROM rust:1.85.0-bullseye AS builder
WORKDIR /builder
COPY Cargo.toml Cargo.lock ./
@@ -15,7 +15,6 @@ RUN cargo build -p komodo_periphery --release
# Final Image
FROM debian:bullseye-slim
COPY ./bin/periphery/starship.toml /config/starship.toml
COPY ./bin/periphery/debian-deps.sh .
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
+1 -11
View File
@@ -1,10 +1,6 @@
#!/bin/bash
## Periphery deps installer
apt-get update
apt-get install -y git curl wget ca-certificates
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
@@ -19,10 +15,4 @@ apt-get update
# apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
apt-get install -y docker-ce-cli docker-buildx-plugin docker-compose-plugin
rm -rf /var/lib/apt/lists/*
# Starship prompt
curl -sS https://starship.rs/install.sh | sh -s -- --yes --bin-dir /usr/local/bin
echo 'export STARSHIP_CONFIG=/config/starship.toml' >> /root/.bashrc
echo 'eval "$(starship init bash)"' >> /root/.bashrc
rm -rf /var/lib/apt/lists/*
-1
View File
@@ -12,7 +12,6 @@ FROM ${AARCH64_BINARIES} AS aarch64
FROM debian:bullseye-slim
COPY ./bin/periphery/starship.toml /config/starship.toml
COPY ./bin/periphery/debian-deps.sh .
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
+1 -1
View File
@@ -8,10 +8,10 @@ FROM ${BINARIES_IMAGE} AS binaries
FROM debian:bullseye-slim
COPY ./bin/periphery/starship.toml /config/starship.toml
COPY ./bin/periphery/debian-deps.sh .
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
WORKDIR /app
COPY --from=binaries /periphery /usr/local/bin/periphery
EXPOSE 8120
+14 -178
View File
@@ -1,12 +1,8 @@
use std::{
fmt::Write,
path::{Path, PathBuf},
};
use std::{fmt::Write, path::Path};
use anyhow::{Context, anyhow};
use command::{
run_komodo_command, run_komodo_command_multiline,
run_komodo_command_with_interpolation,
run_komodo_command, run_komodo_command_with_interpolation,
};
use formatting::format_serror;
use komodo_client::{
@@ -20,12 +16,9 @@ use komodo_client::{
parsers::QUOTE_PATTERN,
};
use periphery_client::api::build::{
self, GetDockerfileContentsOnHost,
GetDockerfileContentsOnHostResponse, PruneBuilders, PruneBuildx,
WriteDockerfileContentsToHost,
self, PruneBuilders, PruneBuildx,
};
use resolver_api::Resolve;
use tokio::fs;
use crate::{
config::periphery_config,
@@ -33,88 +26,6 @@ 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(
@@ -125,7 +36,7 @@ impl Resolve<super::Args> for build::Build {
build,
registry_token,
additional_tags,
replacers: mut core_replacers,
replacers: core_replacers,
} = self;
let Build {
name,
@@ -142,19 +53,11 @@ 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
@@ -179,81 +82,14 @@ impl Resolve<super::Args> for build::Build {
let name = to_komodo_name(name);
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:?}"),
));
// 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(),
};
// 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")?;
@@ -273,7 +109,7 @@ impl Resolve<super::Args> for build::Build {
.context("Invalid secret_args")?;
let command_secret_args = parse_secret_args(
&secret_args,
&build_path,
&build_dir,
*skip_secret_interp,
)
.await?;
@@ -295,14 +131,14 @@ impl Resolve<super::Args> for build::Build {
if *skip_secret_interp {
let build_log = run_komodo_command(
"Docker Build",
build_path.as_ref(),
build_dir.as_ref(),
command,
)
.await;
logs.push(build_log);
} else if let Some(log) = run_komodo_command_with_interpolation(
"Docker Build",
build_path.as_ref(),
build_dir.as_ref(),
command,
false,
&periphery_config().secrets,
@@ -356,7 +192,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,
+3 -3
View File
@@ -138,7 +138,7 @@ impl Resolve<super::Args> for GetComposeContentsOnHost {
file_paths,
} = self;
let root =
periphery_config().stack_dir().join(to_komodo_name(&name));
periphery_config().stack_dir.join(to_komodo_name(&name));
let run_directory =
root.join(&run_directory).components().collect::<PathBuf>();
@@ -196,7 +196,7 @@ impl Resolve<super::Args> for WriteComposeContentsToHost {
contents,
} = self;
let file_path = periphery_config()
.stack_dir()
.stack_dir
.join(to_komodo_name(&name))
.join(&run_directory)
.join(file_path)
@@ -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 compose file parent directory {parent:?}"))?;
.with_context(|| format!("Failed to initialize environment file parent directory {parent:?}"))?;
}
fs::write(&file_path, contents).await.with_context(|| {
format!(
+17 -41
View File
@@ -1,5 +1,4 @@
use anyhow::{Context, anyhow};
use formatting::format_serror;
use git::GitRes;
use komodo_client::entities::{CloneArgs, LatestCommit, update::Log};
use periphery_client::api::git::{
@@ -13,14 +12,14 @@ use tokio::fs;
use crate::config::periphery_config;
impl Resolve<super::Args> for GetLatestCommit {
#[instrument(name = "GetLatestCommit", level = "debug")]
#[instrument(name = "CloneRepo", level = "debug")]
async fn resolve(
self,
_: &super::Args,
) -> serror::Result<LatestCommit> {
let repo_path = match self.path {
Some(p) => PathBuf::from(p),
None => periphery_config().repo_dir().join(self.name),
None => periphery_config().repo_dir.join(self.name),
};
if !repo_path.is_dir() {
return Err(
@@ -69,14 +68,9 @@ 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,
&parent_dir,
&periphery_config().repo_dir,
token,
&environment,
&env_file_path,
@@ -139,14 +133,9 @@ 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,
&parent_dir,
&periphery_config().repo_dir,
token,
&environment,
&env_file_path,
@@ -209,14 +198,9 @@ 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,
&parent_dir,
&periphery_config().repo_dir,
token,
&environment,
&env_file_path,
@@ -252,10 +236,11 @@ impl Resolve<super::Args> for RenameRepo {
curr_name,
new_name,
} = self;
let repo_dir = periphery_config().repo_dir();
let renamed =
fs::rename(repo_dir.join(&curr_name), repo_dir.join(&new_name))
.await;
let renamed = fs::rename(
periphery_config().repo_dir.join(&curr_name),
periphery_config().repo_dir.join(&new_name),
)
.await;
let msg = match renamed {
Ok(_) => String::from("Renamed Repo directory on Server"),
Err(_) => format!("No Repo cloned at {curr_name} to rename"),
@@ -269,25 +254,16 @@ 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, is_build } = self;
let DeleteRepo { name } = 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 root = if is_build {
periphery_config().build_dir()
} else {
periphery_config().repo_dir()
};
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())),
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"),
};
Ok(log)
Ok(Log::simple("Delete Repo on Host", msg))
}
}
+5 -15
View File
@@ -8,8 +8,11 @@ use komodo_client::entities::{
update::Log,
};
use periphery_client::api::{
build::*, compose::*, container::*, git::*, image::*, network::*,
stats::*, terminal::*, volume::*, *,
GetDockerLists, GetDockerListsResponse, GetHealth,
GetHealthResponse, GetVersion, GetVersionResponse,
ListDockerRegistries, ListGitProviders, ListSecrets, PruneSystem,
RunCommand, build::*, compose::*, container::*, git::*, image::*,
network::*, stats::*, volume::*,
};
use resolver_api::Resolve;
use response::Response;
@@ -24,13 +27,9 @@ mod deploy;
mod git;
mod image;
mod network;
mod router;
mod stats;
mod terminal;
mod volume;
pub use router::router;
pub struct Args;
#[derive(
@@ -67,8 +66,6 @@ pub enum PeripheryRequest {
DeleteRepo(DeleteRepo),
// Build
GetDockerfileContentsOnHost(GetDockerfileContentsOnHost),
WriteDockerfileContentsToHost(WriteDockerfileContentsToHost),
Build(Build),
PruneBuilders(PruneBuilders),
PruneBuildx(PruneBuildx),
@@ -137,13 +134,6 @@ pub enum PeripheryRequest {
// All in one (Write)
PruneSystem(PruneSystem),
// Terminal
ListTerminals(ListTerminals),
CreateTerminal(CreateTerminal),
DeleteTerminal(DeleteTerminal),
DeleteAllTerminals(DeleteAllTerminals),
CreateTerminalAuthToken(CreateTerminalAuthToken),
}
//
-333
View File
@@ -1,333 +0,0 @@
use std::{collections::HashMap, sync::OnceLock};
use anyhow::{Context, anyhow};
use axum::{
extract::{
Query, WebSocketUpgrade,
ws::{Message, Utf8Bytes},
},
http::StatusCode,
response::Response,
};
use bytes::Bytes;
use futures::{SinkExt, StreamExt};
use komodo_client::entities::{
NoData, komodo_timestamp, server::TerminalInfo,
};
use periphery_client::api::terminal::{
ConnectTerminalQuery, CreateTerminal, CreateTerminalAuthToken,
CreateTerminalAuthTokenResponse, DeleteAllTerminals,
DeleteTerminal, ListTerminals,
};
use rand::Rng;
use resolver_api::Resolve;
use serror::AddStatusCodeError;
use tokio_util::sync::CancellationToken;
use crate::{
config::periphery_config,
terminal::{
ResizeDimensions, StdinMsg, clean_up_terminals, create_terminal,
delete_all_terminals, delete_terminal, get_terminal,
list_terminals,
},
};
impl Resolve<super::Args> for ListTerminals {
#[instrument(name = "ListTerminals", level = "debug")]
async fn resolve(
self,
_: &super::Args,
) -> serror::Result<Vec<TerminalInfo>> {
if periphery_config().disable_terminals {
return Err(
anyhow!("Terminals are disabled in the periphery config")
.status_code(StatusCode::FORBIDDEN),
);
}
clean_up_terminals().await;
Ok(list_terminals().await)
}
}
impl Resolve<super::Args> for CreateTerminal {
#[instrument(name = "CreateTerminal", level = "debug")]
async fn resolve(self, _: &super::Args) -> serror::Result<NoData> {
if periphery_config().disable_terminals {
return Err(
anyhow!("Terminals are disabled in the periphery config")
.status_code(StatusCode::FORBIDDEN),
);
}
create_terminal(self.name, self.command, self.recreate)
.await
.map(|_| NoData {})
.map_err(Into::into)
}
}
impl Resolve<super::Args> for DeleteTerminal {
#[instrument(name = "DeleteTerminal", level = "debug")]
async fn resolve(self, _: &super::Args) -> serror::Result<NoData> {
if periphery_config().disable_terminals {
return Err(
anyhow!("Terminals are disabled in the periphery config")
.status_code(StatusCode::FORBIDDEN),
);
}
delete_terminal(&self.terminal).await;
Ok(NoData {})
}
}
impl Resolve<super::Args> for DeleteAllTerminals {
#[instrument(name = "DeleteAllTerminals", level = "debug")]
async fn resolve(self, _: &super::Args) -> serror::Result<NoData> {
if periphery_config().disable_terminals {
return Err(
anyhow!("Terminals are disabled in the periphery config")
.status_code(StatusCode::FORBIDDEN),
);
}
delete_all_terminals().await;
Ok(NoData {})
}
}
impl Resolve<super::Args> for CreateTerminalAuthToken {
#[instrument(name = "CreateTerminalAuthToken", level = "debug")]
async fn resolve(
self,
_: &super::Args,
) -> serror::Result<CreateTerminalAuthTokenResponse> {
if periphery_config().disable_terminals {
return Err(
anyhow!("Terminals are disabled in the periphery config")
.status_code(StatusCode::FORBIDDEN),
);
}
Ok(CreateTerminalAuthTokenResponse {
token: auth_tokens().create_auth_token(),
})
}
}
/// Tokens valid for 3 seconds
const TOKEN_VALID_FOR_MS: i64 = 3_000;
fn auth_tokens() -> &'static AuthTokens {
static AUTH_TOKENS: OnceLock<AuthTokens> = OnceLock::new();
AUTH_TOKENS.get_or_init(Default::default)
}
#[derive(Default)]
struct AuthTokens {
map: std::sync::Mutex<HashMap<String, i64>>,
}
impl AuthTokens {
pub fn create_auth_token(&self) -> String {
let token: String = rand::rng()
.sample_iter(&rand::distr::Alphanumeric)
.take(30)
.map(char::from)
.collect();
self
.map
.lock()
.unwrap()
.insert(token.clone(), komodo_timestamp() + TOKEN_VALID_FOR_MS);
token
}
pub fn check_token(&self, token: String) -> serror::Result<()> {
let Some(valid_until) = self.map.lock().unwrap().remove(&token)
else {
return Err(
anyhow!("Terminal auth token not found")
.status_code(StatusCode::UNAUTHORIZED),
);
};
if komodo_timestamp() <= valid_until {
Ok(())
} else {
Err(
anyhow!("Terminal token is expired")
.status_code(StatusCode::UNAUTHORIZED),
)
}
}
}
pub async fn connect_terminal(
Query(ConnectTerminalQuery {
token,
terminal,
init,
}): Query<ConnectTerminalQuery>,
ws: WebSocketUpgrade,
) -> serror::Result<Response> {
if periphery_config().disable_terminals {
return Err(
anyhow!("Terminals are disabled in the periphery config")
.status_code(StatusCode::FORBIDDEN),
);
}
// Auth the connection with single use token
auth_tokens().check_token(token)?;
clean_up_terminals().await;
let terminal = get_terminal(&terminal).await?;
Ok(ws.on_upgrade(|mut socket| async move {
let init_res = async {
let (a, b) = terminal.history.bytes_parts();
if !a.is_empty() {
socket.send(Message::Binary(a)).await.context("Failed to send history part a")?;
}
if !b.is_empty() {
socket.send(Message::Binary(b)).await.context("Failed to send history part b")?;
}
if let Some(init) = init {
terminal
.stdin
.send(StdinMsg::Bytes(Bytes::from(init + "\n")))
.await
.context("Failed to run init command")?
}
anyhow::Ok(())
}.await;
if let Err(e) = init_res {
let _ = socket.send(Message::Text(format!("ERROR: {e:#}").into())).await;
let _ = socket.close().await;
return;
}
let (mut ws_write, mut ws_read) = socket.split();
let cancel = CancellationToken::new();
let ws_read = async {
loop {
let res = tokio::select! {
res = ws_read.next() => res,
_ = terminal.cancel.cancelled() => {
trace!("ws read: cancelled from outside");
break
},
_ = cancel.cancelled() => {
trace!("ws read: cancelled from inside");
break;
}
};
match res {
Some(Ok(Message::Binary(bytes)))
if bytes.first() == Some(&0x00) =>
{
// println!("Got ws read bytes - for stdin");
if let Err(e) = terminal.stdin.send(StdinMsg::Bytes(
Bytes::copy_from_slice(&bytes[1..]),
)).await {
debug!("WS -> PTY channel send error: {e:}");
terminal.cancel();
break;
};
}
Some(Ok(Message::Binary(bytes)))
if bytes.first() == Some(&0xFF) =>
{
// println!("Got ws read bytes - for resize");
if let Ok(dimensions) =
serde_json::from_slice::<ResizeDimensions>(&bytes[1..])
{
if let Err(e) =
terminal.stdin.send(StdinMsg::Resize(dimensions)).await
{
debug!("WS -> PTY channel send error: {e:}");
terminal.cancel();
break;
};
}
}
Some(Ok(Message::Text(text))) => {
trace!("Got ws read text");
if let Err(e) =
terminal.stdin.send(StdinMsg::Bytes(Bytes::from(text))).await
{
debug!("WS -> PTY channel send error: {e:?}");
terminal.cancel();
break;
};
}
Some(Ok(Message::Close(_))) => {
debug!("got ws read close");
cancel.cancel();
break;
}
Some(Ok(_)) => {
// Do nothing (ping, non-prefixed bytes, etc.)
}
Some(Err(e)) => {
debug!("Got ws read error: {e:?}");
cancel.cancel();
break;
}
None => {
debug!("Got ws read none");
cancel.cancel();
break;
}
}
}
};
let ws_write = async {
let mut stdout = terminal.stdout.resubscribe();
loop {
let res = tokio::select! {
res = stdout.recv() => res.context("Failed to get message over stdout receiver"),
_ = terminal.cancel.cancelled() => {
trace!("ws write: cancelled from outside");
let _ = ws_write.send(Message::Text(Utf8Bytes::from_static("PTY KILLED"))).await;
if let Err(e) = ws_write.close().await {
debug!("Failed to close ws: {e:?}");
};
break
},
_ = cancel.cancelled() => {
let _ = ws_write.send(Message::Text(Utf8Bytes::from_static("WS KILLED"))).await;
if let Err(e) = ws_write.close().await {
debug!("Failed to close ws: {e:?}");
};
break
}
};
match res {
Ok(bytes) => {
if let Err(e) =
ws_write.send(Message::Binary(bytes)).await
{
debug!("Failed to send to WS: {e:?}");
cancel.cancel();
break;
}
}
Err(e) => {
debug!("PTY -> WS channel read error: {e:?}");
let _ = ws_write.send(Message::Text(Utf8Bytes::from(format!("ERROR: {e:#}")))).await;
let _ = ws_write.close().await;
terminal.cancel();
break;
}
}
}
};
tokio::join!(ws_read, ws_write);
clean_up_terminals().await;
}))
}
+1 -1
View File
@@ -430,7 +430,7 @@ pub async fn write_stack(
Option<Vec<(String, String)>>,
)> {
let root = periphery_config()
.stack_dir()
.stack_dir
.join(to_komodo_name(&stack.name));
let run_directory = root.join(&stack.config.run_directory);
// This will remove any intermediate '/./' in the path, which is a problem for some OS.
+4 -15
View File
@@ -35,16 +35,8 @@ 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),
root_directory: env
.periphery_root_directory
.unwrap_or(config.root_directory),
repo_dir: env.periphery_repo_dir.or(config.repo_dir),
stack_dir: env.periphery_stack_dir.or(config.stack_dir),
build_dir: env.periphery_build_dir.or(config.build_dir),
disable_terminals: env
.periphery_disable_terminals
.unwrap_or(config.disable_terminals),
repo_dir: env.periphery_repo_dir.unwrap_or(config.repo_dir),
stack_dir: env.periphery_stack_dir.unwrap_or(config.stack_dir),
stats_polling_rate: env
.periphery_stats_polling_rate
.unwrap_or(config.stats_polling_rate),
@@ -60,9 +52,6 @@ pub fn periphery_config() -> &'static PeripheryConfig {
stdio: env
.periphery_logging_stdio
.unwrap_or(config.logging.stdio),
pretty: env
.periphery_logging_pretty
.unwrap_or(config.logging.pretty),
otlp_endpoint: env
.periphery_logging_otlp_endpoint
.unwrap_or(config.logging.otlp_endpoint),
@@ -89,10 +78,10 @@ pub fn periphery_config() -> &'static PeripheryConfig {
.unwrap_or(config.ssl_enabled),
ssl_key_file: env
.periphery_ssl_key_file
.or(config.ssl_key_file),
.unwrap_or(config.ssl_key_file),
ssl_cert_file: env
.periphery_ssl_cert_file
.or(config.ssl_cert_file),
.unwrap_or(config.ssl_cert_file),
secrets: config.secrets,
git_providers: config.git_providers,
docker_registries: config.docker_registries,
+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() {
+1 -1
View File
@@ -101,7 +101,7 @@ pub async fn pull_or_clone_stack(
}
let root = periphery_config()
.stack_dir()
.stack_dir
.join(to_komodo_name(&stack.name));
let mut args: CloneArgs = stack.into();
+9 -17
View File
@@ -12,9 +12,9 @@ mod compose;
mod config;
mod docker;
mod helpers;
mod router;
mod ssl;
mod stats;
mod terminal;
async fn app() -> anyhow::Result<()> {
dotenvy::dotenv().ok();
@@ -26,17 +26,12 @@ async fn app() -> anyhow::Result<()> {
stats::spawn_system_stats_polling_thread();
let addr = format!(
"{}:{}",
config::periphery_config().bind_ip,
config::periphery_config().port
);
let socket_addr =
SocketAddr::from_str(&format!("0.0.0.0:{}", config.port))
.context("failed to parse socket addr")?;
let socket_addr = SocketAddr::from_str(&addr)
.context("failed to parse listen address")?;
let app =
api::router().into_make_service_with_connect_info::<SocketAddr>();
let app = router::router()
.into_make_service_with_connect_info::<SocketAddr>();
if config.ssl_enabled {
info!("🔒 Periphery SSL Enabled");
@@ -46,8 +41,8 @@ async fn app() -> anyhow::Result<()> {
ssl::ensure_certs().await;
info!("Komodo Periphery starting on https://{}", socket_addr);
let ssl_config = RustlsConfig::from_pem_file(
config.ssl_cert_file(),
config.ssl_key_file(),
&config.ssl_cert_file,
&config.ssl_key_file,
)
.await
.context("Invalid ssl cert / key")?;
@@ -73,10 +68,7 @@ async fn main() -> anyhow::Result<()> {
tokio::select! {
res = app => return res?,
_ = term_signal.recv() => {
info!("Exiting all active Terminals for shutdown");
terminal::delete_all_terminals().await;
},
_ = term_signal.recv() => {},
}
Ok(())
@@ -8,7 +8,7 @@ use axum::{
http::{Request, StatusCode},
middleware::{self, Next},
response::Response,
routing::{get, post},
routing::post,
};
use derive_variants::ExtractVariant;
use resolver_api::Resolve;
@@ -19,13 +19,9 @@ use crate::config::periphery_config;
pub fn router() -> Router {
Router::new()
.merge(
Router::new()
.route("/", post(handler))
.layer(middleware::from_fn(guard_request_by_passkey)),
)
.route("/terminal", get(super::terminal::connect_terminal))
.route("/", post(handler))
.layer(middleware::from_fn(guard_request_by_ip))
.layer(middleware::from_fn(guard_request_by_passkey))
}
async fn handler(
+5 -9
View File
@@ -2,8 +2,7 @@ use crate::config::periphery_config;
pub async fn ensure_certs() {
let config = periphery_config();
if !config.ssl_cert_file().is_file()
|| !config.ssl_key_file().is_file()
if !config.ssl_cert_file.is_file() || !config.ssl_key_file.is_file()
{
generate_self_signed_ssl_certs().await
}
@@ -15,19 +14,16 @@ async fn generate_self_signed_ssl_certs() {
let config = periphery_config();
let ssl_key_file = config.ssl_key_file();
let ssl_cert_file = config.ssl_cert_file();
// ensure cert folders exist
if let Some(parent) = ssl_key_file.parent() {
if let Some(parent) = config.ssl_key_file.parent() {
let _ = std::fs::create_dir_all(parent);
}
if let Some(parent) = ssl_cert_file.parent() {
if let Some(parent) = config.ssl_cert_file.parent() {
let _ = std::fs::create_dir_all(parent);
}
let key_path = ssl_key_file.display();
let cert_path = ssl_cert_file.display();
let key_path = &config.ssl_key_file.display();
let cert_path = &config.ssl_cert_file.display();
let command = format!(
"openssl req -x509 -newkey rsa:4096 -keyout {key_path} -out {cert_path} -sha256 -days 3650 -nodes -subj \"/C=XX/CN=periphery\""
+1 -2
View File
@@ -194,13 +194,12 @@ fn get_system_information(
os: System::long_os_version(),
kernel: System::kernel_version(),
host_name: System::host_name(),
core_count: System::physical_core_count().map(|c| c as u32),
core_count: sys.physical_core_count().map(|c| c as u32),
cpu_brand: sys
.cpus()
.iter()
.next()
.map(|cpu| cpu.brand().to_string())
.unwrap_or_default(),
terminals_disabled: periphery_config().disable_terminals,
}
}
-346
View File
@@ -1,346 +0,0 @@
use std::{
collections::{HashMap, VecDeque},
sync::{Arc, OnceLock},
time::Duration,
};
use anyhow::{Context, anyhow};
use bytes::Bytes;
use komodo_client::{
api::write::TerminalRecreateMode, entities::server::TerminalInfo,
};
use portable_pty::{CommandBuilder, PtySize, native_pty_system};
use tokio::sync::{broadcast, mpsc};
use tokio_util::sync::CancellationToken;
type PtyName = String;
type PtyMap = tokio::sync::RwLock<HashMap<PtyName, Arc<Terminal>>>;
type StdinSender = mpsc::Sender<StdinMsg>;
type StdoutReceiver = broadcast::Receiver<Bytes>;
pub async fn create_terminal(
name: String,
command: String,
recreate: TerminalRecreateMode,
) -> anyhow::Result<()> {
trace!(
"CreateTerminal: {name} | command: {command} | recreate: {recreate:?}"
);
let mut terminals = terminals().write().await;
use TerminalRecreateMode::*;
if matches!(recreate, Never | DifferentCommand) {
if let Some(terminal) = terminals.get(&name) {
if terminal.command == command {
return Ok(());
} else if matches!(recreate, Never) {
return Err(anyhow!(
"Terminal {name} already exists, but has command {} instead of {command}",
terminal.command
));
}
}
}
if let Some(prev) = terminals.insert(
name,
Terminal::new(command)
.await
.context("Failed to init terminal")?
.into(),
) {
prev.cancel();
}
Ok(())
}
pub async fn delete_terminal(name: &str) {
if let Some(terminal) = terminals().write().await.remove(name) {
terminal.cancel.cancel();
}
}
pub async fn list_terminals() -> Vec<TerminalInfo> {
let mut terminals = terminals()
.read()
.await
.iter()
.map(|(name, terminal)| TerminalInfo {
name: name.to_string(),
command: terminal.command.clone(),
stored_size_kb: terminal.history.size_kb(),
})
.collect::<Vec<_>>();
terminals.sort_by(|a, b| a.name.cmp(&b.name));
terminals
}
pub async fn get_terminal(
name: &str,
) -> anyhow::Result<Arc<Terminal>> {
terminals()
.read()
.await
.get(name)
.cloned()
.with_context(|| format!("No terminal at {name}"))
}
pub async fn clean_up_terminals() {
terminals()
.write()
.await
.retain(|_, terminal| !terminal.cancel.is_cancelled());
}
pub async fn delete_all_terminals() {
terminals()
.write()
.await
.drain()
.for_each(|(_, terminal)| terminal.cancel());
// The terminals poll cancel every 500 millis, need to wait for them
// to finish cancelling.
tokio::time::sleep(Duration::from_millis(100)).await;
}
fn terminals() -> &'static PtyMap {
static TERMINALS: OnceLock<PtyMap> = OnceLock::new();
TERMINALS.get_or_init(Default::default)
}
#[derive(Clone, serde::Deserialize)]
pub struct ResizeDimensions {
rows: u16,
cols: u16,
}
#[derive(Clone)]
pub enum StdinMsg {
Bytes(Bytes),
Resize(ResizeDimensions),
}
pub struct Terminal {
/// The command that was used as the root command, eg `shell`
command: String,
pub cancel: CancellationToken,
pub stdin: StdinSender,
pub stdout: StdoutReceiver,
pub history: Arc<History>,
}
impl Terminal {
async fn new(command: String) -> anyhow::Result<Terminal> {
trace!("Creating terminal with command: {command}");
let terminal = native_pty_system()
.openpty(PtySize::default())
.context("Failed to open terminal")?;
let mut command_split = command.split(' ').map(|arg| arg.trim());
let cmd =
command_split.next().context("Command cannot be empty")?;
let mut cmd = CommandBuilder::new(cmd);
for arg in command_split {
cmd.arg(arg);
}
cmd.env("TERM", "xterm-256color");
cmd.env("COLORTERM", "truecolor");
let mut child = terminal
.slave
.spawn_command(cmd)
.context("Failed to spawn child command")?;
// Check the child didn't stop immediately (after a little wait) with error
tokio::time::sleep(Duration::from_millis(100)).await;
if let Some(status) = child
.try_wait()
.context("Failed to check child process exit status")?
{
return Err(anyhow!(
"Child process exited immediately with code {}",
status.exit_code()
));
}
let mut terminal_write = terminal
.master
.take_writer()
.context("Failed to take terminal writer")?;
let mut terminal_read = terminal
.master
.try_clone_reader()
.context("Failed to clone terminal reader")?;
let cancel = CancellationToken::new();
// CHILD WAIT TASK
let _cancel = cancel.clone();
tokio::task::spawn_blocking(move || {
loop {
if _cancel.is_cancelled() {
trace!("child wait handle cancelled from outside");
if let Err(e) = child.kill() {
debug!("Failed to kill child | {e:?}");
}
break;
}
match child.try_wait() {
Ok(Some(code)) => {
debug!("child exited with code {code}");
_cancel.cancel();
break;
}
Ok(None) => {
std::thread::sleep(Duration::from_millis(500));
}
Err(e) => {
debug!("failed to wait for child | {e:?}");
_cancel.cancel();
break;
}
}
}
});
// WS (channel) -> STDIN TASK
// Theres only one consumer here, so use mpsc
let (stdin, mut channel_read) =
tokio::sync::mpsc::channel::<StdinMsg>(8192);
let _cancel = cancel.clone();
tokio::task::spawn_blocking(move || {
loop {
if _cancel.is_cancelled() {
trace!("terminal write: cancelled from outside");
break;
}
match channel_read.blocking_recv() {
Some(StdinMsg::Bytes(bytes)) => {
if let Err(e) = terminal_write.write_all(&bytes) {
debug!("Failed to write to PTY: {e:?}");
_cancel.cancel();
break;
}
}
Some(StdinMsg::Resize(dimensions)) => {
if let Err(e) = terminal.master.resize(PtySize {
cols: dimensions.cols,
rows: dimensions.rows,
pixel_width: 0,
pixel_height: 0,
}) {
debug!("Failed to resize | {e:?}");
_cancel.cancel();
break;
};
}
None => {
debug!("WS -> PTY channel read error: Disconnected");
_cancel.cancel();
break;
}
}
}
});
let history = Arc::new(History::default());
// PTY -> WS (channel) TASK
// Uses broadcast to output to multiple client simultaneously
let (write, stdout) =
tokio::sync::broadcast::channel::<Bytes>(8192);
let _cancel = cancel.clone();
let _history = history.clone();
tokio::task::spawn_blocking(move || {
let mut buf = [0u8; 8192];
loop {
if _cancel.is_cancelled() {
trace!("terminal read: cancelled from outside");
break;
}
match terminal_read.read(&mut buf) {
Ok(0) => {
// EOF
trace!("Got PTY read EOF");
_cancel.cancel();
break;
}
Ok(n) => {
_history.push(&buf[..n]);
if let Err(e) =
write.send(Bytes::copy_from_slice(&buf[..n]))
{
debug!("PTY -> WS channel send error: {e:?}");
_cancel.cancel();
break;
}
}
Err(e) => {
debug!("Failed to read for PTY: {e:?}");
_cancel.cancel();
break;
}
}
}
});
trace!("terminal tasks spawned");
Ok(Terminal {
command,
cancel,
stdin,
stdout,
history,
})
}
pub fn cancel(&self) {
trace!("Cancel called");
self.cancel.cancel();
}
}
/// 1 MiB max history size per terminal
const MAX_BYTES: usize = 1024 * 1024;
pub struct History {
buf: std::sync::RwLock<VecDeque<u8>>,
}
impl Default for History {
fn default() -> Self {
History {
buf: VecDeque::with_capacity(MAX_BYTES).into(),
}
}
}
impl History {
/// Push some bytes, evicting the oldest when full.
fn push(&self, bytes: &[u8]) {
let mut buf = self.buf.write().unwrap();
for byte in bytes {
if buf.len() == MAX_BYTES {
buf.pop_front();
}
buf.push_back(*byte);
}
}
pub fn bytes_parts(&self) -> (Bytes, Bytes) {
let buf = self.buf.read().unwrap();
let (a, b) = buf.as_slices();
(Bytes::copy_from_slice(a), Bytes::copy_from_slice(b))
}
pub fn size_kb(&self) -> f64 {
self.buf.read().unwrap().len() as f64 / 1024.0
}
}
-67
View File
@@ -1,67 +0,0 @@
## This is used to customize the shell prompt in Periphery container for Terminals
"$schema" = 'https://starship.rs/config-schema.json'
add_newline = true
format = "$time$hostname$container$memory_usage$all"
[character]
success_symbol = "[❯](bright-blue bold)"
error_symbol = "[❯](bright-red bold)"
[package]
disabled = true
[time]
format = "[❯$time](white dimmed) "
time_format = "%l:%M %p"
utc_time_offset = '-5'
disabled = true
[username]
format = "[❯ $user]($style) "
style_user = "bright-green"
show_always = true
[hostname]
format = "[❯ $hostname]($style) "
style = "bright-blue"
ssh_only = false
[directory]
format = "[❯ $path]($style)[$read_only]($read_only_style) "
style = "bright-cyan"
[git_branch]
format = "[❯ $symbol$branch(:$remote_branch)]($style) "
style = "bright-purple"
[git_status]
style = "bright-purple"
[rust]
format = "[❯ $symbol($version )]($style)"
symbol = "rustc "
style = "bright-red"
[nodejs]
format = "[❯ $symbol($version )]($style)"
symbol = "nodejs "
style = "bright-red"
[memory_usage]
format = "[❯ mem ${ram} ${ram_pct}]($style) "
threshold = -1
style = "white"
[cmd_duration]
format = "[❯ $duration]($style)"
style = "bright-yellow"
[container]
format = "[❯ 🦎 periphery container ]($style)"
style = "bright-green"
[aws]
disabled = true
+2 -2
View File
@@ -1,7 +1,7 @@
# Komodo
*A system to build and deploy software across many servers*. [https://komo.do](https://komo.do)
*A system to build and deploy software across many servers*
Docs: [https://docs.rs/komodo_client/latest/komodo_client](https://docs.rs/komodo_client/latest/komodo_client).
Full 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:
-1
View File
@@ -68,7 +68,6 @@
pub mod auth;
pub mod execute;
pub mod terminal;
pub mod read;
pub mod user;
pub mod write;
+1 -54
View File
@@ -13,7 +13,7 @@ use crate::entities::{
},
server::{
Server, ServerActionState, ServerListItem, ServerQuery,
ServerState, TerminalInfo,
ServerState,
},
stack::ComposeProject,
stats::{
@@ -294,35 +294,6 @@ 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(
@@ -628,27 +599,3 @@ pub struct GetServersSummaryResponse {
/// The number of disabled servers.
pub disabled: I64,
}
//
/// List the current terminals on specified server.
/// Response: [ListTerminalsResponse].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoReadRequest)]
#[response(ListTerminalsResponse)]
#[error(serror::Error)]
pub struct ListTerminals {
/// Id or name
#[serde(alias = "id", alias = "name")]
pub server: String,
/// Force a fresh call to Periphery for the list.
/// Otherwise the response will be cached for 30s
#[serde(default)]
pub fresh: bool,
}
#[typeshare]
pub type ListTerminalsResponse = Vec<TerminalInfo>;
-20
View File
@@ -1,20 +0,0 @@
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
/// Query to connect to a terminal (interactive shell over websocket) on the given server.
/// TODO: Document calling.
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ConnectTerminalQuery {
/// Server Id or name
pub server: String,
/// Each periphery can keep multiple terminals open.
/// If a terminals with the specified name already exists,
/// it will be attached to.
/// Otherwise a new terminal will be created for the command,
/// which will persist until it is deleted using
/// [DeleteTerminal][crate::api::write::server::DeleteTerminal]
pub terminal: String,
/// Optional. The initial command to execute on connection to the shell.
pub init: Option<String>,
}
+1
View File
@@ -98,6 +98,7 @@ 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,6 +95,7 @@ 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,
+1 -18
View File
@@ -98,6 +98,7 @@ 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,
@@ -107,24 +108,6 @@ 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,6 +97,7 @@ 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,6 +122,7 @@ 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,6 +109,7 @@ 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,6 +101,7 @@ 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 -81
View File
@@ -4,7 +4,6 @@ use serde::{Deserialize, Serialize};
use typeshare::typeshare;
use crate::entities::{
NoData,
server::{_PartialServerConfig, Server},
update::Update,
};
@@ -80,6 +79,7 @@ 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,89 +100,10 @@ pub struct RenameServer {
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
#[error(serror::Error)]
pub struct CreateNetwork {
/// Server Id or name
pub server: String,
/// The name of the network to create.
pub name: String,
}
//
/// Configures the behavior of [CreateTerminal] if the
/// specified terminal name already exists.
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)]
pub enum TerminalRecreateMode {
/// Never kill the old terminal if it already exists.
/// If the command is different, returns error.
#[default]
Never,
/// Always kill the old terminal and create new one
Always,
/// Only kill and recreate if the command is different.
DifferentCommand,
}
/// Create a terminal on the server.
/// Response: [NoData]
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoWriteRequest)]
#[response(NoData)]
#[error(serror::Error)]
pub struct CreateTerminal {
/// Server Id or name
pub server: String,
/// The name of the terminal on the server to create.
pub name: String,
/// The shell command (eg `bash`) to init the shell.
///
/// This can also include args:
/// `docker exec -it container sh`
///
/// Default: `bash`
#[serde(default = "default_command")]
pub command: String,
/// Default: `Never`
#[serde(default)]
pub recreate: TerminalRecreateMode,
}
fn default_command() -> String {
String::from("bash")
}
//
/// Delete a terminal on the server.
/// Response: [NoData]
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoWriteRequest)]
#[response(NoData)]
#[error(serror::Error)]
pub struct DeleteTerminal {
/// Server Id or name
pub server: String,
/// The name of the terminal on the server to delete.
pub terminal: String,
}
/// Delete all terminals on the server.
/// Response: [NoData]
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoWriteRequest)]
#[response(NoData)]
#[error(serror::Error)]
pub struct DeleteAllTerminals {
/// Server Id or name
pub server: String,
}
@@ -97,6 +97,7 @@ 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,6 +100,7 @@ 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,
@@ -117,6 +118,7 @@ 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,6 +99,7 @@ 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,
@@ -132,6 +133,7 @@ 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")]
@@ -164,6 +166,7 @@ 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")]
+9 -87
View File
@@ -13,10 +13,7 @@ use crate::{
entities::I64,
};
use super::{
ScheduleFormat,
resource::{Resource, ResourceListItem, ResourceQuery},
};
use super::resource::{Resource, ResourceListItem, ResourceQuery};
#[typeshare]
pub type ActionListItem = ResourceListItem<ActionListItemInfo>;
@@ -28,12 +25,6 @@ pub struct ActionListItemInfo {
pub last_run_at: I64,
/// Whether last action run successful
pub state: ActionState,
/// If the procedure has schedule enabled, this is the
/// next scheduled run time in unix ms.
pub next_scheduled_run: Option<I64>,
/// If there is an error parsing schedule expression,
/// it will be given here.
pub schedule_error: Option<String>,
}
#[typeshare]
@@ -71,55 +62,14 @@ pub type _PartialActionConfig = PartialActionConfig;
#[partial_derive(Serialize, Deserialize, Debug, Clone, Default)]
#[partial(skip_serializing_none, from, diff)]
pub struct ActionConfig {
/// Choose whether to specify schedule as regular CRON, or using the english to CRON parser.
#[serde(default)]
/// Typescript file contents using pre-initialized `komodo` client.
#[serde(default, deserialize_with = "file_contents_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_file_contents_deserializer"
))]
#[builder(default)]
pub schedule_format: ScheduleFormat,
/// Optionally provide a schedule for the procedure to run on.
///
/// There are 2 ways to specify a schedule:
///
/// 1. Regular CRON expression:
///
/// (second, minute, hour, day, month, day-of-week)
/// ```
/// 0 0 0 1,15 * ?
/// ```
///
/// 2. "English" expression via [english-to-cron](https://crates.io/crates/english-to-cron):
///
/// ```
/// at midnight on the 1st and 15th of the month
/// ```
#[serde(default)]
#[builder(default)]
pub schedule: String,
/// Whether schedule is enabled if one is provided.
/// Can be used to temporarily disable the schedule.
#[serde(default = "default_schedule_enabled")]
#[builder(default = "default_schedule_enabled()")]
#[partial_default(default_schedule_enabled())]
pub schedule_enabled: bool,
/// Optional. A TZ Identifier. If not provided, will use Core local timezone.
/// https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.
#[serde(default)]
#[builder(default)]
pub schedule_timezone: String,
/// Whether to send alerts when the schedule was run.
#[serde(default = "default_schedule_alert")]
#[builder(default = "default_schedule_alert()")]
#[partial_default(default_schedule_alert())]
pub schedule_alert: bool,
/// Whether to send alerts when this action fails.
#[serde(default = "default_failure_alert")]
#[builder(default = "default_failure_alert()")]
#[partial_default(default_failure_alert())]
pub failure_alert: bool,
pub file_contents: String,
/// Whether incoming webhooks actually trigger action.
#[serde(default = "default_webhook_enabled")]
@@ -132,28 +82,6 @@ pub struct ActionConfig {
#[serde(default)]
#[builder(default)]
pub webhook_secret: String,
/// Typescript file contents using pre-initialized `komodo` client.
/// Supports variable / secret interpolation.
#[serde(default, deserialize_with = "file_contents_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_file_contents_deserializer"
))]
#[builder(default)]
pub file_contents: String,
}
fn default_schedule_enabled() -> bool {
true
}
fn default_schedule_alert() -> bool {
true
}
fn default_failure_alert() -> bool {
true
}
fn default_webhook_enabled() -> bool {
@@ -169,15 +97,9 @@ impl ActionConfig {
impl Default for ActionConfig {
fn default() -> Self {
Self {
schedule_format: Default::default(),
schedule: Default::default(),
schedule_enabled: default_schedule_enabled(),
schedule_timezone: Default::default(),
schedule_alert: default_schedule_alert(),
failure_alert: default_failure_alert(),
file_contents: Default::default(),
webhook_enabled: default_webhook_enabled(),
webhook_secret: Default::default(),
file_contents: Default::default(),
}
}
}
+2 -28
View File
@@ -8,8 +8,8 @@ use typeshare::typeshare;
use crate::entities::{I64, MongoId};
use super::{
_Serror, ResourceTarget, ResourceTargetVariant, Version,
deployment::DeploymentState, stack::StackState,
_Serror, ResourceTarget, Version, deployment::DeploymentState,
stack::StackState,
};
/// Representation of an alert in the system.
@@ -260,32 +260,6 @@ pub enum AlertData {
/// The name of the repo
name: String,
},
/// A procedure has failed
ProcedureFailed {
/// The id of the procedure
id: String,
/// The name of the procedure
name: String,
},
/// An action has failed
ActionFailed {
/// The id of the action
id: String,
/// The name of the action
name: String,
},
/// A schedule was run
ScheduleRun {
/// Procedure or Action
resource_type: ResourceTargetVariant,
/// The resource id
id: String,
/// The resource name
name: String,
},
}
impl Default for AlertData {
+1 -56
View File
@@ -111,12 +111,6 @@ pub enum AlerterEndpoint {
/// Send alert to a Discord app
Discord(DiscordAlerterEndpoint),
/// Send alert to Ntfy
Ntfy(NtfyAlerterEndpoint),
/// Send alert to Pushover
Pushover(PushoverAlerterEndpoint),
}
impl Default for AlerterEndpoint {
@@ -201,57 +195,8 @@ 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")
}
/// Configuration for a Pushover alerter.
#[typeshare]
#[derive(
Debug, Clone, PartialEq, Serialize, Deserialize, Builder,
)]
pub struct PushoverAlerterEndpoint {
/// The pushover URL including application and user tokens in parameters.
#[serde(default = "default_pushover_url")]
#[builder(default = "default_pushover_url()")]
pub url: String,
}
impl Default for PushoverAlerterEndpoint {
fn default() -> Self {
Self {
url: default_pushover_url(),
}
}
}
fn default_pushover_url() -> String {
String::from(
"https://api.pushover.net/1/messages.json?token=XXXXXXXXXXXXX&user=XXXXXXXXXXXXX",
)
}
// QUERY
#[typeshare]
pub type AlerterQuery = ResourceQuery<AlerterQuerySpecifics>;
+11 -47
View File
@@ -32,31 +32,26 @@ 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: Option<String>,
pub git_provider: String,
/// The image registry domain
pub image_registry_domain: String,
/// The repo used as the source of the build
pub repo: Option<String>,
pub repo: String,
/// The branch of the repo
pub branch: Option<String>,
pub branch: String,
/// State of the build. Reflects whether most recent build successful.
pub state: BuildState,
/// 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]
@@ -78,26 +73,11 @@ 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
@@ -141,8 +121,8 @@ pub struct BuildConfig {
#[builder(default)]
pub image_name: String,
/// 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.
/// 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.
/// 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
@@ -212,13 +192,10 @@ pub struct BuildConfig {
#[builder(default)]
pub webhook_secret: String,
/// 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.
/// The optional command run after repo clone and before docker build.
#[serde(default)]
#[builder(default)]
pub files_on_host: bool,
pub pre_build: SystemCommand,
/// The path of the docker build context relative to the root of the repo.
/// Default: "." (the root of the repo).
@@ -261,17 +238,6 @@ 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`.
@@ -372,8 +338,6 @@ 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,8 +44,6 @@ 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
@@ -85,8 +83,6 @@ pub struct Env {
pub komodo_logging_level: Option<LogLevel>,
/// Override `logging.stdio`
pub komodo_logging_stdio: Option<StdioLogMode>,
/// Override `logging.pretty`
pub komodo_logging_pretty: Option<bool>,
/// Override `logging.otlp_endpoint`
pub komodo_logging_otlp_endpoint: Option<String>,
/// Override `logging.opentelemetry_service_name`
@@ -260,11 +256,6 @@ 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,
@@ -530,10 +521,6 @@ fn default_core_port() -> u16 {
9120
}
fn default_core_bind_ip() -> String {
"[::]".to_string()
}
fn default_frontend_path() -> String {
"/app/frontend".to_string()
}
@@ -584,7 +571,6 @@ 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,
+28 -105
View File
@@ -114,18 +114,10 @@ pub struct Env {
/// Override `port`
pub periphery_port: Option<u16>,
/// Override `bind_ip`
pub periphery_bind_ip: Option<String>,
/// Override `root_directory`
pub periphery_root_directory: Option<PathBuf>,
/// 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 `disable_terminals`
pub periphery_disable_terminals: Option<bool>,
/// Override `stats_polling_rate`
pub periphery_stats_polling_rate: Option<Timelength>,
/// Override `legacy_compose_cli`
@@ -136,8 +128,6 @@ pub struct Env {
pub periphery_logging_level: Option<LogLevel>,
/// Override `logging.stdio`
pub periphery_logging_stdio: Option<StdioLogMode>,
/// Override `logging.pretty`
pub periphery_logging_pretty: Option<bool>,
/// Override `logging.otlp_endpoint`
pub periphery_logging_otlp_endpoint: Option<String>,
/// Override `logging.opentelemetry_service_name`
@@ -172,44 +162,15 @@ 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 directory Komodo will use as the default root for the specific (repo, stack, build) directories.
///
/// repo: ${root_directory}/repos
/// stack: ${root_directory}/stacks
/// build: ${root_directory}/builds
///
/// Note. These can each be overridden with a specific directory
/// by specifying `repo_dir`, `stack_dir`, or `build_dir` explicitly
///
/// Default: `/etc/komodo`
#[serde(default = "default_root_directory")]
pub root_directory: PathBuf,
/// The system directory where Komodo managed repos will be cloned.
/// If not provided, will default to `${root_directory}/repos`.
/// Default: empty
pub repo_dir: Option<PathBuf>,
/// Default: `/etc/komodo/repos`
#[serde(default = "default_repo_dir")]
pub repo_dir: PathBuf,
/// The system directory where stacks will managed.
/// If not provided, will default to `${root_directory}/stacks`.
/// Default: empty
pub stack_dir: Option<PathBuf>,
/// The system directory where builds will managed.
/// If not provided, will default to `${root_directory}/builds`.
/// Default: empty
pub build_dir: Option<PathBuf>,
/// Whether to disable the terminal APIs
/// and disallow remote shell access.
/// Default: false
#[serde(default)]
pub disable_terminals: bool,
/// Default: `/etc/komodo/stacks`
#[serde(default = "default_stack_dir")]
pub stack_dir: PathBuf,
/// The rate at which the system stats will be polled to update the cache.
/// Default: `5-sec`
@@ -269,24 +230,26 @@ pub struct PeripheryConfig {
pub ssl_enabled: bool,
/// Path to the ssl key.
/// Default: `${root_directory}/ssl/key.pem`.
pub ssl_key_file: Option<PathBuf>,
/// Default: `/etc/komodo/ssl/periphery/key.pem`.
#[serde(default = "default_ssl_key_file")]
pub ssl_key_file: PathBuf,
/// Path to the ssl cert.
/// Default: `${root_directory}/ssl/cert.pem`.
pub ssl_cert_file: Option<PathBuf>,
/// Default: `/etc/komodo/ssl/periphery/cert.pem`.
#[serde(default = "default_ssl_cert_file")]
pub ssl_cert_file: PathBuf,
}
fn default_periphery_port() -> u16 {
8120
}
fn default_periphery_bind_ip() -> String {
"[::]".to_string()
fn default_repo_dir() -> PathBuf {
"/etc/komodo/repos".parse().unwrap()
}
fn default_root_directory() -> PathBuf {
"/etc/komodo".parse().unwrap()
fn default_stack_dir() -> PathBuf {
"/etc/komodo/stacks".parse().unwrap()
}
fn default_stats_polling_rate() -> Timelength {
@@ -297,16 +260,20 @@ fn default_ssl_enabled() -> bool {
false
}
fn default_ssl_key_file() -> PathBuf {
"/etc/komodo/ssl/key.pem".parse().unwrap()
}
fn default_ssl_cert_file() -> PathBuf {
"/etc/komodo/ssl/cert.pem".parse().unwrap()
}
impl Default for PeripheryConfig {
fn default() -> Self {
Self {
port: default_periphery_port(),
bind_ip: default_periphery_bind_ip(),
root_directory: default_root_directory(),
repo_dir: None,
stack_dir: None,
build_dir: None,
disable_terminals: Default::default(),
repo_dir: default_repo_dir(),
stack_dir: default_stack_dir(),
stats_polling_rate: default_stats_polling_rate(),
legacy_compose_cli: Default::default(),
logging: Default::default(),
@@ -318,8 +285,8 @@ impl Default for PeripheryConfig {
git_providers: Default::default(),
docker_registries: Default::default(),
ssl_enabled: default_ssl_enabled(),
ssl_key_file: None,
ssl_cert_file: None,
ssl_key_file: default_ssl_key_file(),
ssl_cert_file: default_ssl_cert_file(),
}
}
}
@@ -328,12 +295,8 @@ impl PeripheryConfig {
pub fn sanitized(&self) -> PeripheryConfig {
PeripheryConfig {
port: self.port,
bind_ip: self.bind_ip.clone(),
root_directory: self.root_directory.clone(),
repo_dir: self.repo_dir.clone(),
stack_dir: self.stack_dir.clone(),
build_dir: self.build_dir.clone(),
disable_terminals: self.disable_terminals,
stats_polling_rate: self.stats_polling_rate,
legacy_compose_cli: self.legacy_compose_cli,
logging: self.logging.clone(),
@@ -389,44 +352,4 @@ impl PeripheryConfig {
ssl_cert_file: self.ssl_cert_file.clone(),
}
}
pub fn repo_dir(&self) -> PathBuf {
if let Some(dir) = &self.repo_dir {
dir.to_owned()
} else {
self.root_directory.join("repos")
}
}
pub fn stack_dir(&self) -> PathBuf {
if let Some(dir) = &self.stack_dir {
dir.to_owned()
} else {
self.root_directory.join("stacks")
}
}
pub fn build_dir(&self) -> PathBuf {
if let Some(dir) = &self.build_dir {
dir.to_owned()
} else {
self.root_directory.join("builds")
}
}
pub fn ssl_key_file(&self) -> PathBuf {
if let Some(dir) = &self.ssl_key_file {
dir.to_owned()
} else {
self.root_directory.join("ssl/key.pem")
}
}
pub fn ssl_cert_file(&self) -> PathBuf {
if let Some(dir) = &self.ssl_cert_file {
dir.to_owned()
} else {
self.root_directory.join("ssl/cert.pem")
}
}
}
-4
View File
@@ -10,9 +10,6 @@ pub struct LogConfig {
#[serde(default)]
pub stdio: StdioLogMode,
#[serde(default)]
pub pretty: bool,
/// Enable opentelemetry exporting
#[serde(default)]
pub otlp_endpoint: String,
@@ -30,7 +27,6 @@ impl Default for LogConfig {
Self {
level: Default::default(),
stdio: Default::default(),
pretty: Default::default(),
otlp_endpoint: Default::default(),
opentelemetry_service_name: default_opentelemetry_service_name(
),
+3 -23
View File
@@ -118,12 +118,11 @@ pub fn all_logs_success(logs: &[update::Log]) -> bool {
true
}
pub fn optional_string(string: impl Into<String>) -> Option<String> {
let string = string.into();
pub fn optional_string(string: &str) -> Option<String> {
if string.is_empty() {
None
} else {
Some(string)
Some(string.to_string())
}
}
@@ -395,8 +394,6 @@ 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
@@ -462,9 +459,8 @@ 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: None,
on_clone: build.config.pre_build.clone().into_option(),
on_pull: None,
https: build.config.git_https,
account: optional_string(&build.config.git_account),
@@ -482,7 +478,6 @@ 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(),
@@ -502,7 +497,6 @@ 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,
@@ -522,7 +516,6 @@ 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,
@@ -720,7 +713,6 @@ pub enum Operation {
DeleteBuild,
RunBuild,
CancelBuild,
WriteDockerfile,
// repo
CreateRepo,
@@ -999,15 +991,3 @@ impl ResourceTargetVariant {
}
}
}
#[typeshare]
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize,
)]
pub enum ScheduleFormat {
#[default]
English,
Cron,
}
pub const KOMODO_EXIT_DATA: &str = "__KOMODO_EXIT_DATA:";
+1 -75
View File
@@ -9,7 +9,7 @@ use typeshare::typeshare;
use crate::api::execute::Execution;
use super::{
I64, ScheduleFormat,
I64,
resource::{Resource, ResourceListItem, ResourceQuery},
};
@@ -23,12 +23,6 @@ pub struct ProcedureListItemInfo {
pub stages: I64,
/// Reflect whether last run successful / currently running.
pub state: ProcedureState,
/// If the procedure has schedule enabled, this is the
/// next scheduled run time in unix ms.
pub next_scheduled_run: Option<I64>,
/// If there is an error parsing schedule expression,
/// it will be given here.
pub schedule_error: Option<String>,
}
#[typeshare]
@@ -67,56 +61,6 @@ pub struct ProcedureConfig {
#[builder(default)]
pub stages: Vec<ProcedureStage>,
/// Choose whether to specify schedule as regular CRON, or using the english to CRON parser.
#[serde(default)]
#[builder(default)]
pub schedule_format: ScheduleFormat,
/// Optionally provide a schedule for the procedure to run on.
///
/// There are 2 ways to specify a schedule:
///
/// 1. Regular CRON expression:
///
/// (second, minute, hour, day, month, day-of-week)
/// ```
/// 0 0 0 1,15 * ?
/// ```
///
/// 2. "English" expression via [english-to-cron](https://crates.io/crates/english-to-cron):
///
/// ```
/// at midnight on the 1st and 15th of the month
/// ```
#[serde(default)]
#[builder(default)]
pub schedule: String,
/// Whether schedule is enabled if one is provided.
/// Can be used to temporarily disable the schedule.
#[serde(default = "default_schedule_enabled")]
#[builder(default = "default_schedule_enabled()")]
#[partial_default(default_schedule_enabled())]
pub schedule_enabled: bool,
/// Optional. A TZ Identifier. If not provided, will use Core local timezone.
/// https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.
#[serde(default)]
#[builder(default)]
pub schedule_timezone: String,
/// Whether to send alerts when the schedule was run.
#[serde(default = "default_schedule_alert")]
#[builder(default = "default_schedule_alert()")]
#[partial_default(default_schedule_alert())]
pub schedule_alert: bool,
/// Whether to send alerts when this procedure fails.
#[serde(default = "default_failure_alert")]
#[builder(default = "default_failure_alert()")]
#[partial_default(default_failure_alert())]
pub failure_alert: bool,
/// Whether incoming webhooks actually trigger action.
#[serde(default = "default_webhook_enabled")]
#[builder(default = "default_webhook_enabled()")]
@@ -136,18 +80,6 @@ impl ProcedureConfig {
}
}
fn default_schedule_enabled() -> bool {
true
}
fn default_schedule_alert() -> bool {
true
}
fn default_failure_alert() -> bool {
true
}
fn default_webhook_enabled() -> bool {
true
}
@@ -156,12 +88,6 @@ impl Default for ProcedureConfig {
fn default() -> Self {
Self {
stages: Default::default(),
schedule_format: Default::default(),
schedule: Default::default(),
schedule_enabled: default_schedule_enabled(),
schedule_timezone: Default::default(),
schedule_alert: default_schedule_alert(),
failure_alert: default_failure_alert(),
webhook_enabled: default_webhook_enabled(),
webhook_secret: Default::default(),
}
-15
View File
@@ -62,21 +62,6 @@ 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> {
-23
View File
@@ -38,8 +38,6 @@ pub struct ServerListItemInfo {
pub send_mem_alerts: bool,
/// Whether server is configured to send disk alerts.
pub send_disk_alerts: bool,
/// Whether terminals are disabled for this Server.
pub terminals_disabled: bool,
}
#[typeshare(serialized_as = "Partial<ServerConfig>")]
@@ -79,13 +77,6 @@ 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")]
@@ -249,7 +240,6 @@ 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(),
@@ -278,19 +268,6 @@ pub struct ServerHealth {
pub disks: HashMap<PathBuf, ServerHealthState>,
}
/// Info about an active terminal on a server.
/// Retrieve with [ListTerminals][crate::api::read::server::ListTerminals].
#[typeshare]
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
pub struct TerminalInfo {
/// The name of the terminal.
pub name: String,
/// The root program / args of the pty
pub command: String,
/// The size of the terminal history in memory.
pub stored_size_kb: f64,
}
/// Current pending actions on the server.
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)]
+41 -42
View File
@@ -269,6 +269,47 @@ 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()")]
@@ -332,47 +373,6 @@ 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,7 +434,6 @@ 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,

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