forked from github-starred/komodo
Compare commits
107
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f458106263 | ||
|
|
f022e83414 | ||
|
|
93ccc1ce7f | ||
|
|
d69b7a74e7 | ||
|
|
96cfaf3355 | ||
|
|
b7587e5426 | ||
|
|
ab874211ed | ||
|
|
7094c4e3c5 | ||
|
|
386d463a0a | ||
|
|
65fa969686 | ||
|
|
92099f311f | ||
|
|
9186d5fd50 | ||
|
|
54e766f48b | ||
|
|
a1dd895b19 | ||
|
|
3f767ed42e | ||
|
|
4c14c33339 | ||
|
|
ac43572acb | ||
|
|
c5451bed8e | ||
|
|
8f568ea8d1 | ||
|
|
dc1062bb57 | ||
|
|
a12f21ca7a | ||
|
|
a0e5a86c89 | ||
|
|
2f9cf82cee | ||
|
|
a118696d54 | ||
|
|
d6abda063a | ||
|
|
8fb4bea790 | ||
|
|
8cc1d0106a | ||
|
|
c0b213dfd4 | ||
|
|
5ad00a41ca | ||
|
|
c91e107245 | ||
|
|
66a80c1262 | ||
|
|
dd0d99dabc | ||
|
|
13a0c5a2fa | ||
|
|
177ce9dd45 | ||
|
|
d09a864453 | ||
|
|
29be281f11 | ||
|
|
adb1a49305 | ||
|
|
014d3b2c13 | ||
|
|
42b1f7b6c8 | ||
|
|
6ee707576b | ||
|
|
999ad9a4ce | ||
|
|
94afb432f3 | ||
|
|
48e871d400 | ||
|
|
723853e92d | ||
|
|
24b2a8ab75 | ||
|
|
d541c4c202 | ||
|
|
a09ced896f | ||
|
|
d0890436c3 | ||
|
|
54447fe56b | ||
|
|
625295d50b | ||
|
|
e23e37ac92 | ||
|
|
d4e058a532 | ||
|
|
3890a287d1 | ||
|
|
e169bbbd31 | ||
|
|
d2cb543c76 | ||
|
|
59da3812a9 | ||
|
|
ce10cbe684 | ||
|
|
dd901f7369 | ||
|
|
7908149226 | ||
|
|
0283930207 | ||
|
|
487664a25a | ||
|
|
130ba1f54f | ||
|
|
2758b91e31 | ||
|
|
d240b5c959 | ||
|
|
f8b8f76569 | ||
|
|
d2b27294be | ||
|
|
c44313a9f1 | ||
|
|
1cc967f215 | ||
|
|
53fcd899a4 | ||
|
|
b296971c1a | ||
|
|
6a4c88f8f4 | ||
|
|
51992c477d | ||
|
|
2c98c6ea40 | ||
|
|
14658ba722 | ||
|
|
192073a12c | ||
|
|
275f204a30 | ||
|
|
a196e1ff7f | ||
|
|
3ef5367ed1 | ||
|
|
ca2c728bf3 | ||
|
|
f0d22642b2 | ||
|
|
5d54876fff | ||
|
|
c3c2f57db4 | ||
|
|
30043e32a4 | ||
|
|
33a2897c2c | ||
|
|
2ae1313170 | ||
|
|
b1e38714cf | ||
|
|
43fe613f85 | ||
|
|
6a8f46ee7a | ||
|
|
cbb323fce5 | ||
|
|
4d85c601d3 | ||
|
|
b03a4db3ce | ||
|
|
a19f2afc74 | ||
|
|
b55e5c584b | ||
|
|
4d1a975c79 | ||
|
|
f45fb6f635 | ||
|
|
5c6e09a48c | ||
|
|
6ffb421662 | ||
|
|
85f1cc699c | ||
|
|
3c4e0b23df | ||
|
|
a524c3ac2d | ||
|
|
e89a5bface | ||
|
|
d67130f6ee | ||
|
|
d76736b71e | ||
|
|
c3857e93b6 | ||
|
|
c879c393be | ||
|
|
7bae90b661 | ||
|
|
bef9d9397c |
Generated
+671
-942
File diff suppressed because it is too large
Load Diff
+22
-31
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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(
|
||||
|
||||
@@ -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,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 {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 },
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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
@@ -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");
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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,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,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
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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 +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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:?}"
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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!()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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:?}"
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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,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,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/*
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -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;
|
||||
}))
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
@@ -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\""
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -68,7 +68,6 @@
|
||||
|
||||
pub mod auth;
|
||||
pub mod execute;
|
||||
pub mod terminal;
|
||||
pub mod read;
|
||||
pub mod user;
|
||||
pub mod write;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
),
|
||||
|
||||
@@ -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:";
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user