mirror of
https://github.com/moghtech/komodo.git
synced 2026-11-03 09:03:37 -06:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e0d1befbd | ||
|
|
5dc609b206 | ||
|
|
f1127007c3 | ||
|
|
765e5a0df1 | ||
|
|
76f2f61be5 |
Generated
+307
-189
File diff suppressed because it is too large
Load Diff
+22
-17
@@ -8,7 +8,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.17.2"
|
||||
version = "1.17.5"
|
||||
edition = "2024"
|
||||
authors = ["mbecker20 <becker.maxh@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
@@ -44,19 +44,21 @@ mungos = "3.2.0"
|
||||
svi = "1.0.1"
|
||||
|
||||
# ASYNC
|
||||
reqwest = { version = "0.12.15", default-features = false, features = ["json", "rustls-tls-native-roots"] }
|
||||
tokio = { version = "1.44.1", features = ["full"] }
|
||||
tokio-util = "0.7.14"
|
||||
reqwest = { version = "0.12.15", default-features = false, features = ["json", "stream", "rustls-tls-native-roots"] }
|
||||
tokio = { version = "1.44.2", features = ["full"] }
|
||||
tokio-util = { version = "0.7.15", features = ["io", "codec"] }
|
||||
tokio-stream = { version = "0.1.17", features = ["sync"] }
|
||||
pin-project-lite = "0.2.16"
|
||||
futures = "0.3.31"
|
||||
futures-util = "0.3.31"
|
||||
arc-swap = "1.7.1"
|
||||
|
||||
# SERVER
|
||||
axum-extra = { version = "0.10.0", features = ["typed-header"] }
|
||||
tokio-tungstenite = { version = "0.26.2", features = ["rustls-tls-native-roots"] }
|
||||
axum-extra = { version = "0.10.1", features = ["typed-header"] }
|
||||
tower-http = { version = "0.6.2", features = ["fs", "cors"] }
|
||||
axum-server = { version = "0.7.2", features = ["tls-rustls"] }
|
||||
axum = { version = "0.8.1", features = ["ws", "json", "macros"] }
|
||||
tokio-tungstenite = "0.26.2"
|
||||
axum = { version = "0.8.4", features = ["ws", "json", "macros"] }
|
||||
|
||||
# SER/DE
|
||||
ordered_hash_map = { version = "0.4.0", features = ["serde"] }
|
||||
@@ -64,7 +66,8 @@ serde = { version = "1.0.219", features = ["derive"] }
|
||||
strum = { version = "0.27.1", features = ["derive"] }
|
||||
serde_json = "1.0.140"
|
||||
serde_yaml = "0.9.34"
|
||||
toml = "0.8.20"
|
||||
serde_qs = "0.15.0"
|
||||
toml = "0.8.22"
|
||||
|
||||
# ERROR
|
||||
anyhow = "1.0.98"
|
||||
@@ -76,11 +79,11 @@ opentelemetry_sdk = { version = "0.29.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 = "0.29.1"
|
||||
tracing = "0.1.41"
|
||||
|
||||
# CONFIG
|
||||
clap = { version = "4.5.36", features = ["derive"] }
|
||||
clap = { version = "4.5.37", features = ["derive"] }
|
||||
dotenvy = "0.15.7"
|
||||
envy = "0.4.2"
|
||||
|
||||
@@ -94,23 +97,24 @@ bcrypt = "0.17.0"
|
||||
base64 = "0.22.1"
|
||||
rustls = "0.23.26"
|
||||
hmac = "0.12.1"
|
||||
sha2 = "0.10.8"
|
||||
rand = "0.9.0"
|
||||
sha2 = "0.10.9"
|
||||
rand = "0.9.1"
|
||||
hex = "0.4.3"
|
||||
|
||||
# SYSTEM
|
||||
portable-pty = "0.9.0"
|
||||
bollard = "0.18.1"
|
||||
sysinfo = "0.34.2"
|
||||
sysinfo = "0.35.0"
|
||||
|
||||
# CLOUD
|
||||
aws-config = "1.6.1"
|
||||
aws-sdk-ec2 = "1.121.1"
|
||||
aws-credential-types = "1.2.2"
|
||||
aws-config = "1.6.2"
|
||||
aws-sdk-ec2 = "1.124.0"
|
||||
aws-credential-types = "1.2.3"
|
||||
|
||||
## CRON
|
||||
english-to-cron = "0.1.4"
|
||||
chrono-tz = "0.10.3"
|
||||
chrono = "0.4.40"
|
||||
chrono = "0.4.41"
|
||||
croner = "2.1.0"
|
||||
|
||||
# MISC
|
||||
@@ -121,4 +125,5 @@ dashmap = "6.1.0"
|
||||
wildcard = "0.3.0"
|
||||
colored = "3.0.0"
|
||||
regex = "1.11.1"
|
||||
bytes = "1.10.1"
|
||||
bson = "2.14.0"
|
||||
|
||||
@@ -185,6 +185,9 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
|
||||
Execution::PullStack(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::BatchPullStack(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::StartStack(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
@@ -429,6 +432,10 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
Execution::BatchPullStack(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Batch),
|
||||
Execution::StartStack(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
|
||||
@@ -38,6 +38,7 @@ 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
|
||||
|
||||
@@ -24,10 +24,9 @@ RUN cd frontend && yarn link komodo_client && yarn && yarn build
|
||||
# Final Image
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
# Install Deps
|
||||
RUN apt update && \
|
||||
apt install -y git ca-certificates && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
COPY ./bin/core/starship.toml /config/starship.toml
|
||||
COPY ./bin/core/debian-deps.sh .
|
||||
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
|
||||
|
||||
# Setup an application directory
|
||||
WORKDIR /app
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/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,10 +15,9 @@ FROM ${FRONTEND_IMAGE} AS frontend
|
||||
# Final Image
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
# Install Deps
|
||||
RUN apt update && \
|
||||
apt install -y git ca-certificates && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
COPY ./bin/core/starship.toml /config/starship.toml
|
||||
COPY ./bin/core/debian-deps.sh .
|
||||
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -16,10 +16,9 @@ RUN cd frontend && yarn link komodo_client && yarn && yarn build
|
||||
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
# Install Deps
|
||||
RUN apt update && \
|
||||
apt install -y git ca-certificates && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
COPY ./bin/core/starship.toml /config/starship.toml
|
||||
COPY ./bin/core/debian-deps.sh .
|
||||
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
|
||||
|
||||
# Copy
|
||||
COPY ./config/core.config.toml /config/config.toml
|
||||
|
||||
@@ -260,9 +260,6 @@ fn resource_link(
|
||||
ResourceTargetVariant::Action => {
|
||||
format!("/actions/{id}")
|
||||
}
|
||||
ResourceTargetVariant::ServerTemplate => {
|
||||
format!("/server-templates/{id}")
|
||||
}
|
||||
ResourceTargetVariant::ResourceSync => {
|
||||
format!("/resource-syncs/{id}")
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use std::{sync::OnceLock, time::Instant};
|
||||
|
||||
use axum::{Router, http::HeaderMap, routing::post};
|
||||
use axum::{Router, extract::Path, http::HeaderMap, routing::post};
|
||||
use derive_variants::{EnumVariants, ExtractVariant};
|
||||
use komodo_client::{api::auth::*, entities::user::User};
|
||||
use resolver_api::Resolve;
|
||||
use response::Response;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use serror::Json;
|
||||
use typeshare::typeshare;
|
||||
use uuid::Uuid;
|
||||
@@ -22,6 +23,8 @@ use crate::{
|
||||
state::jwt_client,
|
||||
};
|
||||
|
||||
use super::Variant;
|
||||
|
||||
pub struct AuthArgs {
|
||||
pub headers: HeaderMap,
|
||||
}
|
||||
@@ -45,7 +48,9 @@ pub enum AuthRequest {
|
||||
}
|
||||
|
||||
pub fn router() -> Router {
|
||||
let mut router = Router::new().route("/", post(handler));
|
||||
let mut router = Router::new()
|
||||
.route("/", post(handler))
|
||||
.route("/{variant}", post(variant_handler));
|
||||
|
||||
if core_config().local_auth {
|
||||
info!("🔑 Local Login Enabled");
|
||||
@@ -69,6 +74,18 @@ pub fn router() -> Router {
|
||||
router
|
||||
}
|
||||
|
||||
async fn variant_handler(
|
||||
headers: HeaderMap,
|
||||
Path(Variant { variant }): Path<Variant>,
|
||||
Json(params): Json<serde_json::Value>,
|
||||
) -> serror::Result<axum::response::Response> {
|
||||
let req: AuthRequest = serde_json::from_value(json!({
|
||||
"type": variant,
|
||||
"params": params,
|
||||
}))?;
|
||||
handler(headers, Json(req)).await
|
||||
}
|
||||
|
||||
#[instrument(name = "AuthHandler", level = "debug", skip(headers))]
|
||||
async fn handler(
|
||||
headers: HeaderMap,
|
||||
|
||||
@@ -134,12 +134,18 @@ impl Resolve<ExecuteArgs> for RunAction {
|
||||
""
|
||||
};
|
||||
|
||||
let reload = if action.config.reload_deno_deps {
|
||||
" --reload"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let mut res = run_komodo_command(
|
||||
// Keep this stage name as is, the UI will find the latest update log by matching the stage name
|
||||
"Execute Action",
|
||||
None,
|
||||
format!(
|
||||
"deno run --allow-all{https_cert_flag} {}",
|
||||
"deno run --allow-all{https_cert_flag}{reload} {}",
|
||||
path.display()
|
||||
),
|
||||
)
|
||||
@@ -245,7 +251,7 @@ fn full_contents(contents: &str, key: &str, secret: &str) -> String {
|
||||
let protocol = if *ssl_enabled { "https" } else { "http" };
|
||||
let base_url = format!("{protocol}://localhost:{port}");
|
||||
format!(
|
||||
"import {{ KomodoClient }} from '{base_url}/client/lib.js';
|
||||
"import {{ KomodoClient, Types }} from '{base_url}/client/lib.js';
|
||||
import * as __YAML__ from 'jsr:@std/yaml';
|
||||
import * as __TOML__ from 'jsr:@std/toml';
|
||||
|
||||
@@ -281,7 +287,7 @@ main()
|
||||
console.error('Status:', error.status);
|
||||
console.error(JSON.stringify(error.result, null, 2));
|
||||
}} else {{
|
||||
console.error(JSON.stringify(error, null, 2));
|
||||
console.error(error);
|
||||
}}
|
||||
Deno.exit(1)
|
||||
}});"
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use std::{pin::Pin, time::Instant};
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::{Extension, Router, middleware, routing::post};
|
||||
use axum::{
|
||||
Extension, Router, extract::Path, middleware, routing::post,
|
||||
};
|
||||
use axum_extra::{TypedHeader, headers::ContentType};
|
||||
use derive_variants::{EnumVariants, ExtractVariant};
|
||||
use formatting::format_serror;
|
||||
@@ -18,6 +20,7 @@ use mungos::by_id::find_one_by_id;
|
||||
use resolver_api::Resolve;
|
||||
use response::JsonString;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use serror::Json;
|
||||
use typeshare::typeshare;
|
||||
use uuid::Uuid;
|
||||
@@ -36,10 +39,11 @@ mod deployment;
|
||||
mod procedure;
|
||||
mod repo;
|
||||
mod server;
|
||||
mod server_template;
|
||||
mod stack;
|
||||
mod sync;
|
||||
|
||||
use super::Variant;
|
||||
|
||||
pub use {
|
||||
deployment::pull_deployment_inner, stack::pull_stack_inner,
|
||||
};
|
||||
@@ -100,6 +104,7 @@ pub enum ExecuteRequest {
|
||||
DeployStackIfChanged(DeployStackIfChanged),
|
||||
BatchDeployStackIfChanged(BatchDeployStackIfChanged),
|
||||
PullStack(PullStack),
|
||||
BatchPullStack(BatchPullStack),
|
||||
StartStack(StartStack),
|
||||
RestartStack(RestartStack),
|
||||
StopStack(StopStack),
|
||||
@@ -130,9 +135,6 @@ pub enum ExecuteRequest {
|
||||
RunAction(RunAction),
|
||||
BatchRunAction(BatchRunAction),
|
||||
|
||||
// ==== SERVER TEMPLATE ====
|
||||
LaunchServer(LaunchServer),
|
||||
|
||||
// ==== ALERTER ====
|
||||
TestAlerter(TestAlerter),
|
||||
|
||||
@@ -143,9 +145,22 @@ pub enum ExecuteRequest {
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/", post(handler))
|
||||
.route("/{variant}", post(variant_handler))
|
||||
.layer(middleware::from_fn(auth_request))
|
||||
}
|
||||
|
||||
async fn variant_handler(
|
||||
user: Extension<User>,
|
||||
Path(Variant { variant }): Path<Variant>,
|
||||
Json(params): Json<serde_json::Value>,
|
||||
) -> serror::Result<(TypedHeader<ContentType>, String)> {
|
||||
let req: ExecuteRequest = serde_json::from_value(json!({
|
||||
"type": variant,
|
||||
"params": params,
|
||||
}))?;
|
||||
handler(user, Json(req)).await
|
||||
}
|
||||
|
||||
async fn handler(
|
||||
Extension(user): Extension<User>,
|
||||
Json(request): Json<ExecuteRequest>,
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
use anyhow::{Context, anyhow};
|
||||
use formatting::format_serror;
|
||||
use komodo_client::{
|
||||
api::{execute::LaunchServer, write::CreateServer},
|
||||
entities::{
|
||||
permission::PermissionLevel,
|
||||
server::PartialServerConfig,
|
||||
server_template::{ServerTemplate, ServerTemplateConfig},
|
||||
update::Update,
|
||||
},
|
||||
};
|
||||
use mungos::mongodb::bson::doc;
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
api::write::WriteArgs,
|
||||
cloud::{
|
||||
aws::ec2::launch_ec2_instance, hetzner::launch_hetzner_server,
|
||||
},
|
||||
helpers::update::update_update,
|
||||
resource,
|
||||
state::db_client,
|
||||
};
|
||||
|
||||
use super::ExecuteArgs;
|
||||
|
||||
impl Resolve<ExecuteArgs> for LaunchServer {
|
||||
#[instrument(name = "LaunchServer", skip(user, update), fields(user_id = user.id, update_id = update.id))]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
// validate name isn't already taken by another server
|
||||
if db_client()
|
||||
.servers
|
||||
.find_one(doc! {
|
||||
"name": &self.name
|
||||
})
|
||||
.await
|
||||
.context("failed to query db for servers")?
|
||||
.is_some()
|
||||
{
|
||||
return Err(anyhow!("name is already taken").into());
|
||||
}
|
||||
|
||||
let template = resource::get_check_permissions::<ServerTemplate>(
|
||||
&self.server_template,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
update.push_simple_log(
|
||||
"launching server",
|
||||
format!("{:#?}", template.config),
|
||||
);
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let config = match template.config {
|
||||
ServerTemplateConfig::Aws(config) => {
|
||||
let region = config.region.clone();
|
||||
let use_https = config.use_https;
|
||||
let port = config.port;
|
||||
let instance =
|
||||
match launch_ec2_instance(&self.name, config).await {
|
||||
Ok(instance) => instance,
|
||||
Err(e) => {
|
||||
update.push_error_log(
|
||||
"launch server",
|
||||
format!("failed to launch aws instance\n\n{e:#?}"),
|
||||
);
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
return Ok(update);
|
||||
}
|
||||
};
|
||||
update.push_simple_log(
|
||||
"launch server",
|
||||
format!(
|
||||
"successfully launched server {} on ip {}",
|
||||
self.name, instance.ip
|
||||
),
|
||||
);
|
||||
let protocol = if use_https { "https" } else { "http" };
|
||||
PartialServerConfig {
|
||||
address: format!("{protocol}://{}:{port}", instance.ip)
|
||||
.into(),
|
||||
region: region.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
ServerTemplateConfig::Hetzner(config) => {
|
||||
let datacenter = config.datacenter;
|
||||
let use_https = config.use_https;
|
||||
let port = config.port;
|
||||
let server =
|
||||
match launch_hetzner_server(&self.name, config).await {
|
||||
Ok(server) => server,
|
||||
Err(e) => {
|
||||
update.push_error_log(
|
||||
"launch server",
|
||||
format!("failed to launch hetzner server\n\n{e:#?}"),
|
||||
);
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
return Ok(update);
|
||||
}
|
||||
};
|
||||
update.push_simple_log(
|
||||
"launch server",
|
||||
format!(
|
||||
"successfully launched server {} on ip {}",
|
||||
self.name, server.ip
|
||||
),
|
||||
);
|
||||
let protocol = if use_https { "https" } else { "http" };
|
||||
PartialServerConfig {
|
||||
address: format!("{protocol}://{}:{port}", server.ip)
|
||||
.into(),
|
||||
region: datacenter.as_ref().to_string().into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match (CreateServer {
|
||||
name: self.name,
|
||||
config,
|
||||
})
|
||||
.resolve(&WriteArgs { user: user.clone() })
|
||||
.await
|
||||
{
|
||||
Ok(server) => {
|
||||
update.push_simple_log(
|
||||
"create server",
|
||||
format!("created server {} ({})", server.name, server.id),
|
||||
);
|
||||
update.other_data = server.id;
|
||||
}
|
||||
Err(e) => {
|
||||
update.push_error_log(
|
||||
"create server",
|
||||
format_serror(
|
||||
&e.error.context("failed to create server").into(),
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
@@ -385,6 +385,32 @@ impl Resolve<ExecuteArgs> for DeployStackIfChanged {
|
||||
}
|
||||
}
|
||||
|
||||
impl super::BatchExecute for BatchPullStack {
|
||||
type Resource = Stack;
|
||||
fn single_request(stack: String) -> ExecuteRequest {
|
||||
ExecuteRequest::PullStack(PullStack {
|
||||
stack,
|
||||
services: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for BatchPullStack {
|
||||
#[instrument(name = "BatchPullStack", skip(user), fields(user_id = user.id))]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, .. }: &ExecuteArgs,
|
||||
) -> serror::Result<BatchExecutionResponse> {
|
||||
Ok(
|
||||
super::batch_execute::<BatchPullStack>(
|
||||
&self.pattern,
|
||||
user,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn pull_stack_inner(
|
||||
mut stack: Stack,
|
||||
services: Vec<String>,
|
||||
|
||||
@@ -16,7 +16,6 @@ use komodo_client::{
|
||||
procedure::Procedure,
|
||||
repo::Repo,
|
||||
server::Server,
|
||||
server_template::ServerTemplate,
|
||||
stack::Stack,
|
||||
sync::ResourceSync,
|
||||
update::{Log, Update},
|
||||
@@ -142,10 +141,6 @@ impl Resolve<ExecuteArgs> for RunSync {
|
||||
.servers
|
||||
.get(&name_or_id)
|
||||
.map(|s| s.name.clone()),
|
||||
ResourceTargetVariant::ServerTemplate => all_resources
|
||||
.templates
|
||||
.get(&name_or_id)
|
||||
.map(|t| t.name.clone()),
|
||||
ResourceTargetVariant::Stack => all_resources
|
||||
.stacks
|
||||
.get(&name_or_id)
|
||||
@@ -332,20 +327,6 @@ impl Resolve<ExecuteArgs> for RunSync {
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let server_template_deltas = if sync.config.include_resources {
|
||||
get_updates_for_execution::<ServerTemplate>(
|
||||
resources.server_templates,
|
||||
delete,
|
||||
&all_resources,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let resource_sync_deltas = if sync.config.include_resources {
|
||||
get_updates_for_execution::<entities::sync::ResourceSync>(
|
||||
resources.resource_syncs,
|
||||
@@ -397,7 +378,6 @@ impl Resolve<ExecuteArgs> for RunSync {
|
||||
|
||||
if deploy_cache.is_empty()
|
||||
&& resource_sync_deltas.no_changes()
|
||||
&& server_template_deltas.no_changes()
|
||||
&& server_deltas.no_changes()
|
||||
&& deployment_deltas.no_changes()
|
||||
&& stack_deltas.no_changes()
|
||||
@@ -451,11 +431,6 @@ impl Resolve<ExecuteArgs> for RunSync {
|
||||
&mut update.logs,
|
||||
ResourceSync::execute_sync_updates(resource_sync_deltas).await,
|
||||
);
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
ServerTemplate::execute_sync_updates(server_template_deltas)
|
||||
.await,
|
||||
);
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
Server::execute_sync_updates(server_deltas).await,
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
pub mod auth;
|
||||
pub mod execute;
|
||||
pub mod read;
|
||||
pub mod terminal;
|
||||
pub mod user;
|
||||
pub mod write;
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Variant {
|
||||
variant: String,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use std::{collections::HashSet, sync::OnceLock, time::Instant};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use axum::{Extension, Router, middleware, routing::post};
|
||||
use axum::{
|
||||
Extension, Router, extract::Path, middleware, routing::post,
|
||||
};
|
||||
use komodo_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
@@ -18,6 +20,7 @@ use komodo_client::{
|
||||
use resolver_api::Resolve;
|
||||
use response::Response;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use serror::Json;
|
||||
use typeshare::typeshare;
|
||||
use uuid::Uuid;
|
||||
@@ -27,6 +30,8 @@ use crate::{
|
||||
resource,
|
||||
};
|
||||
|
||||
use super::Variant;
|
||||
|
||||
mod action;
|
||||
mod alert;
|
||||
mod alerter;
|
||||
@@ -38,7 +43,6 @@ mod procedure;
|
||||
mod provider;
|
||||
mod repo;
|
||||
mod server;
|
||||
mod server_template;
|
||||
mod stack;
|
||||
mod sync;
|
||||
mod tag;
|
||||
@@ -93,12 +97,6 @@ enum ReadRequest {
|
||||
ListActions(ListActions),
|
||||
ListFullActions(ListFullActions),
|
||||
|
||||
// ==== SERVER TEMPLATE ====
|
||||
GetServerTemplate(GetServerTemplate),
|
||||
GetServerTemplatesSummary(GetServerTemplatesSummary),
|
||||
ListServerTemplates(ListServerTemplates),
|
||||
ListFullServerTemplates(ListFullServerTemplates),
|
||||
|
||||
// ==== SERVER ====
|
||||
GetServersSummary(GetServersSummary),
|
||||
GetServer(GetServer),
|
||||
@@ -123,6 +121,7 @@ enum ReadRequest {
|
||||
ListDockerImages(ListDockerImages),
|
||||
ListDockerVolumes(ListDockerVolumes),
|
||||
ListComposeProjects(ListComposeProjects),
|
||||
ListTerminals(ListTerminals),
|
||||
|
||||
// ==== DEPLOYMENT ====
|
||||
GetDeploymentsSummary(GetDeploymentsSummary),
|
||||
@@ -223,9 +222,22 @@ enum ReadRequest {
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/", post(handler))
|
||||
.route("/{variant}", post(variant_handler))
|
||||
.layer(middleware::from_fn(auth_request))
|
||||
}
|
||||
|
||||
async fn variant_handler(
|
||||
user: Extension<User>,
|
||||
Path(Variant { variant }): Path<Variant>,
|
||||
Json(params): Json<serde_json::Value>,
|
||||
) -> serror::Result<axum::response::Response> {
|
||||
let req: ReadRequest = serde_json::from_value(json!({
|
||||
"type": variant,
|
||||
"params": params,
|
||||
}))?;
|
||||
handler(user, Json(req)).await
|
||||
}
|
||||
|
||||
#[instrument(name = "ReadHandler", level = "debug", skip(user), fields(user_id = user.id))]
|
||||
async fn handler(
|
||||
Extension(user): Extension<User>,
|
||||
@@ -270,6 +282,7 @@ fn core_info() -> &'static GetCoreInfoResponse {
|
||||
ui_write_disabled: config.ui_write_disabled,
|
||||
disable_confirm_dialog: config.disable_confirm_dialog,
|
||||
disable_non_admin_create: config.disable_non_admin_create,
|
||||
disable_websocket_reconnect: config.disable_websocket_reconnect,
|
||||
github_webhook_owners: config
|
||||
.github_webhook_app
|
||||
.installations
|
||||
|
||||
@@ -21,9 +21,11 @@ use komodo_client::{
|
||||
network::Network,
|
||||
volume::Volume,
|
||||
},
|
||||
komodo_timestamp,
|
||||
permission::PermissionLevel,
|
||||
server::{
|
||||
Server, ServerActionState, ServerListItem, ServerState,
|
||||
TerminalInfo,
|
||||
},
|
||||
stack::{Stack, StackServiceNames},
|
||||
stats::{SystemInformation, SystemProcess},
|
||||
@@ -45,7 +47,10 @@ use resolver_api::Resolve;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
helpers::{periphery_client, query::get_all_tags},
|
||||
helpers::{
|
||||
periphery_client,
|
||||
query::{get_all_tags, get_system_info},
|
||||
},
|
||||
resource,
|
||||
stack::compose_container_match_regex,
|
||||
state::{action_states, db_client, server_status_cache},
|
||||
@@ -198,16 +203,6 @@ 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,
|
||||
@@ -219,25 +214,7 @@ impl Resolve<ReadArgs> for GetSystemInformation {
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
|
||||
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)
|
||||
get_system_info(&server).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -812,3 +789,66 @@ 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
use anyhow::Context;
|
||||
use komodo_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
permission::PermissionLevel, server_template::ServerTemplate,
|
||||
},
|
||||
};
|
||||
use mongo_indexed::Document;
|
||||
use mungos::mongodb::bson::doc;
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_all_tags, resource, state::db_client,
|
||||
};
|
||||
|
||||
use super::ReadArgs;
|
||||
|
||||
impl Resolve<ReadArgs> for GetServerTemplate {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetServerTemplateResponse> {
|
||||
Ok(
|
||||
resource::get_check_permissions::<ServerTemplate>(
|
||||
&self.server_template,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListServerTemplates {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListServerTemplatesResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
Ok(
|
||||
resource::list_for_user::<ServerTemplate>(
|
||||
self.query, user, &all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListFullServerTemplates {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListFullServerTemplatesResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
Ok(
|
||||
resource::list_full_for_user::<ServerTemplate>(
|
||||
self.query, user, &all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetServerTemplatesSummary {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetServerTemplatesSummaryResponse> {
|
||||
let query = match resource::get_resource_object_ids_for_user::<
|
||||
ServerTemplate,
|
||||
>(user)
|
||||
.await?
|
||||
{
|
||||
Some(ids) => doc! {
|
||||
"_id": { "$in": ids }
|
||||
},
|
||||
None => Document::new(),
|
||||
};
|
||||
let total = db_client()
|
||||
.server_templates
|
||||
.count_documents(query)
|
||||
.await
|
||||
.context("failed to count all server template documents")?;
|
||||
let res = GetServerTemplatesSummaryResponse {
|
||||
total: total as u32,
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,7 @@ use komodo_client::{
|
||||
ResourceTarget, action::Action, alerter::Alerter, build::Build,
|
||||
builder::Builder, deployment::Deployment,
|
||||
permission::PermissionLevel, procedure::Procedure, repo::Repo,
|
||||
resource::ResourceQuery, server::Server,
|
||||
server_template::ServerTemplate, stack::Stack,
|
||||
resource::ResourceQuery, server::Server, stack::Stack,
|
||||
sync::ResourceSync, toml::ResourcesToml, user::User,
|
||||
},
|
||||
};
|
||||
@@ -132,16 +131,6 @@ async fn get_all_targets(
|
||||
.into_iter()
|
||||
.map(|resource| ResourceTarget::Action(resource.id)),
|
||||
);
|
||||
targets.extend(
|
||||
resource::list_for_user::<ServerTemplate>(
|
||||
ResourceQuery::builder().tags(tags).build(),
|
||||
user,
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|resource| ResourceTarget::ServerTemplate(resource.id)),
|
||||
);
|
||||
targets.extend(
|
||||
resource::list_full_for_user::<ResourceSync>(
|
||||
ResourceQuery::builder().tags(tags).build(),
|
||||
@@ -241,20 +230,6 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
|
||||
))
|
||||
}
|
||||
}
|
||||
ResourceTarget::ServerTemplate(id) => {
|
||||
let template = resource::get_check_permissions::<
|
||||
ServerTemplate,
|
||||
>(&id, user, PermissionLevel::Read)
|
||||
.await?;
|
||||
res.server_templates.push(
|
||||
convert_resource::<ServerTemplate>(
|
||||
template,
|
||||
false,
|
||||
vec![],
|
||||
&id_to_tags,
|
||||
),
|
||||
)
|
||||
}
|
||||
ResourceTarget::Server(id) => {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
&id,
|
||||
@@ -503,14 +478,6 @@ fn serialize_resources_toml(
|
||||
Builder::push_to_toml_string(builder, &mut toml)?;
|
||||
}
|
||||
|
||||
for server_template in resources.server_templates {
|
||||
if !toml.is_empty() {
|
||||
toml.push_str("\n\n##\n\n");
|
||||
}
|
||||
toml.push_str("[[server_template]]\n");
|
||||
ServerTemplate::push_to_toml_string(server_template, &mut toml)?;
|
||||
}
|
||||
|
||||
for resource_sync in resources.resource_syncs {
|
||||
if !toml.is_empty() {
|
||||
toml.push_str("\n\n##\n\n");
|
||||
|
||||
@@ -14,7 +14,6 @@ use komodo_client::{
|
||||
procedure::Procedure,
|
||||
repo::Repo,
|
||||
server::Server,
|
||||
server_template::ServerTemplate,
|
||||
stack::Stack,
|
||||
sync::ResourceSync,
|
||||
update::{Update, UpdateListItem},
|
||||
@@ -132,16 +131,6 @@ impl Resolve<ReadArgs> for ListUpdates {
|
||||
})
|
||||
.unwrap_or_else(|| doc! { "target.type": "Alerter" });
|
||||
|
||||
let server_template_query =
|
||||
resource::get_resource_ids_for_user::<ServerTemplate>(user)
|
||||
.await?
|
||||
.map(|ids| {
|
||||
doc! {
|
||||
"target.type": "ServerTemplate", "target.id": { "$in": ids }
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| doc! { "target.type": "ServerTemplate" });
|
||||
|
||||
let resource_sync_query =
|
||||
resource::get_resource_ids_for_user::<ResourceSync>(
|
||||
user,
|
||||
@@ -166,7 +155,6 @@ impl Resolve<ReadArgs> for ListUpdates {
|
||||
action_query,
|
||||
alerter_query,
|
||||
builder_query,
|
||||
server_template_query,
|
||||
resource_sync_query,
|
||||
]
|
||||
});
|
||||
@@ -308,14 +296,6 @@ impl Resolve<ReadArgs> for GetUpdate {
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::ServerTemplate(id) => {
|
||||
resource::get_check_permissions::<ServerTemplate>(
|
||||
id,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::ResourceSync(id) => {
|
||||
resource::get_check_permissions::<ResourceSync>(
|
||||
id,
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
use anyhow::Context;
|
||||
use axum::{Extension, Router, middleware, routing::post};
|
||||
use komodo_client::{
|
||||
api::terminal::ExecuteTerminalBody,
|
||||
entities::{
|
||||
permission::PermissionLevel, server::Server, user::User,
|
||||
},
|
||||
};
|
||||
use serror::Json;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
auth::auth_request, helpers::periphery_client, resource,
|
||||
};
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/execute", post(execute))
|
||||
.layer(middleware::from_fn(auth_request))
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
Extension(user): Extension<User>,
|
||||
Json(request): Json<ExecuteTerminalBody>,
|
||||
) -> serror::Result<axum::body::Body> {
|
||||
execute_inner(Uuid::new_v4(), request, user).await
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
name = "ExecuteTerminal",
|
||||
skip(user),
|
||||
fields(
|
||||
user_id = user.id,
|
||||
)
|
||||
)]
|
||||
async fn execute_inner(
|
||||
req_id: Uuid,
|
||||
ExecuteTerminalBody {
|
||||
server,
|
||||
terminal,
|
||||
command,
|
||||
}: ExecuteTerminalBody,
|
||||
user: User,
|
||||
) -> serror::Result<axum::body::Body> {
|
||||
info!("/terminal request | user: {}", user.username);
|
||||
|
||||
let res = async {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
&server,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
let stream = periphery
|
||||
.execute_terminal(terminal, command)
|
||||
.await
|
||||
.context("Failed to execute command on periphery")?;
|
||||
|
||||
anyhow::Ok(stream)
|
||||
}
|
||||
.await;
|
||||
|
||||
let stream = match res {
|
||||
Ok(stream) => stream,
|
||||
Err(e) => {
|
||||
warn!("/terminal request {req_id} error: {e:#}");
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
|
||||
Ok(axum::body::Body::from_stream(stream.into_line_stream()))
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
use std::{collections::VecDeque, time::Instant};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use axum::{Extension, Json, Router, middleware, routing::post};
|
||||
use axum::{
|
||||
Extension, Json, Router, extract::Path, middleware, routing::post,
|
||||
};
|
||||
use derive_variants::EnumVariants;
|
||||
use komodo_client::{
|
||||
api::user::*,
|
||||
@@ -12,6 +14,7 @@ use mungos::{by_id::update_one_by_id, mongodb::bson::to_bson};
|
||||
use resolver_api::Resolve;
|
||||
use response::Response;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use typeshare::typeshare;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -21,6 +24,8 @@ use crate::{
|
||||
state::db_client,
|
||||
};
|
||||
|
||||
use super::Variant;
|
||||
|
||||
pub struct UserArgs {
|
||||
pub user: User,
|
||||
}
|
||||
@@ -43,9 +48,22 @@ enum UserRequest {
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/", post(handler))
|
||||
.route("/{variant}", post(variant_handler))
|
||||
.layer(middleware::from_fn(auth_request))
|
||||
}
|
||||
|
||||
async fn variant_handler(
|
||||
user: Extension<User>,
|
||||
Path(Variant { variant }): Path<Variant>,
|
||||
Json(params): Json<serde_json::Value>,
|
||||
) -> serror::Result<axum::response::Response> {
|
||||
let req: UserRequest = serde_json::from_value(json!({
|
||||
"type": variant,
|
||||
"params": params,
|
||||
}))?;
|
||||
handler(user, Json(req)).await
|
||||
}
|
||||
|
||||
#[instrument(name = "UserHandler", level = "debug", skip(user))]
|
||||
async fn handler(
|
||||
Extension(user): Extension<User>,
|
||||
|
||||
@@ -4,8 +4,7 @@ use komodo_client::{
|
||||
entities::{
|
||||
ResourceTarget, action::Action, alerter::Alerter, build::Build,
|
||||
builder::Builder, deployment::Deployment, procedure::Procedure,
|
||||
repo::Repo, server::Server, server_template::ServerTemplate,
|
||||
stack::Stack, sync::ResourceSync,
|
||||
repo::Repo, server::Server, stack::Stack, sync::ResourceSync,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
@@ -93,14 +92,6 @@ impl Resolve<WriteArgs> for UpdateDescription {
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::ServerTemplate(id) => {
|
||||
resource::update_description::<ServerTemplate>(
|
||||
&id,
|
||||
&self.description,
|
||||
user,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::ResourceSync(id) => {
|
||||
resource::update_description::<ResourceSync>(
|
||||
&id,
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::{Extension, Router, middleware, routing::post};
|
||||
use axum::{
|
||||
Extension, Router, extract::Path, middleware, routing::post,
|
||||
};
|
||||
use derive_variants::{EnumVariants, ExtractVariant};
|
||||
use komodo_client::{api::write::*, entities::user::User};
|
||||
use resolver_api::Resolve;
|
||||
use response::Response;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use serror::Json;
|
||||
use typeshare::typeshare;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::auth::auth_request;
|
||||
|
||||
use super::Variant;
|
||||
|
||||
mod action;
|
||||
mod alerter;
|
||||
mod build;
|
||||
@@ -24,7 +29,6 @@ mod procedure;
|
||||
mod provider;
|
||||
mod repo;
|
||||
mod server;
|
||||
mod server_template;
|
||||
mod service_user;
|
||||
mod stack;
|
||||
mod sync;
|
||||
@@ -81,6 +85,9 @@ pub enum WriteRequest {
|
||||
UpdateServer(UpdateServer),
|
||||
RenameServer(RenameServer),
|
||||
CreateNetwork(CreateNetwork),
|
||||
CreateTerminal(CreateTerminal),
|
||||
DeleteTerminal(DeleteTerminal),
|
||||
DeleteAllTerminals(DeleteAllTerminals),
|
||||
|
||||
// ==== DEPLOYMENT ====
|
||||
CreateDeployment(CreateDeployment),
|
||||
@@ -108,13 +115,6 @@ pub enum WriteRequest {
|
||||
UpdateBuilder(UpdateBuilder),
|
||||
RenameBuilder(RenameBuilder),
|
||||
|
||||
// ==== SERVER TEMPLATE ====
|
||||
CreateServerTemplate(CreateServerTemplate),
|
||||
CopyServerTemplate(CopyServerTemplate),
|
||||
DeleteServerTemplate(DeleteServerTemplate),
|
||||
UpdateServerTemplate(UpdateServerTemplate),
|
||||
RenameServerTemplate(RenameServerTemplate),
|
||||
|
||||
// ==== REPO ====
|
||||
CreateRepo(CreateRepo),
|
||||
CopyRepo(CopyRepo),
|
||||
@@ -195,9 +195,22 @@ pub enum WriteRequest {
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/", post(handler))
|
||||
.route("/{variant}", post(variant_handler))
|
||||
.layer(middleware::from_fn(auth_request))
|
||||
}
|
||||
|
||||
async fn variant_handler(
|
||||
user: Extension<User>,
|
||||
Path(Variant { variant }): Path<Variant>,
|
||||
Json(params): Json<serde_json::Value>,
|
||||
) -> serror::Result<axum::response::Response> {
|
||||
let req: WriteRequest = serde_json::from_value(json!({
|
||||
"type": variant,
|
||||
"params": params,
|
||||
}))?;
|
||||
handler(user, Json(req)).await
|
||||
}
|
||||
|
||||
async fn handler(
|
||||
Extension(user): Extension<User>,
|
||||
Json(request): Json<WriteRequest>,
|
||||
@@ -208,10 +221,6 @@ async fn handler(
|
||||
.await
|
||||
.context("failure in spawned task");
|
||||
|
||||
if let Err(e) = &res {
|
||||
warn!("/write request {req_id} spawn error: {e:#}");
|
||||
}
|
||||
|
||||
res?
|
||||
}
|
||||
|
||||
|
||||
@@ -406,20 +406,6 @@ async fn extract_resource_target_with_validation(
|
||||
.id;
|
||||
Ok((ResourceTargetVariant::Action, id))
|
||||
}
|
||||
ResourceTarget::ServerTemplate(ident) => {
|
||||
let filter = match ObjectId::from_str(ident) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
Err(_) => doc! { "name": ident },
|
||||
};
|
||||
let id = db_client()
|
||||
.server_templates
|
||||
.find_one(filter)
|
||||
.await
|
||||
.context("failed to query db for server templates")?
|
||||
.context("no matching server template found")?
|
||||
.id;
|
||||
Ok((ResourceTargetVariant::ServerTemplate, id))
|
||||
}
|
||||
ResourceTarget::ResourceSync(ident) => {
|
||||
let filter = match ObjectId::from_str(ident) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use anyhow::Context;
|
||||
use formatting::format_serror;
|
||||
use komodo_client::{
|
||||
api::write::*,
|
||||
entities::{
|
||||
Operation,
|
||||
NoData, Operation,
|
||||
permission::PermissionLevel,
|
||||
server::Server,
|
||||
update::{Update, UpdateStatus},
|
||||
@@ -101,3 +102,81 @@ impl Resolve<WriteArgs> for CreateNetwork {
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for CreateTerminal {
|
||||
#[instrument(name = "CreateTerminal", skip(user))]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<NoData> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
periphery
|
||||
.request(api::terminal::CreateTerminal {
|
||||
name: self.name,
|
||||
command: self.command,
|
||||
recreate: self.recreate,
|
||||
})
|
||||
.await
|
||||
.context("Failed to create terminal on periphery")?;
|
||||
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for DeleteTerminal {
|
||||
#[instrument(name = "DeleteTerminal", skip(user))]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<NoData> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
periphery
|
||||
.request(api::terminal::DeleteTerminal {
|
||||
terminal: self.terminal,
|
||||
})
|
||||
.await
|
||||
.context("Failed to delete terminal on periphery")?;
|
||||
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for DeleteAllTerminals {
|
||||
#[instrument(name = "DeleteAllTerminals", skip(user))]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<NoData> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
periphery
|
||||
.request(api::terminal::DeleteAllTerminals {})
|
||||
.await
|
||||
.context("Failed to delete all terminals on periphery")?;
|
||||
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
use komodo_client::{
|
||||
api::write::{
|
||||
CopyServerTemplate, CreateServerTemplate, DeleteServerTemplate,
|
||||
RenameServerTemplate, UpdateServerTemplate,
|
||||
},
|
||||
entities::{
|
||||
permission::PermissionLevel, server_template::ServerTemplate,
|
||||
update::Update,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::resource;
|
||||
|
||||
use super::WriteArgs;
|
||||
|
||||
impl Resolve<WriteArgs> for CreateServerTemplate {
|
||||
#[instrument(name = "CreateServerTemplate", skip(user))]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<ServerTemplate> {
|
||||
Ok(
|
||||
resource::create::<ServerTemplate>(
|
||||
&self.name,
|
||||
self.config,
|
||||
user,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for CopyServerTemplate {
|
||||
#[instrument(name = "CopyServerTemplate", skip(user))]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<ServerTemplate> {
|
||||
let ServerTemplate { config, .. } =
|
||||
resource::get_check_permissions::<ServerTemplate>(
|
||||
&self.id,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
Ok(
|
||||
resource::create::<ServerTemplate>(
|
||||
&self.name,
|
||||
config.into(),
|
||||
user,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for DeleteServerTemplate {
|
||||
#[instrument(name = "DeleteServerTemplate", skip(args))]
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &WriteArgs,
|
||||
) -> serror::Result<ServerTemplate> {
|
||||
Ok(resource::delete::<ServerTemplate>(&self.id, args).await?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for UpdateServerTemplate {
|
||||
#[instrument(name = "UpdateServerTemplate", skip(user))]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<ServerTemplate> {
|
||||
Ok(
|
||||
resource::update::<ServerTemplate>(&self.id, self.config, user)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for RenameServerTemplate {
|
||||
#[instrument(name = "RenameServerTemplate", skip(user))]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
Ok(
|
||||
resource::rename::<ServerTemplate>(&self.id, &self.name, user)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@ use komodo_client::{
|
||||
procedure::Procedure,
|
||||
repo::Repo,
|
||||
server::Server,
|
||||
server_template::ServerTemplate,
|
||||
stack::Stack,
|
||||
sync::{
|
||||
PartialResourceSyncConfig, ResourceSync, ResourceSyncInfo,
|
||||
@@ -686,17 +685,6 @@ impl Resolve<WriteArgs> for RefreshResourceSyncPending {
|
||||
&mut diffs,
|
||||
)
|
||||
.await?;
|
||||
push_updates_for_view::<ServerTemplate>(
|
||||
resources.server_templates,
|
||||
delete,
|
||||
&all_resources,
|
||||
None,
|
||||
None,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
)
|
||||
.await?;
|
||||
push_updates_for_view::<ResourceSync>(
|
||||
resources.resource_syncs,
|
||||
delete,
|
||||
|
||||
@@ -17,7 +17,6 @@ use komodo_client::{
|
||||
procedure::Procedure,
|
||||
repo::Repo,
|
||||
server::Server,
|
||||
server_template::ServerTemplate,
|
||||
stack::Stack,
|
||||
sync::ResourceSync,
|
||||
tag::{Tag, TagColor},
|
||||
@@ -131,7 +130,6 @@ impl Resolve<WriteArgs> for DeleteTag {
|
||||
resource::remove_tag_from_all::<Builder>(&self.id),
|
||||
resource::remove_tag_from_all::<Alerter>(&self.id),
|
||||
resource::remove_tag_from_all::<Procedure>(&self.id),
|
||||
resource::remove_tag_from_all::<ServerTemplate>(&self.id),
|
||||
)?;
|
||||
|
||||
delete_one_by_id(&db_client().tags, &self.id, None).await?;
|
||||
@@ -225,16 +223,6 @@ impl Resolve<WriteArgs> for UpdateTagsOnResource {
|
||||
.await?;
|
||||
resource::update_tags::<Action>(&id, self.tags, args).await?
|
||||
}
|
||||
ResourceTarget::ServerTemplate(id) => {
|
||||
resource::get_check_permissions::<ServerTemplate>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
resource::update_tags::<ServerTemplate>(&id, self.tags, args)
|
||||
.await?
|
||||
}
|
||||
ResourceTarget::ResourceSync(id) => {
|
||||
resource::get_check_permissions::<ResourceSync>(
|
||||
&id,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{str::FromStr, time::Duration};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use aws_config::{BehaviorVersion, Region};
|
||||
@@ -8,15 +8,15 @@ use aws_sdk_ec2::{
|
||||
BlockDeviceMapping, EbsBlockDevice,
|
||||
InstanceNetworkInterfaceSpecification, InstanceStateChange,
|
||||
InstanceStateName, InstanceStatus, InstanceType, ResourceType,
|
||||
Tag, TagSpecification, VolumeType,
|
||||
Tag, TagSpecification,
|
||||
},
|
||||
};
|
||||
use base64::Engine;
|
||||
use komodo_client::entities::{
|
||||
ResourceTarget,
|
||||
alert::{Alert, AlertData, SeverityLevel},
|
||||
builder::AwsBuilderConfig,
|
||||
komodo_timestamp,
|
||||
server_template::aws::AwsServerTemplateConfig,
|
||||
};
|
||||
|
||||
use crate::{alert::send_alerts, config::core_config};
|
||||
@@ -71,12 +71,12 @@ async fn create_ec2_client(region: String) -> Client {
|
||||
#[instrument]
|
||||
pub async fn launch_ec2_instance(
|
||||
name: &str,
|
||||
config: AwsServerTemplateConfig,
|
||||
config: &AwsBuilderConfig,
|
||||
) -> anyhow::Result<Ec2Instance> {
|
||||
let AwsServerTemplateConfig {
|
||||
let AwsBuilderConfig {
|
||||
region,
|
||||
instance_type,
|
||||
volumes,
|
||||
volume_gb,
|
||||
ami_id,
|
||||
subnet_id,
|
||||
security_group_ids,
|
||||
@@ -86,19 +86,22 @@ pub async fn launch_ec2_instance(
|
||||
user_data,
|
||||
port: _,
|
||||
use_https: _,
|
||||
git_providers: _,
|
||||
docker_registries: _,
|
||||
secrets: _,
|
||||
} = config;
|
||||
let instance_type = handle_unknown_instance_type(
|
||||
InstanceType::from(instance_type.as_str()),
|
||||
)?;
|
||||
let client = create_ec2_client(region.clone()).await;
|
||||
let mut req = client
|
||||
let req = client
|
||||
.run_instances()
|
||||
.image_id(ami_id)
|
||||
.instance_type(instance_type)
|
||||
.network_interfaces(
|
||||
InstanceNetworkInterfaceSpecification::builder()
|
||||
.subnet_id(subnet_id)
|
||||
.associate_public_ip_address(assign_public_ip)
|
||||
.associate_public_ip_address(*assign_public_ip)
|
||||
.set_groups(security_group_ids.to_vec().into())
|
||||
.device_index(0)
|
||||
.build(),
|
||||
@@ -110,6 +113,17 @@ pub async fn launch_ec2_instance(
|
||||
.resource_type(ResourceType::Instance)
|
||||
.build(),
|
||||
)
|
||||
.block_device_mappings(
|
||||
BlockDeviceMapping::builder()
|
||||
.set_device_name("/dev/sda1".to_string().into())
|
||||
.set_ebs(
|
||||
EbsBlockDevice::builder()
|
||||
.volume_size(*volume_gb)
|
||||
.build()
|
||||
.into(),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
.min_count(1)
|
||||
.max_count(1)
|
||||
.user_data(
|
||||
@@ -117,26 +131,6 @@ pub async fn launch_ec2_instance(
|
||||
.encode(user_data),
|
||||
);
|
||||
|
||||
for volume in volumes {
|
||||
let ebs = EbsBlockDevice::builder()
|
||||
.volume_size(volume.size_gb)
|
||||
.volume_type(
|
||||
VolumeType::from_str(volume.volume_type.as_ref())
|
||||
.context("invalid volume type")?,
|
||||
)
|
||||
.set_iops((volume.iops != 0).then_some(volume.iops))
|
||||
.set_throughput(
|
||||
(volume.throughput != 0).then_some(volume.throughput),
|
||||
)
|
||||
.build();
|
||||
req = req.block_device_mappings(
|
||||
BlockDeviceMapping::builder()
|
||||
.set_device_name(volume.device_name.into())
|
||||
.set_ebs(ebs.into())
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
let res = req
|
||||
.send()
|
||||
.await
|
||||
@@ -156,7 +150,7 @@ pub async fn launch_ec2_instance(
|
||||
let state_name =
|
||||
get_ec2_instance_state_name(&client, &instance_id).await?;
|
||||
if state_name == Some(InstanceStateName::Running) {
|
||||
let ip = if use_public_ip {
|
||||
let ip = if *use_public_ip {
|
||||
get_ec2_instance_public_ip(&client, &instance_id).await?
|
||||
} else {
|
||||
instance
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
use anyhow::{Context, anyhow};
|
||||
use axum::http::{HeaderName, HeaderValue};
|
||||
use reqwest::{RequestBuilder, StatusCode};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
|
||||
use super::{
|
||||
common::{
|
||||
HetznerActionResponse, HetznerDatacenterResponse,
|
||||
HetznerServerResponse, HetznerVolumeResponse,
|
||||
},
|
||||
create_server::{CreateServerBody, CreateServerResponse},
|
||||
create_volume::{CreateVolumeBody, CreateVolumeResponse},
|
||||
};
|
||||
|
||||
const BASE_URL: &str = "https://api.hetzner.cloud/v1";
|
||||
|
||||
pub struct HetznerClient(reqwest::Client);
|
||||
|
||||
impl HetznerClient {
|
||||
pub fn new(token: &str) -> HetznerClient {
|
||||
HetznerClient(
|
||||
reqwest::ClientBuilder::new()
|
||||
.default_headers(
|
||||
[(
|
||||
HeaderName::from_static("authorization"),
|
||||
HeaderValue::from_str(&format!("Bearer {token}"))
|
||||
.unwrap(),
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
)
|
||||
.build()
|
||||
.context("failed to build Hetzner request client")
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn get_server(
|
||||
&self,
|
||||
id: i64,
|
||||
) -> anyhow::Result<HetznerServerResponse> {
|
||||
self.get(&format!("/servers/{id}")).await
|
||||
}
|
||||
|
||||
pub async fn create_server(
|
||||
&self,
|
||||
body: &CreateServerBody,
|
||||
) -> anyhow::Result<CreateServerResponse> {
|
||||
self.post("/servers", body).await
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub async fn delete_server(
|
||||
&self,
|
||||
id: i64,
|
||||
) -> anyhow::Result<HetznerActionResponse> {
|
||||
self.delete(&format!("/servers/{id}")).await
|
||||
}
|
||||
|
||||
pub async fn get_volume(
|
||||
&self,
|
||||
id: i64,
|
||||
) -> anyhow::Result<HetznerVolumeResponse> {
|
||||
self.get(&format!("/volumes/{id}")).await
|
||||
}
|
||||
|
||||
pub async fn create_volume(
|
||||
&self,
|
||||
body: &CreateVolumeBody,
|
||||
) -> anyhow::Result<CreateVolumeResponse> {
|
||||
self.post("/volumes", body).await
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub async fn delete_volume(&self, id: i64) -> anyhow::Result<()> {
|
||||
let res = self
|
||||
.0
|
||||
.delete(format!("{BASE_URL}/volumes/{id}"))
|
||||
.send()
|
||||
.await
|
||||
.context("failed at request to delete volume")?;
|
||||
|
||||
let status = res.status();
|
||||
|
||||
if status == StatusCode::NO_CONTENT {
|
||||
Ok(())
|
||||
} else {
|
||||
let text = res
|
||||
.text()
|
||||
.await
|
||||
.context("failed to get response body as text")?;
|
||||
Err(anyhow!("{status} | {text}"))
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub async fn list_datacenters(
|
||||
&self,
|
||||
) -> anyhow::Result<HetznerDatacenterResponse> {
|
||||
self.get("/datacenters").await
|
||||
}
|
||||
|
||||
async fn get<Res: DeserializeOwned>(
|
||||
&self,
|
||||
path: &str,
|
||||
) -> anyhow::Result<Res> {
|
||||
let req = self.0.get(format!("{BASE_URL}{path}"));
|
||||
handle_req(req).await.with_context(|| {
|
||||
format!("failed at GET request to Hetzner | path: {path}")
|
||||
})
|
||||
}
|
||||
|
||||
async fn post<Body: Serialize, Res: DeserializeOwned>(
|
||||
&self,
|
||||
path: &str,
|
||||
body: &Body,
|
||||
) -> anyhow::Result<Res> {
|
||||
let req = self.0.post(format!("{BASE_URL}{path}")).json(&body);
|
||||
handle_req(req).await.with_context(|| {
|
||||
format!("failed at POST request to Hetzner | path: {path}")
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
async fn delete<Res: DeserializeOwned>(
|
||||
&self,
|
||||
path: &str,
|
||||
) -> anyhow::Result<Res> {
|
||||
let req = self.0.delete(format!("{BASE_URL}{path}"));
|
||||
handle_req(req).await.with_context(|| {
|
||||
format!("failed at DELETE request to Hetzner | path: {path}")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_req<Res: DeserializeOwned>(
|
||||
req: RequestBuilder,
|
||||
) -> anyhow::Result<Res> {
|
||||
let res = req.send().await?;
|
||||
|
||||
let status = res.status();
|
||||
|
||||
if status.is_success() {
|
||||
res.json().await.context("failed to parse response to json")
|
||||
} else {
|
||||
let text = res
|
||||
.text()
|
||||
.await
|
||||
.context("failed to get response body as text")?;
|
||||
if let Ok(json_error) =
|
||||
serde_json::from_str::<serde_json::Value>(&text)
|
||||
{
|
||||
return Err(anyhow!("{status} | {json_error:?}"));
|
||||
}
|
||||
Err(anyhow!("{status} | {text}"))
|
||||
}
|
||||
}
|
||||
@@ -1,280 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct HetznerServerResponse {
|
||||
pub server: HetznerServer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct HetznerServer {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub primary_disk_size: f64,
|
||||
pub image: Option<HetznerImage>,
|
||||
pub private_net: Vec<HetznerPrivateNet>,
|
||||
pub public_net: HetznerPublicNet,
|
||||
pub server_type: HetznerServerTypeDetails,
|
||||
pub status: HetznerServerStatus,
|
||||
#[serde(default)]
|
||||
pub volumes: Vec<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct HetznerServerTypeDetails {
|
||||
pub architecture: String,
|
||||
pub cores: i64,
|
||||
pub cpu_type: String,
|
||||
pub description: String,
|
||||
pub disk: f64,
|
||||
pub id: i64,
|
||||
pub memory: f64,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct HetznerPrivateNet {
|
||||
pub alias_ips: Vec<String>,
|
||||
pub ip: String,
|
||||
pub mac_address: String,
|
||||
pub network: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct HetznerPublicNet {
|
||||
#[serde(default)]
|
||||
pub firewalls: Vec<HetznerFirewall>,
|
||||
pub floating_ips: Vec<i64>,
|
||||
pub ipv4: Option<HetznerIpv4>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct HetznerFirewall {
|
||||
pub id: i64,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct HetznerIpv4 {
|
||||
pub id: Option<i64>,
|
||||
pub blocked: bool,
|
||||
pub dns_ptr: String,
|
||||
pub ip: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct HetznerImage {
|
||||
pub id: i64,
|
||||
pub description: String,
|
||||
pub name: Option<String>,
|
||||
pub os_flavor: String,
|
||||
pub os_version: Option<String>,
|
||||
pub rapid_deploy: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct HetznerActionResponse {
|
||||
pub action: HetznerAction,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct HetznerAction {
|
||||
pub command: String,
|
||||
pub error: Option<HetznerError>,
|
||||
pub finished: Option<String>,
|
||||
pub id: i64,
|
||||
pub progress: i32,
|
||||
pub resources: Vec<HetznerResource>,
|
||||
pub started: String,
|
||||
pub status: HetznerActionStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct HetznerError {
|
||||
pub code: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct HetznerResource {
|
||||
pub id: i64,
|
||||
#[serde(rename = "type")]
|
||||
pub ty: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct HetznerVolumeResponse {
|
||||
pub volume: HetznerVolume,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct HetznerVolume {
|
||||
/// Name of the Resource. Must be unique per Project.
|
||||
pub name: String,
|
||||
/// Point in time when the Resource was created (in ISO-8601 format).
|
||||
pub created: String,
|
||||
/// Filesystem of the Volume if formatted on creation, null if not formatted on creation
|
||||
pub format: Option<HetznerVolumeFormat>,
|
||||
/// ID of the Volume.
|
||||
pub id: i64,
|
||||
/// User-defined labels ( key/value pairs) for the Resource
|
||||
pub labels: HashMap<String, String>,
|
||||
/// Device path on the file system for the Volume
|
||||
pub linux_device: String,
|
||||
/// Protection configuration for the Resource.
|
||||
pub protection: HetznerProtection,
|
||||
/// ID of the Server the Volume is attached to, null if it is not attached at all
|
||||
pub server: Option<i64>,
|
||||
/// Size in GB of the Volume
|
||||
pub size: i64,
|
||||
/// Current status of the Volume. Allowed: `creating`, `available`
|
||||
pub status: HetznerVolumeStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct HetznerProtection {
|
||||
/// Prevent the Resource from being deleted.
|
||||
pub delete: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct HetznerDatacenterResponse {
|
||||
pub datacenters: Vec<HetznerDatacenterDetails>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct HetznerDatacenterDetails {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub location: serde_json::Map<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum HetznerLocation {
|
||||
#[serde(rename = "nbg1")]
|
||||
Nuremberg1,
|
||||
#[serde(rename = "hel1")]
|
||||
Helsinki1,
|
||||
#[serde(rename = "fsn1")]
|
||||
Falkenstein1,
|
||||
#[serde(rename = "ash")]
|
||||
Ashburn,
|
||||
#[serde(rename = "hil")]
|
||||
Hillsboro,
|
||||
#[serde(rename = "sin")]
|
||||
Singapore,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub enum HetznerDatacenter {
|
||||
#[serde(rename = "nbg1-dc3")]
|
||||
Nuremberg1Dc3,
|
||||
#[serde(rename = "hel1-dc2")]
|
||||
Helsinki1Dc2,
|
||||
#[serde(rename = "fsn1-dc14")]
|
||||
Falkenstein1Dc14,
|
||||
#[serde(rename = "ash-dc1")]
|
||||
AshburnDc1,
|
||||
#[serde(rename = "hil-dc1")]
|
||||
HillsboroDc1,
|
||||
#[serde(rename = "sin-dc1")]
|
||||
SingaporeDc1,
|
||||
}
|
||||
|
||||
impl From<HetznerDatacenter> for HetznerLocation {
|
||||
fn from(value: HetznerDatacenter) -> Self {
|
||||
match value {
|
||||
HetznerDatacenter::Nuremberg1Dc3 => HetznerLocation::Nuremberg1,
|
||||
HetznerDatacenter::Helsinki1Dc2 => HetznerLocation::Helsinki1,
|
||||
HetznerDatacenter::Falkenstein1Dc14 => {
|
||||
HetznerLocation::Falkenstein1
|
||||
}
|
||||
HetznerDatacenter::AshburnDc1 => HetznerLocation::Ashburn,
|
||||
HetznerDatacenter::HillsboroDc1 => HetznerLocation::Hillsboro,
|
||||
HetznerDatacenter::SingaporeDc1 => HetznerLocation::Singapore,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum HetznerVolumeFormat {
|
||||
Xfs,
|
||||
Ext4,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum HetznerVolumeStatus {
|
||||
Creating,
|
||||
Available,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum HetznerServerStatus {
|
||||
Running,
|
||||
Initializing,
|
||||
Starting,
|
||||
Stopping,
|
||||
Off,
|
||||
Deleting,
|
||||
Migrating,
|
||||
Rebuilding,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum HetznerActionStatus {
|
||||
Running,
|
||||
Success,
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub enum HetznerServerType {
|
||||
// Shared
|
||||
#[serde(rename = "cpx11")]
|
||||
SharedAmd2Core2Ram40Disk,
|
||||
#[serde(rename = "cax11")]
|
||||
SharedArm2Core4Ram40Disk,
|
||||
#[serde(rename = "cx22")]
|
||||
SharedIntel2Core4Ram40Disk,
|
||||
#[serde(rename = "cpx21")]
|
||||
SharedAmd3Core4Ram80Disk,
|
||||
#[serde(rename = "cax21")]
|
||||
SharedArm4Core8Ram80Disk,
|
||||
#[serde(rename = "cx32")]
|
||||
SharedIntel4Core8Ram80Disk,
|
||||
#[serde(rename = "cpx31")]
|
||||
SharedAmd4Core8Ram160Disk,
|
||||
#[serde(rename = "cax31")]
|
||||
SharedArm8Core16Ram160Disk,
|
||||
#[serde(rename = "cx42")]
|
||||
SharedIntel8Core16Ram160Disk,
|
||||
#[serde(rename = "cpx41")]
|
||||
SharedAmd8Core16Ram240Disk,
|
||||
#[serde(rename = "cax41")]
|
||||
SharedArm16Core32Ram320Disk,
|
||||
#[serde(rename = "cx52")]
|
||||
SharedIntel16Core32Ram320Disk,
|
||||
#[serde(rename = "cpx51")]
|
||||
SharedAmd16Core32Ram360Disk,
|
||||
// Dedicated
|
||||
#[serde(rename = "ccx13")]
|
||||
DedicatedAmd2Core8Ram80Disk,
|
||||
#[serde(rename = "ccx23")]
|
||||
DedicatedAmd4Core16Ram160Disk,
|
||||
#[serde(rename = "ccx33")]
|
||||
DedicatedAmd8Core32Ram240Disk,
|
||||
#[serde(rename = "ccx43")]
|
||||
DedicatedAmd16Core64Ram360Disk,
|
||||
#[serde(rename = "ccx53")]
|
||||
DedicatedAmd32Core128Ram600Disk,
|
||||
#[serde(rename = "ccx63")]
|
||||
DedicatedAmd48Core192Ram960Disk,
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::common::{
|
||||
HetznerAction, HetznerDatacenter, HetznerLocation, HetznerServer,
|
||||
HetznerServerType,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct CreateServerBody {
|
||||
/// Name of the Server to create (must be unique per Project and a valid hostname as per RFC 1123)
|
||||
pub name: String,
|
||||
/// Auto-mount Volumes after attach
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub automount: Option<bool>,
|
||||
/// ID or name of Datacenter to create Server in (must not be used together with location)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub datacenter: Option<HetznerDatacenter>,
|
||||
/// ID or name of Location to create Server in (must not be used together with datacenter)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub location: Option<HetznerLocation>,
|
||||
/// Firewalls which should be applied on the Server's public network interface at creation time
|
||||
pub firewalls: Vec<Firewall>,
|
||||
/// ID or name of the Image the Server is created from
|
||||
pub image: String,
|
||||
/// User-defined labels (key-value pairs) for the Resource
|
||||
pub labels: HashMap<String, String>,
|
||||
/// Network IDs which should be attached to the Server private network interface at the creation time
|
||||
pub networks: Vec<i64>,
|
||||
/// ID of the Placement Group the server should be in
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub placement_group: Option<i64>,
|
||||
/// Public Network options
|
||||
pub public_net: PublicNet,
|
||||
/// ID or name of the Server type this Server should be created with
|
||||
pub server_type: HetznerServerType,
|
||||
/// SSH key IDs ( integer ) or names ( string ) which should be injected into the Server at creation time
|
||||
pub ssh_keys: Vec<String>,
|
||||
/// This automatically triggers a Power on a Server-Server Action after the creation is finished and is returned in the next_actions response object.
|
||||
pub start_after_create: bool,
|
||||
/// Cloud-Init user data to use during Server creation. This field is limited to 32KiB.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user_data: Option<String>,
|
||||
/// Volume IDs which should be attached to the Server at the creation time. Volumes must be in the same Location.
|
||||
pub volumes: Vec<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize)]
|
||||
pub struct Firewall {
|
||||
/// ID of the Firewall
|
||||
pub firewall: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize)]
|
||||
pub struct PublicNet {
|
||||
/// Attach an IPv4 on the public NIC. If false, no IPv4 address will be attached.
|
||||
pub enable_ipv4: bool,
|
||||
/// Attach an IPv6 on the public NIC. If false, no IPv6 address will be attached.
|
||||
pub enable_ipv6: bool,
|
||||
/// ID of the ipv4 Primary IP to use. If omitted and enable_ipv4 is true, a new ipv4 Primary IP will automatically be created.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ipv4: Option<i64>,
|
||||
/// ID of the ipv6 Primary IP to use. If omitted and enable_ipv6 is true, a new ipv6 Primary IP will automatically be created.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ipv6: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct CreateServerResponse {
|
||||
pub action: HetznerAction,
|
||||
pub next_actions: Vec<HetznerAction>,
|
||||
pub root_password: Option<String>,
|
||||
pub server: HetznerServer,
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::common::{
|
||||
HetznerAction, HetznerLocation, HetznerVolume, HetznerVolumeFormat,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct CreateVolumeBody {
|
||||
/// Name of the volume
|
||||
pub name: String,
|
||||
/// Auto-mount Volume after attach. server must be provided.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub automount: Option<bool>,
|
||||
/// Format Volume after creation. One of: xfs, ext4
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub format: Option<HetznerVolumeFormat>,
|
||||
/// User-defined labels (key-value pairs) for the Resource
|
||||
pub labels: HashMap<String, String>,
|
||||
/// Location to create the Volume in (can be omitted if Server is specified)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub location: Option<HetznerLocation>,
|
||||
/// Server to which to attach the Volume once it's created (Volume will be created in the same Location as the server)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub server: Option<i64>,
|
||||
/// Size of the Volume in GB
|
||||
pub size: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct CreateVolumeResponse {
|
||||
pub action: HetznerAction,
|
||||
pub next_actions: Vec<HetznerAction>,
|
||||
pub volume: HetznerVolume,
|
||||
}
|
||||
@@ -1,281 +0,0 @@
|
||||
use std::{
|
||||
sync::{Arc, Mutex, OnceLock},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use futures::future::join_all;
|
||||
use komodo_client::entities::server_template::hetzner::{
|
||||
HetznerDatacenter, HetznerServerTemplateConfig, HetznerServerType,
|
||||
HetznerVolumeFormat,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
cloud::hetzner::{
|
||||
common::HetznerServerStatus, create_server::CreateServerBody,
|
||||
create_volume::CreateVolumeBody,
|
||||
},
|
||||
config::core_config,
|
||||
};
|
||||
|
||||
use self::{client::HetznerClient, common::HetznerVolumeStatus};
|
||||
|
||||
mod client;
|
||||
mod common;
|
||||
mod create_server;
|
||||
mod create_volume;
|
||||
|
||||
fn hetzner() -> Option<&'static HetznerClient> {
|
||||
static HETZNER_CLIENT: OnceLock<Option<HetznerClient>> =
|
||||
OnceLock::new();
|
||||
HETZNER_CLIENT
|
||||
.get_or_init(|| {
|
||||
let token = &core_config().hetzner.token;
|
||||
(!token.is_empty()).then(|| HetznerClient::new(token))
|
||||
})
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
pub struct HetznerServerMinimal {
|
||||
pub id: i64,
|
||||
pub ip: String,
|
||||
}
|
||||
|
||||
const POLL_RATE_SECS: u64 = 3;
|
||||
const MAX_POLL_TRIES: usize = 100;
|
||||
|
||||
#[instrument]
|
||||
pub async fn launch_hetzner_server(
|
||||
name: &str,
|
||||
config: HetznerServerTemplateConfig,
|
||||
) -> anyhow::Result<HetznerServerMinimal> {
|
||||
let hetzner =
|
||||
*hetzner().as_ref().context("Hetzner token not configured")?;
|
||||
let HetznerServerTemplateConfig {
|
||||
image,
|
||||
datacenter,
|
||||
private_network_ids,
|
||||
placement_group,
|
||||
enable_public_ipv4,
|
||||
enable_public_ipv6,
|
||||
firewall_ids,
|
||||
server_type,
|
||||
ssh_keys,
|
||||
user_data,
|
||||
use_public_ip,
|
||||
labels,
|
||||
volumes,
|
||||
port: _,
|
||||
use_https: _,
|
||||
} = config;
|
||||
let datacenter = hetzner_datacenter(datacenter);
|
||||
|
||||
// Create volumes and get their ids
|
||||
let mut volume_ids = Vec::new();
|
||||
for volume in volumes {
|
||||
let body = CreateVolumeBody {
|
||||
name: volume.name,
|
||||
format: Some(hetzner_format(volume.format)),
|
||||
location: Some(datacenter.into()),
|
||||
labels: volume.labels,
|
||||
size: volume.size_gb,
|
||||
automount: None,
|
||||
server: None,
|
||||
};
|
||||
let id = hetzner
|
||||
.create_volume(&body)
|
||||
.await
|
||||
.context("failed to create hetzner volume")?
|
||||
.volume
|
||||
.id;
|
||||
volume_ids.push(id);
|
||||
}
|
||||
|
||||
// Make sure volumes are available before continue
|
||||
let vol_ids_poll = Arc::new(Mutex::new(volume_ids.clone()));
|
||||
for _ in 0..MAX_POLL_TRIES {
|
||||
if vol_ids_poll.lock().unwrap().is_empty() {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(POLL_RATE_SECS)).await;
|
||||
let ids = vol_ids_poll.lock().unwrap().clone();
|
||||
let futures = ids.into_iter().map(|id| {
|
||||
let vol_ids = vol_ids_poll.clone();
|
||||
async move {
|
||||
let Ok(res) = hetzner.get_volume(id).await else {
|
||||
return;
|
||||
};
|
||||
if matches!(res.volume.status, HetznerVolumeStatus::Available)
|
||||
{
|
||||
vol_ids.lock().unwrap().retain(|_id| *_id != id);
|
||||
}
|
||||
}
|
||||
});
|
||||
join_all(futures).await;
|
||||
}
|
||||
if !vol_ids_poll.lock().unwrap().is_empty() {
|
||||
return Err(anyhow!("Volumes not ready after poll"));
|
||||
}
|
||||
|
||||
let body = CreateServerBody {
|
||||
name: name.to_string(),
|
||||
automount: None,
|
||||
datacenter: Some(datacenter),
|
||||
location: None,
|
||||
firewalls: firewall_ids
|
||||
.into_iter()
|
||||
.map(|firewall| create_server::Firewall { firewall })
|
||||
.collect(),
|
||||
image,
|
||||
labels,
|
||||
networks: private_network_ids,
|
||||
placement_group: (placement_group > 0).then_some(placement_group),
|
||||
public_net: create_server::PublicNet {
|
||||
enable_ipv4: enable_public_ipv4,
|
||||
enable_ipv6: enable_public_ipv6,
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
},
|
||||
server_type: hetzner_server_type(server_type),
|
||||
ssh_keys,
|
||||
start_after_create: true,
|
||||
user_data: (!user_data.is_empty()).then_some(user_data),
|
||||
volumes: volume_ids,
|
||||
};
|
||||
|
||||
let server_id = hetzner
|
||||
.create_server(&body)
|
||||
.await
|
||||
.context("failed to create hetnzer server")?
|
||||
.server
|
||||
.id;
|
||||
|
||||
for _ in 0..MAX_POLL_TRIES {
|
||||
tokio::time::sleep(Duration::from_secs(POLL_RATE_SECS)).await;
|
||||
let Ok(res) = hetzner.get_server(server_id).await else {
|
||||
continue;
|
||||
};
|
||||
if matches!(res.server.status, HetznerServerStatus::Running) {
|
||||
let ip = if use_public_ip {
|
||||
res
|
||||
.server
|
||||
.public_net
|
||||
.ipv4
|
||||
.context("instance does not have public ipv4 attached")?
|
||||
.ip
|
||||
} else {
|
||||
res
|
||||
.server
|
||||
.private_net
|
||||
.first()
|
||||
.context("no private networks attached")?
|
||||
.ip
|
||||
.to_string()
|
||||
};
|
||||
let server = HetznerServerMinimal { id: server_id, ip };
|
||||
return Ok(server);
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow!(
|
||||
"failed to verify server running after polling status"
|
||||
))
|
||||
}
|
||||
|
||||
fn hetzner_format(
|
||||
format: HetznerVolumeFormat,
|
||||
) -> common::HetznerVolumeFormat {
|
||||
match format {
|
||||
HetznerVolumeFormat::Xfs => common::HetznerVolumeFormat::Xfs,
|
||||
HetznerVolumeFormat::Ext4 => common::HetznerVolumeFormat::Ext4,
|
||||
}
|
||||
}
|
||||
|
||||
fn hetzner_datacenter(
|
||||
datacenter: HetznerDatacenter,
|
||||
) -> common::HetznerDatacenter {
|
||||
match datacenter {
|
||||
HetznerDatacenter::Nuremberg1Dc3 => {
|
||||
common::HetznerDatacenter::Nuremberg1Dc3
|
||||
}
|
||||
HetznerDatacenter::Helsinki1Dc2 => {
|
||||
common::HetznerDatacenter::Helsinki1Dc2
|
||||
}
|
||||
HetznerDatacenter::Falkenstein1Dc14 => {
|
||||
common::HetznerDatacenter::Falkenstein1Dc14
|
||||
}
|
||||
HetznerDatacenter::AshburnDc1 => {
|
||||
common::HetznerDatacenter::AshburnDc1
|
||||
}
|
||||
HetznerDatacenter::HillsboroDc1 => {
|
||||
common::HetznerDatacenter::HillsboroDc1
|
||||
}
|
||||
HetznerDatacenter::SingaporeDc1 => {
|
||||
common::HetznerDatacenter::SingaporeDc1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn hetzner_server_type(
|
||||
server_type: HetznerServerType,
|
||||
) -> common::HetznerServerType {
|
||||
match server_type {
|
||||
HetznerServerType::SharedAmd2Core2Ram40Disk => {
|
||||
common::HetznerServerType::SharedAmd2Core2Ram40Disk
|
||||
}
|
||||
HetznerServerType::SharedArm2Core4Ram40Disk => {
|
||||
common::HetznerServerType::SharedArm2Core4Ram40Disk
|
||||
}
|
||||
HetznerServerType::SharedIntel2Core4Ram40Disk => {
|
||||
common::HetznerServerType::SharedIntel2Core4Ram40Disk
|
||||
}
|
||||
HetznerServerType::SharedAmd3Core4Ram80Disk => {
|
||||
common::HetznerServerType::SharedAmd3Core4Ram80Disk
|
||||
}
|
||||
HetznerServerType::SharedArm4Core8Ram80Disk => {
|
||||
common::HetznerServerType::SharedArm4Core8Ram80Disk
|
||||
}
|
||||
HetznerServerType::SharedIntel4Core8Ram80Disk => {
|
||||
common::HetznerServerType::SharedIntel4Core8Ram80Disk
|
||||
}
|
||||
HetznerServerType::SharedAmd4Core8Ram160Disk => {
|
||||
common::HetznerServerType::SharedAmd4Core8Ram160Disk
|
||||
}
|
||||
HetznerServerType::SharedArm8Core16Ram160Disk => {
|
||||
common::HetznerServerType::SharedArm8Core16Ram160Disk
|
||||
}
|
||||
HetznerServerType::SharedIntel8Core16Ram160Disk => {
|
||||
common::HetznerServerType::SharedIntel8Core16Ram160Disk
|
||||
}
|
||||
HetznerServerType::SharedAmd8Core16Ram240Disk => {
|
||||
common::HetznerServerType::SharedAmd8Core16Ram240Disk
|
||||
}
|
||||
HetznerServerType::SharedArm16Core32Ram320Disk => {
|
||||
common::HetznerServerType::SharedArm16Core32Ram320Disk
|
||||
}
|
||||
HetznerServerType::SharedIntel16Core32Ram320Disk => {
|
||||
common::HetznerServerType::SharedIntel16Core32Ram320Disk
|
||||
}
|
||||
HetznerServerType::SharedAmd16Core32Ram360Disk => {
|
||||
common::HetznerServerType::SharedAmd16Core32Ram360Disk
|
||||
}
|
||||
HetznerServerType::DedicatedAmd2Core8Ram80Disk => {
|
||||
common::HetznerServerType::DedicatedAmd2Core8Ram80Disk
|
||||
}
|
||||
HetznerServerType::DedicatedAmd4Core16Ram160Disk => {
|
||||
common::HetznerServerType::DedicatedAmd4Core16Ram160Disk
|
||||
}
|
||||
HetznerServerType::DedicatedAmd8Core32Ram240Disk => {
|
||||
common::HetznerServerType::DedicatedAmd8Core32Ram240Disk
|
||||
}
|
||||
HetznerServerType::DedicatedAmd16Core64Ram360Disk => {
|
||||
common::HetznerServerType::DedicatedAmd16Core64Ram360Disk
|
||||
}
|
||||
HetznerServerType::DedicatedAmd32Core128Ram600Disk => {
|
||||
common::HetznerServerType::DedicatedAmd32Core128Ram600Disk
|
||||
}
|
||||
HetznerServerType::DedicatedAmd48Core192Ram960Disk => {
|
||||
common::HetznerServerType::DedicatedAmd48Core192Ram960Disk
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
pub mod aws;
|
||||
|
||||
#[allow(unused)]
|
||||
pub mod hetzner;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum BuildCleanupData {
|
||||
/// Nothing to clean up
|
||||
|
||||
@@ -8,7 +8,7 @@ use komodo_client::entities::{
|
||||
config::core::{
|
||||
AwsCredentials, CoreConfig, DatabaseConfig, Env,
|
||||
GithubWebhookAppConfig, GithubWebhookAppInstallationConfig,
|
||||
HetznerCredentials, OauthCredentials,
|
||||
OauthCredentials,
|
||||
},
|
||||
logger::LogConfig,
|
||||
};
|
||||
@@ -120,11 +120,6 @@ pub fn core_config() -> &'static CoreConfig {
|
||||
.komodo_aws_secret_access_key)
|
||||
.unwrap_or(config.aws.secret_access_key),
|
||||
},
|
||||
hetzner: HetznerCredentials {
|
||||
token: maybe_read_item_from_file(env.komodo_hetzner_token_file, env
|
||||
.komodo_hetzner_token)
|
||||
.unwrap_or(config.hetzner.token),
|
||||
},
|
||||
github_webhook_app: GithubWebhookAppConfig {
|
||||
app_id: maybe_read_item_from_file(env.komodo_github_webhook_app_app_id_file, env
|
||||
.komodo_github_webhook_app_app_id)
|
||||
@@ -177,6 +172,8 @@ pub fn core_config() -> &'static CoreConfig {
|
||||
.unwrap_or(config.ui_write_disabled),
|
||||
disable_confirm_dialog: env.komodo_disable_confirm_dialog
|
||||
.unwrap_or(config.disable_confirm_dialog),
|
||||
disable_websocket_reconnect: env.komodo_disable_websocket_reconnect
|
||||
.unwrap_or(config.disable_websocket_reconnect),
|
||||
enable_new_users: env.komodo_enable_new_users
|
||||
.unwrap_or(config.enable_new_users),
|
||||
disable_user_registration: env.komodo_disable_user_registration
|
||||
|
||||
@@ -12,7 +12,6 @@ use komodo_client::entities::{
|
||||
provider::{DockerRegistryAccount, GitProviderAccount},
|
||||
repo::Repo,
|
||||
server::Server,
|
||||
server_template::ServerTemplate,
|
||||
stack::Stack,
|
||||
stats::SystemStatsRecord,
|
||||
sync::ResourceSync,
|
||||
@@ -50,7 +49,6 @@ pub struct DbClient {
|
||||
pub procedures: Collection<Procedure>,
|
||||
pub actions: Collection<Action>,
|
||||
pub alerters: Collection<Alerter>,
|
||||
pub server_templates: Collection<ServerTemplate>,
|
||||
pub resource_syncs: Collection<ResourceSync>,
|
||||
pub stacks: Collection<Stack>,
|
||||
//
|
||||
@@ -120,8 +118,6 @@ impl DbClient {
|
||||
alerters: resource_collection(&db, "Alerter").await?,
|
||||
procedures: resource_collection(&db, "Procedure").await?,
|
||||
actions: resource_collection(&db, "Action").await?,
|
||||
server_templates: resource_collection(&db, "ServerTemplate")
|
||||
.await?,
|
||||
resource_syncs: resource_collection(&db, "ResourceSync")
|
||||
.await?,
|
||||
stacks: resource_collection(&db, "Stack").await?,
|
||||
|
||||
@@ -7,7 +7,6 @@ use komodo_client::entities::{
|
||||
builder::{AwsBuilderConfig, Builder, BuilderConfig},
|
||||
komodo_timestamp,
|
||||
server::Server,
|
||||
server_template::aws::AwsServerTemplateConfig,
|
||||
update::{Log, Update},
|
||||
};
|
||||
use periphery_client::{
|
||||
@@ -88,11 +87,8 @@ async fn get_aws_builder(
|
||||
|
||||
let version = version.map(|v| format!("-v{v}")).unwrap_or_default();
|
||||
let instance_name = format!("BUILDER-{resource_name}{version}");
|
||||
let Ec2Instance { instance_id, ip } = launch_ec2_instance(
|
||||
&instance_name,
|
||||
AwsServerTemplateConfig::from_builder_config(&config),
|
||||
)
|
||||
.await?;
|
||||
let Ec2Instance { instance_id, ip } =
|
||||
launch_ec2_instance(&instance_name, &config).await?;
|
||||
|
||||
info!("ec2 instance launched");
|
||||
|
||||
|
||||
+8
-180
@@ -1,33 +1,18 @@
|
||||
use std::{str::FromStr, time::Duration};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use futures::future::join_all;
|
||||
use komodo_client::{
|
||||
api::write::{CreateBuilder, CreateServer},
|
||||
entities::{
|
||||
ResourceTarget,
|
||||
builder::{PartialBuilderConfig, PartialServerBuilderConfig},
|
||||
komodo_timestamp,
|
||||
permission::{Permission, PermissionLevel, UserTarget},
|
||||
server::{PartialServerConfig, Server},
|
||||
sync::ResourceSync,
|
||||
update::Log,
|
||||
user::{User, system_user},
|
||||
},
|
||||
use komodo_client::entities::{
|
||||
ResourceTarget,
|
||||
permission::{Permission, PermissionLevel, UserTarget},
|
||||
server::Server,
|
||||
user::User,
|
||||
};
|
||||
use mongo_indexed::Document;
|
||||
use mungos::{
|
||||
find::find_collect,
|
||||
mongodb::bson::{Bson, doc, oid::ObjectId, to_document},
|
||||
};
|
||||
use mungos::mongodb::bson::{Bson, doc};
|
||||
use periphery_client::PeripheryClient;
|
||||
use rand::Rng;
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
api::write::WriteArgs, config::core_config, resource,
|
||||
state::db_client,
|
||||
};
|
||||
use crate::{config::core_config, state::db_client};
|
||||
|
||||
pub mod action_state;
|
||||
pub mod builder;
|
||||
@@ -203,160 +188,3 @@ pub fn flatten_document(doc: Document) -> Document {
|
||||
|
||||
target
|
||||
}
|
||||
|
||||
pub async fn startup_cleanup() {
|
||||
tokio::join!(
|
||||
startup_in_progress_update_cleanup(),
|
||||
startup_open_alert_cleanup(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Run on startup, as no updates should be in progress on startup
|
||||
async fn startup_in_progress_update_cleanup() {
|
||||
let log = Log::error(
|
||||
"Komodo shutdown",
|
||||
String::from(
|
||||
"Komodo shutdown during execution. If this is a build, the builder may not have been terminated.",
|
||||
),
|
||||
);
|
||||
// This static log won't fail to serialize, unwrap ok.
|
||||
let log = to_document(&log).unwrap();
|
||||
if let Err(e) = db_client()
|
||||
.updates
|
||||
.update_many(
|
||||
doc! { "status": "InProgress" },
|
||||
doc! {
|
||||
"$set": {
|
||||
"status": "Complete",
|
||||
"success": false,
|
||||
},
|
||||
"$push": {
|
||||
"logs": log
|
||||
}
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("failed to cleanup in progress updates on startup | {e:#}")
|
||||
}
|
||||
}
|
||||
|
||||
/// Run on startup, ensure open alerts pointing to invalid resources are closed.
|
||||
async fn startup_open_alert_cleanup() {
|
||||
let db = db_client();
|
||||
let Ok(alerts) =
|
||||
find_collect(&db.alerts, doc! { "resolved": false }, None)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
error!(
|
||||
"failed to list all alerts for startup open alert cleanup | {e:?}"
|
||||
)
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let futures = alerts.into_iter().map(|alert| async move {
|
||||
match alert.target {
|
||||
ResourceTarget::Server(id) => {
|
||||
resource::get::<Server>(&id)
|
||||
.await
|
||||
.is_err()
|
||||
.then(|| ObjectId::from_str(&alert.id).inspect_err(|e| warn!("failed to clean up alert - id is invalid ObjectId | {e:?}")).ok()).flatten()
|
||||
}
|
||||
ResourceTarget::ResourceSync(id) => {
|
||||
resource::get::<ResourceSync>(&id)
|
||||
.await
|
||||
.is_err()
|
||||
.then(|| ObjectId::from_str(&alert.id).inspect_err(|e| warn!("failed to clean up alert - id is invalid ObjectId | {e:?}")).ok()).flatten()
|
||||
}
|
||||
// No other resources should have open alerts.
|
||||
_ => ObjectId::from_str(&alert.id).inspect_err(|e| warn!("failed to clean up alert - id is invalid ObjectId | {e:?}")).ok(),
|
||||
}
|
||||
});
|
||||
let to_update_ids = join_all(futures)
|
||||
.await
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
if let Err(e) = db
|
||||
.alerts
|
||||
.update_many(
|
||||
doc! { "_id": { "$in": to_update_ids } },
|
||||
doc! { "$set": {
|
||||
"resolved": true,
|
||||
"resolved_ts": komodo_timestamp()
|
||||
} },
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!(
|
||||
"failed to clean up invalid open alerts on startup | {e:#}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensures a default server / builder exists with the defined address
|
||||
pub async fn ensure_first_server_and_builder() {
|
||||
let first_server = &core_config().first_server;
|
||||
if first_server.is_empty() {
|
||||
return;
|
||||
}
|
||||
let db = db_client();
|
||||
let Ok(server) = db
|
||||
.servers
|
||||
.find_one(Document::new())
|
||||
.await
|
||||
.inspect_err(|e| error!("Failed to initialize 'first_server'. Failed to query db. {e:?}"))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let server = if let Some(server) = server {
|
||||
server
|
||||
} else {
|
||||
match (CreateServer {
|
||||
name: format!("server-{}", random_string(5)),
|
||||
config: PartialServerConfig {
|
||||
address: Some(first_server.to_string()),
|
||||
enabled: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
})
|
||||
.resolve(&WriteArgs {
|
||||
user: system_user().to_owned(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(server) => server,
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Failed to initialize 'first_server'. Failed to CreateServer. {:#}",
|
||||
e.error
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
let Ok(None) = db.builders
|
||||
.find_one(Document::new()).await
|
||||
.inspect_err(|e| error!("Failed to initialize 'first_builder' | Failed to query db | {e:?}")) else {
|
||||
return;
|
||||
};
|
||||
if let Err(e) = (CreateBuilder {
|
||||
name: String::from("local"),
|
||||
config: PartialBuilderConfig::Server(
|
||||
PartialServerBuilderConfig {
|
||||
server_id: Some(server.id),
|
||||
},
|
||||
),
|
||||
})
|
||||
.resolve(&WriteArgs {
|
||||
user: system_user().to_owned(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
error!(
|
||||
"Failed to initialize 'first_builder' | Failed to CreateBuilder | {:#}",
|
||||
e.error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,6 +166,13 @@ async fn execute_stage(
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Execution::BatchPullStack(exec) => {
|
||||
extend_batch_exection::<BatchPullStack>(
|
||||
&exec.pattern,
|
||||
&mut executions,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Execution::BatchDestroyStack(exec) => {
|
||||
extend_batch_exection::<BatchDestroyStack>(
|
||||
&exec.pattern,
|
||||
@@ -985,6 +992,12 @@ async fn execute_execution(
|
||||
)
|
||||
.await?
|
||||
}
|
||||
Execution::BatchPullStack(_) => {
|
||||
// All batch executions must be expanded in `execute_stage`
|
||||
return Err(anyhow!(
|
||||
"Batch method BatchPullStack not implemented correctly"
|
||||
));
|
||||
}
|
||||
Execution::StartStack(req) => {
|
||||
let req = ExecuteRequest::StartStack(req);
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
@@ -1275,6 +1288,16 @@ impl ExtendBatch for BatchDeployStackIfChanged {
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtendBatch for BatchPullStack {
|
||||
type Resource = Stack;
|
||||
fn single_execution(stack: String) -> Execution {
|
||||
Execution::PullStack(PullStack {
|
||||
stack,
|
||||
services: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtendBatch for BatchDestroyStack {
|
||||
type Resource = Stack;
|
||||
fn single_execution(stack: String) -> Execution {
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
str::FromStr,
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use async_timing_util::{ONE_MIN_MS, unix_timestamp_ms};
|
||||
use komodo_client::entities::{
|
||||
Operation, ResourceTarget, ResourceTargetVariant,
|
||||
action::Action,
|
||||
@@ -13,8 +18,8 @@ use komodo_client::entities::{
|
||||
procedure::Procedure,
|
||||
repo::Repo,
|
||||
server::{Server, ServerState},
|
||||
server_template::ServerTemplate,
|
||||
stack::{Stack, StackServiceNames, StackState},
|
||||
stats::SystemInformation,
|
||||
sync::ResourceSync,
|
||||
tag::Tag,
|
||||
update::Update,
|
||||
@@ -29,6 +34,8 @@ use mungos::{
|
||||
options::FindOneOptions,
|
||||
},
|
||||
};
|
||||
use periphery_client::api::stats;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
@@ -37,6 +44,8 @@ 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> {
|
||||
@@ -295,10 +304,6 @@ pub async fn get_user_permission_on_target(
|
||||
ResourceTarget::Action(id) => {
|
||||
get_user_permission_on_resource::<Action>(user, id).await
|
||||
}
|
||||
ResourceTarget::ServerTemplate(id) => {
|
||||
get_user_permission_on_resource::<ServerTemplate>(user, id)
|
||||
.await
|
||||
}
|
||||
ResourceTarget::ResourceSync(id) => {
|
||||
get_user_permission_on_resource::<ResourceSync>(user, id).await
|
||||
}
|
||||
@@ -382,3 +387,36 @@ 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)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ use komodo_client::entities::{
|
||||
procedure::Procedure,
|
||||
repo::Repo,
|
||||
server::Server,
|
||||
server_template::ServerTemplate,
|
||||
stack::Stack,
|
||||
sync::ResourceSync,
|
||||
update::{Update, UpdateListItem},
|
||||
@@ -385,16 +384,6 @@ pub async fn init_execution_update(
|
||||
return Ok(Default::default());
|
||||
}
|
||||
|
||||
// Server template
|
||||
ExecuteRequest::LaunchServer(data) => (
|
||||
Operation::LaunchServer,
|
||||
ResourceTarget::ServerTemplate(
|
||||
resource::get::<ServerTemplate>(&data.server_template)
|
||||
.await?
|
||||
.id,
|
||||
),
|
||||
),
|
||||
|
||||
// Resource Sync
|
||||
ExecuteRequest::RunSync(data) => (
|
||||
Operation::RunSync,
|
||||
@@ -446,6 +435,9 @@ pub async fn init_execution_update(
|
||||
resource::get::<Stack>(&data.stack).await?.id,
|
||||
),
|
||||
),
|
||||
ExecuteRequest::BatchPullStack(_data) => {
|
||||
return Ok(Default::default());
|
||||
}
|
||||
ExecuteRequest::RestartStack(data) => (
|
||||
if !data.services.is_empty() {
|
||||
Operation::RestartStackService
|
||||
|
||||
+14
-10
@@ -25,6 +25,7 @@ mod monitor;
|
||||
mod resource;
|
||||
mod schedule;
|
||||
mod stack;
|
||||
mod startup;
|
||||
mod state;
|
||||
mod sync;
|
||||
mod ts_client;
|
||||
@@ -34,26 +35,28 @@ 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());
|
||||
|
||||
// Init jwt client to crash on failure
|
||||
state::jwt_client();
|
||||
tokio::join!(
|
||||
// Init db_client check to crash on db init failure
|
||||
state::init_db_client(),
|
||||
// Manage OIDC client (defined in config / env vars / compose secret file)
|
||||
auth::oidc::client::spawn_oidc_client_management()
|
||||
);
|
||||
tokio::join!(
|
||||
// Maybe initialize first server
|
||||
helpers::ensure_first_server_and_builder(),
|
||||
// Cleanup open updates / invalid alerts
|
||||
helpers::startup_cleanup(),
|
||||
);
|
||||
// init jwt client to crash on failure
|
||||
state::jwt_client();
|
||||
|
||||
// Spawn tasks
|
||||
// Run after db connection.
|
||||
startup::on_startup().await;
|
||||
|
||||
// Spawn background tasks
|
||||
monitor::spawn_monitor_loop();
|
||||
resource::spawn_resource_refresh_loop();
|
||||
resource::spawn_build_state_refresh_loop();
|
||||
@@ -76,6 +79,7 @@ async fn app() -> anyhow::Result<()> {
|
||||
.nest("/read", api::read::router())
|
||||
.nest("/write", api::write::router())
|
||||
.nest("/execute", api::execute::router())
|
||||
.nest("/terminal", api::terminal::router())
|
||||
.nest("/listener", listener::router())
|
||||
.nest("/ws", ws::router())
|
||||
.nest("/client", ts_client::router())
|
||||
|
||||
@@ -56,7 +56,6 @@ mod procedure;
|
||||
mod refresh;
|
||||
mod repo;
|
||||
mod server;
|
||||
mod server_template;
|
||||
mod stack;
|
||||
mod sync;
|
||||
|
||||
@@ -773,9 +772,6 @@ fn resource_target<T: KomodoResource>(id: String) -> ResourceTarget {
|
||||
ResourceTargetVariant::Repo => ResourceTarget::Repo(id),
|
||||
ResourceTargetVariant::Alerter => ResourceTarget::Alerter(id),
|
||||
ResourceTargetVariant::Procedure => ResourceTarget::Procedure(id),
|
||||
ResourceTargetVariant::ServerTemplate => {
|
||||
ResourceTarget::ServerTemplate(id)
|
||||
}
|
||||
ResourceTargetVariant::ResourceSync => {
|
||||
ResourceTarget::ResourceSync(id)
|
||||
}
|
||||
@@ -1020,9 +1016,6 @@ where
|
||||
ResourceTarget::Stack(id) => ("recents.Stack", id),
|
||||
ResourceTarget::Builder(id) => ("recents.Builder", id),
|
||||
ResourceTarget::Alerter(id) => ("recents.Alerter", id),
|
||||
ResourceTarget::ServerTemplate(id) => {
|
||||
("recents.ServerTemplate", id)
|
||||
}
|
||||
ResourceTarget::ResourceSync(id) => ("recents.ResourceSync", id),
|
||||
ResourceTarget::System(_) => return,
|
||||
};
|
||||
|
||||
@@ -641,6 +641,13 @@ async fn validate_config(
|
||||
.await?;
|
||||
params.stack = stack.id;
|
||||
}
|
||||
Execution::BatchPullStack(_params) => {
|
||||
if !user.admin {
|
||||
return Err(anyhow!(
|
||||
"Non admin user cannot configure Batch executions"
|
||||
));
|
||||
}
|
||||
}
|
||||
Execution::StartStack(params) => {
|
||||
let stack = super::get_check_permissions::<Stack>(
|
||||
¶ms.stack,
|
||||
|
||||
@@ -13,6 +13,7 @@ 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},
|
||||
};
|
||||
@@ -42,6 +43,11 @@ 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, container_exec_disabled) =
|
||||
get_system_info(&server)
|
||||
.await
|
||||
.map(|i| (i.terminals_disabled, i.container_exec_disabled))
|
||||
.unwrap_or((true, true));
|
||||
ServerListItem {
|
||||
name: server.name,
|
||||
id: server.id,
|
||||
@@ -57,6 +63,8 @@ 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,
|
||||
container_exec_disabled,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
use komodo_client::entities::{
|
||||
MergePartial, Operation, ResourceTarget, ResourceTargetVariant,
|
||||
resource::Resource,
|
||||
server_template::{
|
||||
PartialServerTemplateConfig, ServerTemplate,
|
||||
ServerTemplateConfig, ServerTemplateConfigDiff,
|
||||
ServerTemplateConfigVariant, ServerTemplateListItem,
|
||||
ServerTemplateListItemInfo, ServerTemplateQuerySpecifics,
|
||||
},
|
||||
update::Update,
|
||||
user::User,
|
||||
};
|
||||
use mungos::mongodb::{
|
||||
Collection,
|
||||
bson::{Document, to_document},
|
||||
};
|
||||
|
||||
use crate::state::db_client;
|
||||
|
||||
impl super::KomodoResource for ServerTemplate {
|
||||
type Config = ServerTemplateConfig;
|
||||
type PartialConfig = PartialServerTemplateConfig;
|
||||
type ConfigDiff = ServerTemplateConfigDiff;
|
||||
type Info = ();
|
||||
type ListItem = ServerTemplateListItem;
|
||||
type QuerySpecifics = ServerTemplateQuerySpecifics;
|
||||
|
||||
fn resource_type() -> ResourceTargetVariant {
|
||||
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
|
||||
}
|
||||
|
||||
async fn to_list_item(
|
||||
server_template: Resource<Self::Config, Self::Info>,
|
||||
) -> Self::ListItem {
|
||||
let (template_type, instance_type) = match server_template.config
|
||||
{
|
||||
ServerTemplateConfig::Aws(config) => (
|
||||
ServerTemplateConfigVariant::Aws.to_string(),
|
||||
Some(config.instance_type),
|
||||
),
|
||||
ServerTemplateConfig::Hetzner(config) => (
|
||||
ServerTemplateConfigVariant::Hetzner.to_string(),
|
||||
Some(config.server_type.as_ref().to_string()),
|
||||
),
|
||||
};
|
||||
ServerTemplateListItem {
|
||||
name: server_template.name,
|
||||
id: server_template.id,
|
||||
tags: server_template.tags,
|
||||
resource_type: ResourceTargetVariant::ServerTemplate,
|
||||
info: ServerTemplateListItemInfo {
|
||||
provider: template_type.to_string(),
|
||||
instance_type,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn busy(_id: &String) -> anyhow::Result<bool> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
// CREATE
|
||||
|
||||
fn create_operation() -> Operation {
|
||||
Operation::CreateServerTemplate
|
||||
}
|
||||
|
||||
fn user_can_create(user: &User) -> bool {
|
||||
user.admin
|
||||
}
|
||||
|
||||
async fn validate_create_config(
|
||||
_config: &mut Self::PartialConfig,
|
||||
_user: &User,
|
||||
) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn post_create(
|
||||
_created: &Resource<Self::Config, Self::Info>,
|
||||
_update: &mut Update,
|
||||
) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// UPDATE
|
||||
|
||||
fn update_operation() -> Operation {
|
||||
Operation::UpdateServerTemplate
|
||||
}
|
||||
|
||||
async fn validate_update_config(
|
||||
_id: &str,
|
||||
_config: &mut Self::PartialConfig,
|
||||
_user: &User,
|
||||
) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_document(
|
||||
original: Resource<Self::Config, Self::Info>,
|
||||
config: Self::PartialConfig,
|
||||
) -> Result<Document, mungos::mongodb::bson::ser::Error> {
|
||||
let config = original.config.merge_partial(config);
|
||||
to_document(&config)
|
||||
}
|
||||
|
||||
async fn post_update(
|
||||
_updated: &Self,
|
||||
_update: &mut Update,
|
||||
) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// RENAME
|
||||
|
||||
fn rename_operation() -> Operation {
|
||||
Operation::RenameServerTemplate
|
||||
}
|
||||
|
||||
// DELETE
|
||||
|
||||
fn delete_operation() -> Operation {
|
||||
Operation::DeleteServerTemplate
|
||||
}
|
||||
|
||||
async fn pre_delete(
|
||||
_resource: &Resource<Self::Config, Self::Info>,
|
||||
_update: &mut Update,
|
||||
) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn post_delete(
|
||||
_resource: &Resource<Self::Config, Self::Info>,
|
||||
_update: &mut Update,
|
||||
) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use futures::future::join_all;
|
||||
use komodo_client::{
|
||||
api::write::{CreateBuilder, CreateServer},
|
||||
entities::{
|
||||
ResourceTarget,
|
||||
builder::{PartialBuilderConfig, PartialServerBuilderConfig},
|
||||
komodo_timestamp,
|
||||
server::{PartialServerConfig, Server},
|
||||
sync::ResourceSync,
|
||||
update::Log,
|
||||
user::system_user,
|
||||
},
|
||||
};
|
||||
use mungos::{
|
||||
find::find_collect,
|
||||
mongodb::bson::{Document, doc, oid::ObjectId, to_document},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
api::write::WriteArgs, config::core_config, helpers::random_string,
|
||||
resource, state::db_client,
|
||||
};
|
||||
|
||||
/// This function should be run on startup,
|
||||
/// after the db client has been initialized
|
||||
pub async fn on_startup() {
|
||||
tokio::join!(
|
||||
in_progress_update_cleanup(),
|
||||
open_alert_cleanup(),
|
||||
ensure_first_server_and_builder(),
|
||||
clean_up_server_templates(),
|
||||
);
|
||||
}
|
||||
|
||||
async fn in_progress_update_cleanup() {
|
||||
let log = Log::error(
|
||||
"Komodo shutdown",
|
||||
String::from(
|
||||
"Komodo shutdown during execution. If this is a build, the builder may not have been terminated.",
|
||||
),
|
||||
);
|
||||
// This static log won't fail to serialize, unwrap ok.
|
||||
let log = to_document(&log).unwrap();
|
||||
if let Err(e) = db_client()
|
||||
.updates
|
||||
.update_many(
|
||||
doc! { "status": "InProgress" },
|
||||
doc! {
|
||||
"$set": {
|
||||
"status": "Complete",
|
||||
"success": false,
|
||||
},
|
||||
"$push": {
|
||||
"logs": log
|
||||
}
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("failed to cleanup in progress updates on startup | {e:#}")
|
||||
}
|
||||
}
|
||||
|
||||
/// Run on startup, ensure open alerts pointing to invalid resources are closed.
|
||||
async fn open_alert_cleanup() {
|
||||
let db = db_client();
|
||||
let Ok(alerts) =
|
||||
find_collect(&db.alerts, doc! { "resolved": false }, None)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
error!(
|
||||
"failed to list all alerts for startup open alert cleanup | {e:?}"
|
||||
)
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let futures = alerts.into_iter().map(|alert| async move {
|
||||
match alert.target {
|
||||
ResourceTarget::Server(id) => {
|
||||
resource::get::<Server>(&id)
|
||||
.await
|
||||
.is_err()
|
||||
.then(|| ObjectId::from_str(&alert.id).inspect_err(|e| warn!("failed to clean up alert - id is invalid ObjectId | {e:?}")).ok()).flatten()
|
||||
}
|
||||
ResourceTarget::ResourceSync(id) => {
|
||||
resource::get::<ResourceSync>(&id)
|
||||
.await
|
||||
.is_err()
|
||||
.then(|| ObjectId::from_str(&alert.id).inspect_err(|e| warn!("failed to clean up alert - id is invalid ObjectId | {e:?}")).ok()).flatten()
|
||||
}
|
||||
// No other resources should have open alerts.
|
||||
_ => ObjectId::from_str(&alert.id).inspect_err(|e| warn!("failed to clean up alert - id is invalid ObjectId | {e:?}")).ok(),
|
||||
}
|
||||
});
|
||||
let to_update_ids = join_all(futures)
|
||||
.await
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
if let Err(e) = db
|
||||
.alerts
|
||||
.update_many(
|
||||
doc! { "_id": { "$in": to_update_ids } },
|
||||
doc! { "$set": {
|
||||
"resolved": true,
|
||||
"resolved_ts": komodo_timestamp()
|
||||
} },
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!(
|
||||
"failed to clean up invalid open alerts on startup | {e:#}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensures a default server / builder exists with the defined address
|
||||
async fn ensure_first_server_and_builder() {
|
||||
let first_server = &core_config().first_server;
|
||||
if first_server.is_empty() {
|
||||
return;
|
||||
}
|
||||
let db = db_client();
|
||||
let Ok(server) = db
|
||||
.servers
|
||||
.find_one(Document::new())
|
||||
.await
|
||||
.inspect_err(|e| error!("Failed to initialize 'first_server'. Failed to query db. {e:?}"))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let server = if let Some(server) = server {
|
||||
server
|
||||
} else {
|
||||
match (CreateServer {
|
||||
name: format!("server-{}", random_string(5)),
|
||||
config: PartialServerConfig {
|
||||
address: Some(first_server.to_string()),
|
||||
enabled: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
})
|
||||
.resolve(&WriteArgs {
|
||||
user: system_user().to_owned(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(server) => server,
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Failed to initialize 'first_server'. Failed to CreateServer. {:#}",
|
||||
e.error
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
let Ok(None) = db.builders
|
||||
.find_one(Document::new()).await
|
||||
.inspect_err(|e| error!("Failed to initialize 'first_builder' | Failed to query db | {e:?}")) else {
|
||||
return;
|
||||
};
|
||||
if let Err(e) = (CreateBuilder {
|
||||
name: String::from("local"),
|
||||
config: PartialBuilderConfig::Server(
|
||||
PartialServerBuilderConfig {
|
||||
server_id: Some(server.id),
|
||||
},
|
||||
),
|
||||
})
|
||||
.resolve(&WriteArgs {
|
||||
user: system_user().to_owned(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
error!(
|
||||
"Failed to initialize 'first_builder' | Failed to CreateBuilder | {:#}",
|
||||
e.error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// v1.17.5 removes the ServerTemplate resource.
|
||||
/// References to this resource type need to be cleaned up
|
||||
/// to avoid type errors reading from the database.
|
||||
async fn clean_up_server_templates() {
|
||||
let db = db_client();
|
||||
tokio::join!(
|
||||
async {
|
||||
db.permissions
|
||||
.delete_many(doc! {
|
||||
"resource_target.type": "ServerTemplate",
|
||||
})
|
||||
.await
|
||||
.expect(
|
||||
"Failed to clean up server template permissions on db",
|
||||
);
|
||||
},
|
||||
async {
|
||||
db.updates
|
||||
.delete_many(doc! { "target.type": "ServerTemplate" })
|
||||
.await
|
||||
.expect("Failed to clean up server template updates on db");
|
||||
},
|
||||
async {
|
||||
db.users
|
||||
.update_many(
|
||||
Document::new(),
|
||||
doc! { "$unset": { "recents.ServerTemplate": 1, "all.ServerTemplate": 1 } }
|
||||
)
|
||||
.await
|
||||
.expect("Failed to clean up server template updates on db");
|
||||
},
|
||||
async {
|
||||
db.user_groups
|
||||
.update_many(
|
||||
Document::new(),
|
||||
doc! { "$unset": { "all.ServerTemplate": 1 } },
|
||||
)
|
||||
.await
|
||||
.expect("Failed to clean up server template updates on db");
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -270,9 +270,6 @@ pub fn extend_resources(
|
||||
resources
|
||||
.builders
|
||||
.extend(filter_by_tag(more.builders, match_tags));
|
||||
resources
|
||||
.server_templates
|
||||
.extend(filter_by_tag(more.server_templates, match_tags));
|
||||
resources
|
||||
.resource_syncs
|
||||
.extend(filter_by_tag(more.resource_syncs, match_tags));
|
||||
|
||||
@@ -11,7 +11,6 @@ use komodo_client::entities::{
|
||||
procedure::Procedure,
|
||||
repo::Repo,
|
||||
server::Server,
|
||||
server_template::ServerTemplate,
|
||||
stack::Stack,
|
||||
sync::ResourceSync,
|
||||
tag::Tag,
|
||||
@@ -166,7 +165,6 @@ pub struct AllResourcesById {
|
||||
pub actions: HashMap<String, Action>,
|
||||
pub builders: HashMap<String, Builder>,
|
||||
pub alerters: HashMap<String, Alerter>,
|
||||
pub templates: HashMap<String, ServerTemplate>,
|
||||
pub syncs: HashMap<String, ResourceSync>,
|
||||
}
|
||||
|
||||
@@ -210,10 +208,6 @@ impl AllResourcesById {
|
||||
id_to_tags, match_tags,
|
||||
)
|
||||
.await?,
|
||||
templates: crate::resource::get_id_to_resource_map::<
|
||||
ServerTemplate,
|
||||
>(id_to_tags, match_tags)
|
||||
.await?,
|
||||
syncs: crate::resource::get_id_to_resource_map::<ResourceSync>(
|
||||
id_to_tags, match_tags,
|
||||
)
|
||||
|
||||
@@ -13,7 +13,6 @@ use komodo_client::{
|
||||
procedure::Procedure,
|
||||
repo::Repo,
|
||||
server::Server,
|
||||
server_template::ServerTemplate,
|
||||
stack::Stack,
|
||||
sync::ResourceSync,
|
||||
tag::Tag,
|
||||
@@ -191,18 +190,6 @@ impl ResourceSyncTrait for Builder {
|
||||
|
||||
impl ExecuteResourceSync for Builder {}
|
||||
|
||||
impl ResourceSyncTrait for ServerTemplate {
|
||||
fn get_diff(
|
||||
original: Self::Config,
|
||||
update: Self::PartialConfig,
|
||||
_resources: &AllResourcesById,
|
||||
) -> anyhow::Result<Self::ConfigDiff> {
|
||||
Ok(original.partial_diff(update))
|
||||
}
|
||||
}
|
||||
|
||||
impl ExecuteResourceSync for ServerTemplate {}
|
||||
|
||||
impl ResourceSyncTrait for Action {
|
||||
fn get_diff(
|
||||
original: Self::Config,
|
||||
@@ -614,6 +601,7 @@ impl ResourceSyncTrait for Procedure {
|
||||
.map(|s| s.name.clone())
|
||||
.unwrap_or_default();
|
||||
}
|
||||
Execution::BatchPullStack(_config) => {}
|
||||
Execution::StartStack(config) => {
|
||||
config.stack = resources
|
||||
.stacks
|
||||
|
||||
@@ -13,7 +13,6 @@ use komodo_client::{
|
||||
repo::Repo,
|
||||
resource::Resource,
|
||||
server::Server,
|
||||
server_template::{PartialServerTemplateConfig, ServerTemplate},
|
||||
stack::Stack,
|
||||
sync::ResourceSync,
|
||||
tag::Tag,
|
||||
@@ -349,25 +348,6 @@ impl ToToml for Repo {
|
||||
}
|
||||
}
|
||||
|
||||
impl ToToml for ServerTemplate {
|
||||
fn push_additional(
|
||||
resource: ResourceToml<Self::PartialConfig>,
|
||||
toml: &mut String,
|
||||
) {
|
||||
let empty_params = match resource.config {
|
||||
PartialServerTemplateConfig::Aws(config) => config.is_none(),
|
||||
PartialServerTemplateConfig::Hetzner(config) => {
|
||||
config.is_none()
|
||||
}
|
||||
};
|
||||
if empty_params {
|
||||
// toml_pretty will remove empty map
|
||||
// but in this case its needed to deserialize the enums.
|
||||
toml.push_str("\nparams = {}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToToml for Builder {
|
||||
fn replace_ids(
|
||||
resource: &mut Resource<Self::Config, Self::Info>,
|
||||
@@ -747,6 +727,7 @@ impl ToToml for Procedure {
|
||||
.map(|r| &r.name)
|
||||
.unwrap_or(&String::new()),
|
||||
),
|
||||
Execution::BatchPullStack(_exec) => {}
|
||||
Execution::StartStack(exec) => exec.stack.clone_from(
|
||||
all
|
||||
.stacks
|
||||
|
||||
@@ -285,13 +285,6 @@ pub async fn get_updates_for_execution(
|
||||
.map(|b| b.name.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
ResourceTarget::ServerTemplate(id) => {
|
||||
*id = all_resources
|
||||
.templates
|
||||
.get(id)
|
||||
.map(|b| b.name.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
ResourceTarget::ResourceSync(id) => {
|
||||
*id = all_resources
|
||||
.syncs
|
||||
@@ -737,19 +730,6 @@ async fn expand_user_group_permissions(
|
||||
});
|
||||
expanded.extend(permissions);
|
||||
}
|
||||
ResourceTargetVariant::ServerTemplate => {
|
||||
let permissions = all_resources
|
||||
.templates
|
||||
.values()
|
||||
.filter(|resource| regex.is_match(&resource.name))
|
||||
.map(|resource| PermissionToml {
|
||||
target: ResourceTarget::ServerTemplate(
|
||||
resource.name.clone(),
|
||||
),
|
||||
level: permission.level,
|
||||
});
|
||||
expanded.extend(permissions);
|
||||
}
|
||||
ResourceTargetVariant::ResourceSync => {
|
||||
let permissions = all_resources
|
||||
.syncs
|
||||
@@ -903,13 +883,6 @@ pub async fn convert_user_groups(
|
||||
.map(|r| r.name.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
ResourceTarget::ServerTemplate(id) => {
|
||||
*id = all
|
||||
.templates
|
||||
.get(id)
|
||||
.map(|r| r.name.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
ResourceTarget::ResourceSync(id) => {
|
||||
*id = all
|
||||
.syncs
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
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:?}"
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
use axum::{
|
||||
extract::{Query, WebSocketUpgrade, ws::Message},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use futures::SinkExt;
|
||||
use komodo_client::{
|
||||
api::terminal::ConnectContainerExecQuery,
|
||||
entities::{permission::PermissionLevel, server::Server},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
helpers::periphery_client, resource, ws::core_periphery_forward_ws,
|
||||
};
|
||||
|
||||
#[instrument(name = "ConnectContainerExec", skip(ws))]
|
||||
pub async fn handler(
|
||||
Query(ConnectContainerExecQuery {
|
||||
server,
|
||||
container,
|
||||
shell,
|
||||
}): Query<ConnectContainerExecQuery>,
|
||||
ws: WebSocketUpgrade,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(|socket| async move {
|
||||
let Some((mut client_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 _ =
|
||||
client_socket.send(Message::text(format!("ERROR: {e:#}"))).await;
|
||||
let _ = client_socket.close().await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let periphery = match periphery_client(&server) {
|
||||
Ok(periphery) => periphery,
|
||||
Err(e) => {
|
||||
debug!("couldn't get periphery | {e:#}");
|
||||
let _ =
|
||||
client_socket.send(Message::text(format!("ERROR: {e:#}"))).await;
|
||||
let _ = client_socket.close().await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
trace!("connecting to periphery container exec websocket");
|
||||
|
||||
let periphery_socket = match periphery
|
||||
.connect_container_exec(
|
||||
container,
|
||||
shell
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(ws) => ws,
|
||||
Err(e) => {
|
||||
debug!("Failed connect to periphery container exec websocket | {e:#}");
|
||||
let _ =
|
||||
client_socket.send(Message::text(format!("ERROR: {e:#}"))).await;
|
||||
let _ = client_socket.close().await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
trace!("connected to periphery container exec websocket");
|
||||
|
||||
core_periphery_forward_ws(client_socket, periphery_socket).await
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
use crate::{
|
||||
auth::{auth_api_key_check_enabled, auth_jwt_check_enabled},
|
||||
helpers::query::get_user,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use axum::{
|
||||
Router,
|
||||
extract::ws::{CloseFrame, Message, Utf8Bytes, WebSocket},
|
||||
routing::get,
|
||||
};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use komodo_client::{entities::user::User, ws::WsLoginMessage};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_tungstenite::{
|
||||
MaybeTlsStream, WebSocketStream, tungstenite,
|
||||
};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
mod container;
|
||||
mod terminal;
|
||||
mod update;
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/update", get(update::handler))
|
||||
.route("/terminal", get(terminal::handler))
|
||||
.route("/container", get(container::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)
|
||||
}
|
||||
|
||||
async fn core_periphery_forward_ws(
|
||||
client_socket: axum::extract::ws::WebSocket,
|
||||
periphery_socket: WebSocketStream<MaybeTlsStream<TcpStream>>,
|
||||
) {
|
||||
let (mut periphery_send, mut periphery_receive) =
|
||||
periphery_socket.split();
|
||||
let (mut core_send, mut core_receive) = client_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 | {e:?}",
|
||||
);
|
||||
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(
|
||||
// TODO: improve this conversion cost from axum ws library
|
||||
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!()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
use axum::{
|
||||
extract::{Query, WebSocketUpgrade, ws::Message},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use futures::SinkExt;
|
||||
use komodo_client::{
|
||||
api::terminal::ConnectTerminalQuery,
|
||||
entities::{permission::PermissionLevel, server::Server},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
helpers::periphery_client, resource, ws::core_periphery_forward_ws,
|
||||
};
|
||||
|
||||
#[instrument(name = "ConnectTerminal", skip(ws))]
|
||||
pub async fn handler(
|
||||
Query(ConnectTerminalQuery { server, terminal }): Query<
|
||||
ConnectTerminalQuery,
|
||||
>,
|
||||
ws: WebSocketUpgrade,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(|socket| async move {
|
||||
let Some((mut client_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 _ = client_socket
|
||||
.send(Message::text(format!("ERROR: {e:#}")))
|
||||
.await;
|
||||
let _ = client_socket.close().await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let periphery = match periphery_client(&server) {
|
||||
Ok(periphery) => periphery,
|
||||
Err(e) => {
|
||||
debug!("couldn't get periphery | {e:#}");
|
||||
let _ = client_socket
|
||||
.send(Message::text(format!("ERROR: {e:#}")))
|
||||
.await;
|
||||
let _ = client_socket.close().await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
trace!("connecting to periphery terminal websocket");
|
||||
|
||||
let periphery_socket =
|
||||
match periphery.connect_terminal(terminal).await {
|
||||
Ok(ws) => ws,
|
||||
Err(e) => {
|
||||
debug!("Failed connect to periphery terminal | {e:#}");
|
||||
let _ = client_socket
|
||||
.send(Message::text(format!("ERROR: {e:#}")))
|
||||
.await;
|
||||
let _ = client_socket.close().await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
trace!("connected to periphery terminal websocket");
|
||||
|
||||
core_periphery_forward_ws(client_socket, periphery_socket).await
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
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:?}"
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
## 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,9 +33,13 @@ resolver_api.workspace = true
|
||||
run_command.workspace = true
|
||||
svi.workspace = true
|
||||
# external
|
||||
pin-project-lite.workspace = true
|
||||
tokio-stream.workspace = true
|
||||
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
|
||||
@@ -45,7 +49,9 @@ 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
|
||||
|
||||
@@ -15,6 +15,7 @@ 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,6 +1,10 @@
|
||||
#!/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
|
||||
@@ -15,4 +19,10 @@ 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/*
|
||||
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
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ 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
|
||||
|
||||
@@ -248,7 +248,7 @@ impl Resolve<super::Args> for build::Build {
|
||||
} {
|
||||
let success = log.success;
|
||||
logs.push(log);
|
||||
if success {
|
||||
if !success {
|
||||
return Ok(logs);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,11 +8,8 @@ use komodo_client::entities::{
|
||||
update::Log,
|
||||
};
|
||||
use periphery_client::api::{
|
||||
GetDockerLists, GetDockerListsResponse, GetHealth,
|
||||
GetHealthResponse, GetVersion, GetVersionResponse,
|
||||
ListDockerRegistries, ListGitProviders, ListSecrets, PruneSystem,
|
||||
RunCommand, build::*, compose::*, container::*, git::*, image::*,
|
||||
network::*, stats::*, volume::*,
|
||||
build::*, compose::*, container::*, git::*, image::*, network::*,
|
||||
stats::*, terminal::*, volume::*, *,
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use response::Response;
|
||||
@@ -27,9 +24,13 @@ mod deploy;
|
||||
mod git;
|
||||
mod image;
|
||||
mod network;
|
||||
mod router;
|
||||
mod stats;
|
||||
mod terminal;
|
||||
mod volume;
|
||||
|
||||
pub use router::router;
|
||||
|
||||
pub struct Args;
|
||||
|
||||
#[derive(
|
||||
@@ -136,6 +137,13 @@ pub enum PeripheryRequest {
|
||||
|
||||
// All in one (Write)
|
||||
PruneSystem(PruneSystem),
|
||||
|
||||
// Terminal
|
||||
ListTerminals(ListTerminals),
|
||||
CreateTerminal(CreateTerminal),
|
||||
DeleteTerminal(DeleteTerminal),
|
||||
DeleteAllTerminals(DeleteAllTerminals),
|
||||
CreateTerminalAuthToken(CreateTerminalAuthToken),
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -8,7 +8,7 @@ use axum::{
|
||||
http::{Request, StatusCode},
|
||||
middleware::{self, Next},
|
||||
response::Response,
|
||||
routing::post,
|
||||
routing::{get, post},
|
||||
};
|
||||
use derive_variants::ExtractVariant;
|
||||
use resolver_api::Resolve;
|
||||
@@ -19,9 +19,27 @@ use crate::config::periphery_config;
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/", post(handler))
|
||||
.merge(
|
||||
Router::new()
|
||||
.route("/", post(handler))
|
||||
.layer(middleware::from_fn(guard_request_by_passkey)),
|
||||
)
|
||||
.nest(
|
||||
"/terminal",
|
||||
Router::new()
|
||||
.route("/", get(super::terminal::connect_terminal))
|
||||
.route(
|
||||
"/container",
|
||||
get(super::terminal::connect_container_exec),
|
||||
)
|
||||
.nest(
|
||||
"/execute",
|
||||
Router::new()
|
||||
.route("/", post(super::terminal::execute_terminal))
|
||||
.layer(middleware::from_fn(guard_request_by_passkey)),
|
||||
),
|
||||
)
|
||||
.layer(middleware::from_fn(guard_request_by_ip))
|
||||
.layer(middleware::from_fn(guard_request_by_passkey))
|
||||
}
|
||||
|
||||
async fn handler(
|
||||
@@ -0,0 +1,441 @@
|
||||
use std::{collections::HashMap, sync::OnceLock, task::Poll};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use axum::{
|
||||
extract::{
|
||||
Query, WebSocketUpgrade,
|
||||
ws::{Message, Utf8Bytes},
|
||||
},
|
||||
http::StatusCode,
|
||||
response::Response,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use futures::{SinkExt, Stream, StreamExt, TryStreamExt};
|
||||
use komodo_client::{
|
||||
api::write::TerminalRecreateMode,
|
||||
entities::{
|
||||
KOMODO_EXIT_CODE, NoData, komodo_timestamp, server::TerminalInfo,
|
||||
},
|
||||
};
|
||||
use periphery_client::api::terminal::*;
|
||||
use pin_project_lite::pin_project;
|
||||
use rand::Rng;
|
||||
use resolver_api::Resolve;
|
||||
use serror::{AddStatusCodeError, Json};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::{config::periphery_config, terminal::*};
|
||||
|
||||
impl Resolve<super::Args> for ListTerminals {
|
||||
#[instrument(name = "ListTerminals", level = "debug")]
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &super::Args,
|
||||
) -> serror::Result<Vec<TerminalInfo>> {
|
||||
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> {
|
||||
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> {
|
||||
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> {
|
||||
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 mut lock = self.map.lock().unwrap();
|
||||
// clear out any old tokens here (prevent unbounded growth)
|
||||
let ts = komodo_timestamp();
|
||||
lock.retain(|_, valid_until| *valid_until > ts);
|
||||
let token: String = rand::rng()
|
||||
.sample_iter(&rand::distr::Alphanumeric)
|
||||
.take(30)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
lock.insert(token.clone(), ts + 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(query): 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),
|
||||
);
|
||||
}
|
||||
handle_terminal_websocket(query, ws).await
|
||||
}
|
||||
|
||||
pub async fn connect_container_exec(
|
||||
Query(ConnectContainerExecQuery {
|
||||
token,
|
||||
container,
|
||||
shell,
|
||||
}): Query<ConnectContainerExecQuery>,
|
||||
ws: WebSocketUpgrade,
|
||||
) -> serror::Result<Response> {
|
||||
if periphery_config().disable_container_exec {
|
||||
return Err(
|
||||
anyhow!("Container exec is disabled in the periphery config")
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
if container.contains("&&") || shell.contains("&&") {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"The use of '&&' is forbidden in the container name or shell"
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
// Create (recreate if shell changed)
|
||||
create_terminal(
|
||||
container.clone(),
|
||||
format!("docker exec -it {container} {shell}"),
|
||||
TerminalRecreateMode::DifferentCommand,
|
||||
)
|
||||
.await
|
||||
.context("Failed to create terminal for container exec")?;
|
||||
|
||||
handle_terminal_websocket(
|
||||
ConnectTerminalQuery {
|
||||
token,
|
||||
terminal: container,
|
||||
},
|
||||
ws,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn handle_terminal_websocket(
|
||||
ConnectTerminalQuery { token, terminal }: ConnectTerminalQuery,
|
||||
ws: WebSocketUpgrade,
|
||||
) -> serror::Result<Response> {
|
||||
// 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")?;
|
||||
}
|
||||
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;
|
||||
}))
|
||||
}
|
||||
|
||||
/// Sentinels
|
||||
const START_OF_OUTPUT: &str = "__KOMODO_START_OF_OUTPUT__";
|
||||
const END_OF_OUTPUT: &str = "__KOMODO_END_OF_OUTPUT__";
|
||||
|
||||
pub async fn execute_terminal(
|
||||
Json(ExecuteTerminalBody { terminal, command }): Json<
|
||||
ExecuteTerminalBody,
|
||||
>,
|
||||
) -> serror::Result<axum::body::Body> {
|
||||
if periphery_config().disable_terminals {
|
||||
return Err(
|
||||
anyhow!("Terminals are disabled in the periphery config")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
let terminal = get_terminal(&terminal).await?;
|
||||
|
||||
// Read the bytes into lines
|
||||
// This is done to check the lines for the EOF sentinal
|
||||
let mut stdout = tokio_util::codec::FramedRead::new(
|
||||
tokio_util::io::StreamReader::new(
|
||||
tokio_stream::wrappers::BroadcastStream::new(
|
||||
terminal.stdout.resubscribe(),
|
||||
)
|
||||
.map(|res| res.map_err(std::io::Error::other)),
|
||||
),
|
||||
tokio_util::codec::LinesCodec::new(),
|
||||
);
|
||||
|
||||
let full_command = format!(
|
||||
"printf '\n{START_OF_OUTPUT}\n\n'; {command}; rc=$? printf '\n{KOMODO_EXIT_CODE}%d\n{END_OF_OUTPUT}\n' \"$rc\"\n"
|
||||
);
|
||||
|
||||
terminal
|
||||
.stdin
|
||||
.send(StdinMsg::Bytes(Bytes::from(full_command)))
|
||||
.await
|
||||
.context("Failed to send command to terminal stdin")?;
|
||||
|
||||
// Only start the response AFTER the start sentinel is printed
|
||||
loop {
|
||||
match stdout
|
||||
.try_next()
|
||||
.await
|
||||
.context("Failed to read stdout line")?
|
||||
{
|
||||
Some(line) if line == START_OF_OUTPUT => break,
|
||||
// Keep looping until the start sentinel received.
|
||||
Some(_) => {}
|
||||
None => {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"Stdout stream terminated before start sentinel received"
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(axum::body::Body::from_stream(TerminalStream { stdout }))
|
||||
}
|
||||
|
||||
pin_project! {
|
||||
struct TerminalStream<S> { #[pin] stdout: S }
|
||||
}
|
||||
|
||||
impl<S> Stream for TerminalStream<S>
|
||||
where
|
||||
S:
|
||||
Stream<Item = Result<String, tokio_util::codec::LinesCodecError>>,
|
||||
{
|
||||
// Axum expects a stream of results
|
||||
type Item = Result<String, String>;
|
||||
|
||||
fn poll_next(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Option<Self::Item>> {
|
||||
let this = self.project();
|
||||
match this.stdout.poll_next(cx) {
|
||||
Poll::Ready(None) => {
|
||||
// This is if a None comes in before END_OF_OUTPUT.
|
||||
// This probably means the terminal has exited early,
|
||||
// and needs to be cleaned up
|
||||
tokio::spawn(async move { clean_up_terminals().await });
|
||||
Poll::Ready(None)
|
||||
}
|
||||
Poll::Ready(Some(line)) => {
|
||||
match line {
|
||||
Ok(line) if line.as_str() == END_OF_OUTPUT => {
|
||||
// Stop the stream on end sentinel
|
||||
Poll::Ready(None)
|
||||
}
|
||||
Ok(line) => Poll::Ready(Some(Ok(line + "\n"))),
|
||||
Err(e) => Poll::Ready(Some(Err(format!("{e:?}")))),
|
||||
}
|
||||
}
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,12 @@ pub fn periphery_config() -> &'static PeripheryConfig {
|
||||
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),
|
||||
disable_container_exec: env
|
||||
.periphery_disable_container_exec
|
||||
.unwrap_or(config.disable_container_exec),
|
||||
stats_polling_rate: env
|
||||
.periphery_stats_polling_rate
|
||||
.unwrap_or(config.stats_polling_rate),
|
||||
|
||||
@@ -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();
|
||||
@@ -35,8 +35,8 @@ async fn app() -> anyhow::Result<()> {
|
||||
let socket_addr = SocketAddr::from_str(&addr)
|
||||
.context("failed to parse listen address")?;
|
||||
|
||||
let app = router::router()
|
||||
.into_make_service_with_connect_info::<SocketAddr>();
|
||||
let app =
|
||||
api::router().into_make_service_with_connect_info::<SocketAddr>();
|
||||
|
||||
if config.ssl_enabled {
|
||||
info!("🔒 Periphery SSL Enabled");
|
||||
@@ -73,7 +73,10 @@ async fn main() -> anyhow::Result<()> {
|
||||
|
||||
tokio::select! {
|
||||
res = app => return res?,
|
||||
_ = term_signal.recv() => {},
|
||||
_ = term_signal.recv() => {
|
||||
info!("Exiting all active Terminals for shutdown");
|
||||
terminal::delete_all_terminals().await;
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -189,6 +189,7 @@ impl StatsClient {
|
||||
fn get_system_information(
|
||||
sys: &sysinfo::System,
|
||||
) -> SystemInformation {
|
||||
let config = periphery_config();
|
||||
SystemInformation {
|
||||
name: System::name(),
|
||||
os: System::long_os_version(),
|
||||
@@ -201,5 +202,7 @@ fn get_system_information(
|
||||
.next()
|
||||
.map(|cpu| cpu.brand().to_string())
|
||||
.unwrap_or_default(),
|
||||
terminals_disabled: config.disable_terminals,
|
||||
container_exec_disabled: config.disable_container_exec,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
## 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
|
||||
@@ -12,7 +12,6 @@ mod deployment;
|
||||
mod procedure;
|
||||
mod repo;
|
||||
mod server;
|
||||
mod server_template;
|
||||
mod stack;
|
||||
mod sync;
|
||||
|
||||
@@ -23,7 +22,6 @@ pub use deployment::*;
|
||||
pub use procedure::*;
|
||||
pub use repo::*;
|
||||
pub use server::*;
|
||||
pub use server_template::*;
|
||||
pub use stack::*;
|
||||
pub use sync::*;
|
||||
|
||||
@@ -128,6 +126,7 @@ pub enum Execution {
|
||||
DeployStackIfChanged(DeployStackIfChanged),
|
||||
BatchDeployStackIfChanged(BatchDeployStackIfChanged),
|
||||
PullStack(PullStack),
|
||||
BatchPullStack(BatchPullStack),
|
||||
StartStack(StartStack),
|
||||
RestartStack(RestartStack),
|
||||
PauseStack(PauseStack),
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
use derive_empty_traits::EmptyTraits;
|
||||
use resolver_api::Resolve;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typeshare::typeshare;
|
||||
|
||||
use crate::entities::update::Update;
|
||||
|
||||
use super::KomodoExecuteRequest;
|
||||
|
||||
/// Launch an EC2 instance with the specified config.
|
||||
/// Response: [Update].
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
|
||||
)]
|
||||
#[empty_traits(KomodoExecuteRequest)]
|
||||
#[response(Update)]
|
||||
#[error(serror::Error)]
|
||||
pub struct LaunchServer {
|
||||
/// The name of the created server.
|
||||
pub name: String,
|
||||
/// The server template used to define the config.
|
||||
pub server_template: String,
|
||||
}
|
||||
@@ -152,6 +152,37 @@ pub struct PullStack {
|
||||
|
||||
//
|
||||
|
||||
/// Pulls multiple Stacks in parallel that match pattern. Response: [BatchExecutionResponse].
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Debug,
|
||||
Clone,
|
||||
PartialEq,
|
||||
Resolve,
|
||||
EmptyTraits,
|
||||
Parser,
|
||||
)]
|
||||
#[empty_traits(KomodoExecuteRequest)]
|
||||
#[response(BatchExecutionResponse)]
|
||||
#[error(serror::Error)]
|
||||
pub struct BatchPullStack {
|
||||
/// Id or name or wildcard pattern or regex.
|
||||
/// Supports multiline and comma delineated combinations of the above.
|
||||
///
|
||||
/// Example:
|
||||
/// ```
|
||||
/// # match all foo-* stacks
|
||||
/// foo-*
|
||||
/// # add some more
|
||||
/// extra-stack-1, extra-stack-2
|
||||
/// ```
|
||||
pub pattern: String,
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
/// Starts the target stack. `docker compose start`. Response: [Update]
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
|
||||
@@ -69,5 +69,6 @@
|
||||
pub mod auth;
|
||||
pub mod execute;
|
||||
pub mod read;
|
||||
pub mod terminal;
|
||||
pub mod user;
|
||||
pub mod write;
|
||||
|
||||
@@ -14,7 +14,6 @@ mod procedure;
|
||||
mod provider;
|
||||
mod repo;
|
||||
mod server;
|
||||
mod server_template;
|
||||
mod stack;
|
||||
mod sync;
|
||||
mod tag;
|
||||
@@ -35,7 +34,6 @@ pub use procedure::*;
|
||||
pub use provider::*;
|
||||
pub use repo::*;
|
||||
pub use server::*;
|
||||
pub use server_template::*;
|
||||
pub use stack::*;
|
||||
pub use sync::*;
|
||||
pub use tag::*;
|
||||
@@ -106,6 +104,8 @@ pub struct GetCoreInfoResponse {
|
||||
pub disable_confirm_dialog: bool,
|
||||
/// The repo owners for which github webhook management api is available
|
||||
pub github_webhook_owners: Vec<String>,
|
||||
/// Whether to disable websocket automatic reconnect.
|
||||
pub disable_websocket_reconnect: bool,
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::entities::{
|
||||
},
|
||||
server::{
|
||||
Server, ServerActionState, ServerListItem, ServerQuery,
|
||||
ServerState,
|
||||
ServerState, TerminalInfo,
|
||||
},
|
||||
stack::ComposeProject,
|
||||
stats::{
|
||||
@@ -628,3 +628,27 @@ 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,86 +0,0 @@
|
||||
use derive_empty_traits::EmptyTraits;
|
||||
use resolver_api::Resolve;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typeshare::typeshare;
|
||||
|
||||
use crate::entities::server_template::{
|
||||
ServerTemplate, ServerTemplateListItem, ServerTemplateQuery,
|
||||
};
|
||||
|
||||
use super::KomodoReadRequest;
|
||||
|
||||
//
|
||||
|
||||
/// Get a specific server template by id or name. Response: [ServerTemplate].
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
|
||||
)]
|
||||
#[empty_traits(KomodoReadRequest)]
|
||||
#[response(GetServerTemplateResponse)]
|
||||
#[error(serror::Error)]
|
||||
pub struct GetServerTemplate {
|
||||
/// Id or name
|
||||
#[serde(alias = "id", alias = "name")]
|
||||
pub server_template: String,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
pub type GetServerTemplateResponse = ServerTemplate;
|
||||
|
||||
//
|
||||
|
||||
/// List server templates matching structured query. Response: [ListServerTemplatesResponse].
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,
|
||||
)]
|
||||
#[empty_traits(KomodoReadRequest)]
|
||||
#[response(ListServerTemplatesResponse)]
|
||||
#[error(serror::Error)]
|
||||
pub struct ListServerTemplates {
|
||||
#[serde(default)]
|
||||
pub query: ServerTemplateQuery,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
pub type ListServerTemplatesResponse = Vec<ServerTemplateListItem>;
|
||||
|
||||
//
|
||||
|
||||
/// List server templates matching structured query. Response: [ListFullServerTemplatesResponse].
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,
|
||||
)]
|
||||
#[empty_traits(KomodoReadRequest)]
|
||||
#[response(ListFullServerTemplatesResponse)]
|
||||
#[error(serror::Error)]
|
||||
pub struct ListFullServerTemplates {
|
||||
#[serde(default)]
|
||||
pub query: ServerTemplateQuery,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
pub type ListFullServerTemplatesResponse = Vec<ServerTemplate>;
|
||||
|
||||
//
|
||||
|
||||
/// Gets a summary of data relating to all server templates.
|
||||
/// Response: [GetServerTemplatesSummaryResponse].
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
|
||||
)]
|
||||
#[empty_traits(KomodoReadRequest)]
|
||||
#[response(GetServerTemplatesSummaryResponse)]
|
||||
#[error(serror::Error)]
|
||||
pub struct GetServerTemplatesSummary {}
|
||||
|
||||
/// Response for [GetServerTemplatesSummary].
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct GetServerTemplatesSummaryResponse {
|
||||
/// The total number of server templates.
|
||||
pub total: u32,
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
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 does not exist,
|
||||
/// the call will fail.
|
||||
/// Create a terminal using [CreateTerminal][super::write::server::CreateTerminal]
|
||||
pub terminal: String,
|
||||
}
|
||||
|
||||
/// Query to connect to a container exec session (interactive shell over websocket) on the given server.
|
||||
/// TODO: Document calling.
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ConnectContainerExecQuery {
|
||||
/// Server Id or name
|
||||
pub server: String,
|
||||
/// The container name
|
||||
pub container: String,
|
||||
/// The shell to connect to
|
||||
pub shell: String,
|
||||
}
|
||||
|
||||
/// Execute a terminal command on the given server.
|
||||
/// TODO: Document calling.
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ExecuteTerminalBody {
|
||||
/// Server Id or name
|
||||
pub server: String,
|
||||
/// The name of the terminal on the server to use to execute.
|
||||
/// If the terminal at name exists, it will be used to execute the command.
|
||||
/// Otherwise, a new terminal will be created for this command, which will
|
||||
/// persist until it exits or is deleted.
|
||||
pub terminal: String,
|
||||
/// The command to execute.
|
||||
pub command: String,
|
||||
}
|
||||
@@ -81,7 +81,7 @@ pub struct DeleteBuild {
|
||||
#[response(Build)]
|
||||
#[error(serror::Error)]
|
||||
pub struct UpdateBuild {
|
||||
/// The id of the build to update.
|
||||
/// The id or name of the build to update.
|
||||
pub id: String,
|
||||
/// The partial config update to apply.
|
||||
pub config: _PartialBuildConfig,
|
||||
|
||||
@@ -10,7 +10,6 @@ mod procedure;
|
||||
mod provider;
|
||||
mod repo;
|
||||
mod server;
|
||||
mod server_template;
|
||||
mod stack;
|
||||
mod sync;
|
||||
mod tags;
|
||||
@@ -30,7 +29,6 @@ pub use procedure::*;
|
||||
pub use provider::*;
|
||||
pub use repo::*;
|
||||
pub use server::*;
|
||||
pub use server_template::*;
|
||||
pub use stack::*;
|
||||
pub use sync::*;
|
||||
pub use tags::*;
|
||||
|
||||
@@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize};
|
||||
use typeshare::typeshare;
|
||||
|
||||
use crate::entities::{
|
||||
NoData,
|
||||
server::{_PartialServerConfig, Server},
|
||||
update::Update,
|
||||
};
|
||||
@@ -105,3 +106,83 @@ pub struct CreateNetwork {
|
||||
/// 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,
|
||||
}
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
use derive_empty_traits::EmptyTraits;
|
||||
use resolver_api::Resolve;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typeshare::typeshare;
|
||||
|
||||
use crate::entities::{
|
||||
server_template::{PartialServerTemplateConfig, ServerTemplate},
|
||||
update::Update,
|
||||
};
|
||||
|
||||
use super::KomodoWriteRequest;
|
||||
|
||||
//
|
||||
|
||||
/// Create a server template. Response: [ServerTemplate].
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
|
||||
)]
|
||||
#[empty_traits(KomodoWriteRequest)]
|
||||
#[response(ServerTemplate)]
|
||||
#[error(serror::Error)]
|
||||
pub struct CreateServerTemplate {
|
||||
/// The name given to newly created server template.
|
||||
pub name: String,
|
||||
/// Optional partial config to initialize the server template with.
|
||||
#[serde(default)]
|
||||
pub config: PartialServerTemplateConfig,
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
/// Creates a new server template with given `name` and the configuration
|
||||
/// of the server template at the given `id`. Response: [ServerTemplate]
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
|
||||
)]
|
||||
#[empty_traits(KomodoWriteRequest)]
|
||||
#[response(ServerTemplate)]
|
||||
#[error(serror::Error)]
|
||||
pub struct CopyServerTemplate {
|
||||
/// The name of the new server template.
|
||||
pub name: String,
|
||||
/// The id of the server template to copy.
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
/// Deletes the server template at the given id, and returns the deleted server template.
|
||||
/// Response: [ServerTemplate]
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
|
||||
)]
|
||||
#[empty_traits(KomodoWriteRequest)]
|
||||
#[response(ServerTemplate)]
|
||||
#[error(serror::Error)]
|
||||
pub struct DeleteServerTemplate {
|
||||
/// The id or name of the server template to delete.
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
/// Update the server template at the given id, and return the updated server template.
|
||||
/// Response: [ServerTemplate].
|
||||
///
|
||||
/// Note. This method updates only the fields which are set in the [PartialServerTemplateConfig],
|
||||
/// effectively merging diffs into the final document.
|
||||
/// This is helpful when multiple users are using
|
||||
/// the same resources concurrently by ensuring no unintentional
|
||||
/// field changes occur from out of date local state.
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
|
||||
)]
|
||||
#[empty_traits(KomodoWriteRequest)]
|
||||
#[response(ServerTemplate)]
|
||||
#[error(serror::Error)]
|
||||
pub struct UpdateServerTemplate {
|
||||
/// The id of the server template to update.
|
||||
pub id: String,
|
||||
/// The partial config update to apply.
|
||||
pub config: PartialServerTemplateConfig,
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
/// Rename the ServerTemplate at id to the given name.
|
||||
/// Response: [Update].
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
|
||||
)]
|
||||
#[empty_traits(KomodoWriteRequest)]
|
||||
#[response(Update)]
|
||||
#[error(serror::Error)]
|
||||
pub struct RenameServerTemplate {
|
||||
/// The id or name of the ServerTemplate to rename.
|
||||
pub id: String,
|
||||
/// The new name.
|
||||
pub name: String,
|
||||
}
|
||||
@@ -133,6 +133,12 @@ pub struct ActionConfig {
|
||||
#[builder(default)]
|
||||
pub webhook_secret: String,
|
||||
|
||||
/// Whether deno will be instructed to reload all dependencies,
|
||||
/// this can usually be kept false outside of development.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub reload_deno_deps: bool,
|
||||
|
||||
/// Typescript file contents using pre-initialized `komodo` client.
|
||||
/// Supports variable / secret interpolation.
|
||||
#[serde(default, deserialize_with = "file_contents_deserializer")]
|
||||
@@ -177,6 +183,7 @@ impl Default for ActionConfig {
|
||||
failure_alert: default_failure_alert(),
|
||||
webhook_enabled: default_webhook_enabled(),
|
||||
webhook_secret: Default::default(),
|
||||
reload_deno_deps: Default::default(),
|
||||
file_contents: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +106,8 @@ pub struct Env {
|
||||
pub komodo_disable_confirm_dialog: Option<bool>,
|
||||
/// Override `disable_non_admin_create`
|
||||
pub komodo_disable_non_admin_create: Option<bool>,
|
||||
/// Override `disable_websocket_reconnect`
|
||||
pub komodo_disable_websocket_reconnect: Option<bool>,
|
||||
|
||||
/// Override `local_auth`
|
||||
pub komodo_local_auth: Option<bool>,
|
||||
@@ -209,11 +211,6 @@ pub struct Env {
|
||||
/// Override `aws.secret_access_key` with file
|
||||
pub komodo_aws_secret_access_key_file: Option<PathBuf>,
|
||||
|
||||
/// Override `hetzner.token`
|
||||
pub komodo_hetzner_token: Option<String>,
|
||||
/// Override `hetzner.token` with file
|
||||
pub komodo_hetzner_token_file: Option<PathBuf>,
|
||||
|
||||
/// Override `ssl_enabled`.
|
||||
pub komodo_ssl_enabled: Option<bool>,
|
||||
/// Override `ssl_key_file`
|
||||
@@ -277,6 +274,10 @@ pub struct CoreConfig {
|
||||
#[serde(default)]
|
||||
pub disable_confirm_dialog: bool,
|
||||
|
||||
/// Disable the UI websocket from automatically reconnecting.
|
||||
#[serde(default)]
|
||||
pub disable_websocket_reconnect: bool,
|
||||
|
||||
/// If defined, ensure an enabled first server exists at this address.
|
||||
/// Example: `http://periphery:8120`
|
||||
#[serde(default)]
|
||||
@@ -456,10 +457,6 @@ pub struct CoreConfig {
|
||||
#[serde(default)]
|
||||
pub aws: AwsCredentials,
|
||||
|
||||
/// Configure Hetzner credentials to use with Hetzner builds / server launches.
|
||||
#[serde(default)]
|
||||
pub hetzner: HetznerCredentials,
|
||||
|
||||
// =================
|
||||
// = Git Providers =
|
||||
// =================
|
||||
@@ -601,6 +598,7 @@ impl CoreConfig {
|
||||
transparent_mode: config.transparent_mode,
|
||||
ui_write_disabled: config.ui_write_disabled,
|
||||
disable_confirm_dialog: config.disable_confirm_dialog,
|
||||
disable_websocket_reconnect: config.disable_websocket_reconnect,
|
||||
enable_new_users: config.enable_new_users,
|
||||
disable_user_registration: config.disable_user_registration,
|
||||
disable_non_admin_create: config.disable_non_admin_create,
|
||||
@@ -646,9 +644,6 @@ impl CoreConfig {
|
||||
&config.aws.secret_access_key,
|
||||
),
|
||||
},
|
||||
hetzner: HetznerCredentials {
|
||||
token: empty_or_redacted(&config.hetzner.token),
|
||||
},
|
||||
secrets: config
|
||||
.secrets
|
||||
.into_iter()
|
||||
@@ -760,12 +755,6 @@ pub struct AwsCredentials {
|
||||
pub secret_access_key: String,
|
||||
}
|
||||
|
||||
/// Provide Hetzner credentials for Komodo to use.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct HetznerCredentials {
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
/// Provide configuration for a Github Webhook app.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GithubWebhookAppConfig {
|
||||
|
||||
@@ -124,6 +124,10 @@ pub struct Env {
|
||||
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 `disable_container_exec`
|
||||
pub periphery_disable_container_exec: Option<bool>,
|
||||
/// Override `stats_polling_rate`
|
||||
pub periphery_stats_polling_rate: Option<Timelength>,
|
||||
/// Override `legacy_compose_cli`
|
||||
@@ -203,6 +207,18 @@ pub struct PeripheryConfig {
|
||||
/// Default: empty
|
||||
pub build_dir: Option<PathBuf>,
|
||||
|
||||
/// Whether to disable the create terminal
|
||||
/// and disallow direct remote shell access.
|
||||
/// Default: false
|
||||
#[serde(default)]
|
||||
pub disable_terminals: bool,
|
||||
|
||||
/// Whether to disable the container exec api
|
||||
/// and disallow remote container shell access.
|
||||
/// Default: false
|
||||
#[serde(default)]
|
||||
pub disable_container_exec: bool,
|
||||
|
||||
/// The rate at which the system stats will be polled to update the cache.
|
||||
/// Default: `5-sec`
|
||||
#[serde(default = "default_stats_polling_rate")]
|
||||
@@ -256,7 +272,7 @@ pub struct PeripheryConfig {
|
||||
pub docker_registries: Vec<DockerRegistry>,
|
||||
|
||||
/// Whether to enable ssl.
|
||||
/// Default: false (will change in later release)
|
||||
/// Default: true
|
||||
#[serde(default = "default_ssl_enabled")]
|
||||
pub ssl_enabled: bool,
|
||||
|
||||
@@ -286,7 +302,7 @@ fn default_stats_polling_rate() -> Timelength {
|
||||
}
|
||||
|
||||
fn default_ssl_enabled() -> bool {
|
||||
false
|
||||
true
|
||||
}
|
||||
|
||||
impl Default for PeripheryConfig {
|
||||
@@ -298,6 +314,8 @@ impl Default for PeripheryConfig {
|
||||
repo_dir: None,
|
||||
stack_dir: None,
|
||||
build_dir: None,
|
||||
disable_terminals: Default::default(),
|
||||
disable_container_exec: Default::default(),
|
||||
stats_polling_rate: default_stats_polling_rate(),
|
||||
legacy_compose_cli: Default::default(),
|
||||
logging: Default::default(),
|
||||
@@ -324,6 +342,8 @@ impl PeripheryConfig {
|
||||
repo_dir: self.repo_dir.clone(),
|
||||
stack_dir: self.stack_dir.clone(),
|
||||
build_dir: self.build_dir.clone(),
|
||||
disable_terminals: self.disable_terminals,
|
||||
disable_container_exec: self.disable_container_exec,
|
||||
stats_polling_rate: self.stats_polling_rate,
|
||||
legacy_compose_cli: self.legacy_compose_cli,
|
||||
logging: self.logging.clone(),
|
||||
|
||||
@@ -54,8 +54,6 @@ pub mod repo;
|
||||
pub mod resource;
|
||||
/// Subtypes of [Server][server::Server].
|
||||
pub mod server;
|
||||
/// Subtypes of [ServerTemplate][server_template::ServerTemplate].
|
||||
pub mod server_template;
|
||||
/// Subtypes of [Stack][stack::Stack]
|
||||
pub mod stack;
|
||||
/// Subtypes for server stats reporting.
|
||||
@@ -759,13 +757,6 @@ pub enum Operation {
|
||||
DeleteAlerter,
|
||||
TestAlerter,
|
||||
|
||||
// server template
|
||||
CreateServerTemplate,
|
||||
UpdateServerTemplate,
|
||||
RenameServerTemplate,
|
||||
DeleteServerTemplate,
|
||||
LaunchServer,
|
||||
|
||||
// sync
|
||||
CreateResourceSync,
|
||||
UpdateResourceSync,
|
||||
@@ -878,7 +869,6 @@ pub enum ResourceTarget {
|
||||
Action(String),
|
||||
Builder(String),
|
||||
Alerter(String),
|
||||
ServerTemplate(String),
|
||||
ResourceSync(String),
|
||||
}
|
||||
|
||||
@@ -897,7 +887,6 @@ impl ResourceTarget {
|
||||
ResourceTarget::Alerter(id) => id,
|
||||
ResourceTarget::Procedure(id) => id,
|
||||
ResourceTarget::Action(id) => id,
|
||||
ResourceTarget::ServerTemplate(id) => id,
|
||||
ResourceTarget::ResourceSync(id) => id,
|
||||
};
|
||||
(self.extract_variant(), id)
|
||||
@@ -956,12 +945,6 @@ impl From<&procedure::Procedure> for ResourceTarget {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&server_template::ServerTemplate> for ResourceTarget {
|
||||
fn from(server_template: &server_template::ServerTemplate) -> Self {
|
||||
Self::ServerTemplate(server_template.id.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&sync::ResourceSync> for ResourceTarget {
|
||||
fn from(resource_sync: &sync::ResourceSync) -> Self {
|
||||
Self::ResourceSync(resource_sync.id.clone())
|
||||
@@ -992,7 +975,6 @@ impl ResourceTargetVariant {
|
||||
ResourceTargetVariant::Repo => "repo",
|
||||
ResourceTargetVariant::Alerter => "alerter",
|
||||
ResourceTargetVariant::Procedure => "procedure",
|
||||
ResourceTargetVariant::ServerTemplate => "server_template",
|
||||
ResourceTargetVariant::ResourceSync => "resource_sync",
|
||||
ResourceTargetVariant::Stack => "stack",
|
||||
ResourceTargetVariant::Action => "action",
|
||||
@@ -1009,3 +991,6 @@ pub enum ScheduleFormat {
|
||||
English,
|
||||
Cron,
|
||||
}
|
||||
|
||||
/// Used with ExecuteTerminal to capture the exit code
|
||||
pub const KOMODO_EXIT_CODE: &str = "__KOMODO_EXIT_CODE:";
|
||||
|
||||
@@ -38,6 +38,10 @@ 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,
|
||||
/// Whether container exec is disabled for this Server.
|
||||
pub container_exec_disabled: bool,
|
||||
}
|
||||
|
||||
#[typeshare(serialized_as = "Partial<ServerConfig>")]
|
||||
@@ -276,6 +280,19 @@ 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)]
|
||||
|
||||
@@ -1,216 +0,0 @@
|
||||
use derive_builder::Builder;
|
||||
use partial_derive2::Partial;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{AsRefStr, Display};
|
||||
use typeshare::typeshare;
|
||||
|
||||
use crate::{
|
||||
deserializers::{
|
||||
option_string_list_deserializer, string_list_deserializer,
|
||||
},
|
||||
entities::builder::AwsBuilderConfig,
|
||||
};
|
||||
|
||||
#[typeshare(serialized_as = "Partial<AwsServerTemplateConfig>")]
|
||||
pub type _PartialAwsServerTemplateConfig =
|
||||
PartialAwsServerTemplateConfig;
|
||||
|
||||
/// Aws EC2 instance config.
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Builder, Partial)]
|
||||
#[partial_derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[partial(skip_serializing_none, from, diff)]
|
||||
pub struct AwsServerTemplateConfig {
|
||||
/// The aws region to launch the server in, eg. us-east-1
|
||||
#[serde(default = "default_region")]
|
||||
#[builder(default = "default_region()")]
|
||||
#[partial_default(default_region())]
|
||||
pub region: String,
|
||||
/// The instance type to launch, eg. c5.2xlarge
|
||||
#[serde(default = "default_instance_type")]
|
||||
#[builder(default = "default_instance_type()")]
|
||||
#[partial_default(default_instance_type())]
|
||||
pub instance_type: String,
|
||||
/// Specify the ami id to use. Must be set up to start the periphery binary on startup.
|
||||
pub ami_id: String,
|
||||
/// The subnet to assign to the instance.
|
||||
pub subnet_id: String,
|
||||
/// The key pair name to give to the instance in case SSH access required.
|
||||
pub key_pair_name: String,
|
||||
/// Assign a public ip to the instance. Depending on how your network is
|
||||
/// setup, this may be required for the instance to reach the public internet.
|
||||
#[serde(default = "default_assign_public_ip")]
|
||||
#[builder(default = "default_assign_public_ip()")]
|
||||
#[partial_default(default_assign_public_ip())]
|
||||
pub assign_public_ip: bool,
|
||||
/// Use the instances public ip as the address for the server.
|
||||
/// Could be used when build instances are created in another non-interconnected network to the core api.
|
||||
#[serde(default = "default_use_public_ip")]
|
||||
#[builder(default = "default_use_public_ip()")]
|
||||
#[partial_default(default_use_public_ip())]
|
||||
pub use_public_ip: bool,
|
||||
/// The port periphery will be running on in AMI.
|
||||
/// Default: `8120`
|
||||
#[serde(default = "default_port")]
|
||||
#[builder(default = "default_port()")]
|
||||
#[partial_default(default_port())]
|
||||
pub port: i32,
|
||||
/// Whether Periphery will be running on https
|
||||
#[serde(default = "default_use_https")]
|
||||
#[builder(default = "default_use_https()")]
|
||||
#[partial_default(default_use_https())]
|
||||
pub use_https: bool,
|
||||
/// The security groups to give to the instance.
|
||||
#[serde(default, deserialize_with = "string_list_deserializer")]
|
||||
#[partial_attr(serde(
|
||||
default,
|
||||
deserialize_with = "option_string_list_deserializer"
|
||||
))]
|
||||
#[builder(default)]
|
||||
pub security_group_ids: Vec<String>,
|
||||
/// Specify the EBS volumes to attach.
|
||||
#[serde(default = "default_volumes")]
|
||||
#[builder(default = "default_volumes()")]
|
||||
#[partial_default(default_volumes())]
|
||||
pub volumes: Vec<AwsVolume>,
|
||||
/// The user data to deploy the instance with.
|
||||
#[serde(default = "default_user_data")]
|
||||
#[builder(default = "default_user_data()")]
|
||||
#[partial_default(default_user_data())]
|
||||
pub user_data: String,
|
||||
}
|
||||
|
||||
impl AwsServerTemplateConfig {
|
||||
pub fn builder() -> AwsServerTemplateConfigBuilder {
|
||||
AwsServerTemplateConfigBuilder::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn default_region() -> String {
|
||||
String::from("us-east-1")
|
||||
}
|
||||
|
||||
fn default_instance_type() -> String {
|
||||
String::from("t3.small")
|
||||
}
|
||||
|
||||
fn default_assign_public_ip() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_use_public_ip() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn default_volumes() -> Vec<AwsVolume> {
|
||||
vec![AwsVolume {
|
||||
device_name: "/dev/sda1".to_string(),
|
||||
size_gb: 20,
|
||||
volume_type: AwsVolumeType::Gp2,
|
||||
iops: 0,
|
||||
throughput: 0,
|
||||
}]
|
||||
}
|
||||
|
||||
fn default_port() -> i32 {
|
||||
8120
|
||||
}
|
||||
|
||||
fn default_use_https() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_user_data() -> String {
|
||||
String::from("#!/bin/bash
|
||||
apt update
|
||||
apt upgrade -y
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
systemctl enable docker.service
|
||||
systemctl enable containerd.service
|
||||
curl -sSL https://raw.githubusercontent.com/moghtech/komodo/main/scripts/setup-periphery.py | HOME=/root python3
|
||||
systemctl enable periphery.service")
|
||||
}
|
||||
|
||||
impl Default for AwsServerTemplateConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
region: default_region(),
|
||||
instance_type: default_instance_type(),
|
||||
assign_public_ip: default_assign_public_ip(),
|
||||
use_public_ip: default_use_public_ip(),
|
||||
port: default_port(),
|
||||
use_https: default_use_https(),
|
||||
volumes: default_volumes(),
|
||||
ami_id: Default::default(),
|
||||
subnet_id: Default::default(),
|
||||
key_pair_name: Default::default(),
|
||||
user_data: default_user_data(),
|
||||
security_group_ids: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// For information on AWS volumes, see
|
||||
/// `<https://docs.aws.amazon.com/ebs/latest/userguide/ebs-volume-types.html>`.
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AwsVolume {
|
||||
/// The device name (for example, `/dev/sda1` or `xvdh`).
|
||||
pub device_name: String,
|
||||
/// The size of the volume in GB
|
||||
pub size_gb: i32,
|
||||
/// The type of volume. Options: gp2, gp3, io1, io2.
|
||||
pub volume_type: AwsVolumeType,
|
||||
/// The iops of the volume, or 0 for AWS default.
|
||||
pub iops: i32,
|
||||
/// The throughput of the volume, or 0 for AWS default.
|
||||
pub throughput: i32,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
Default,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Display,
|
||||
AsRefStr,
|
||||
)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
pub enum AwsVolumeType {
|
||||
#[default]
|
||||
Gp2,
|
||||
Gp3,
|
||||
Io1,
|
||||
Io2,
|
||||
}
|
||||
|
||||
impl AwsServerTemplateConfig {
|
||||
pub fn from_builder_config(value: &AwsBuilderConfig) -> Self {
|
||||
Self {
|
||||
region: value.region.clone(),
|
||||
instance_type: value.instance_type.clone(),
|
||||
volumes: vec![AwsVolume {
|
||||
device_name: "/dev/sda1".to_string(),
|
||||
size_gb: value.volume_gb,
|
||||
volume_type: AwsVolumeType::Gp2,
|
||||
iops: 0,
|
||||
throughput: 0,
|
||||
}],
|
||||
ami_id: value.ami_id.clone(),
|
||||
subnet_id: value.subnet_id.clone(),
|
||||
security_group_ids: value.security_group_ids.clone(),
|
||||
key_pair_name: value.key_pair_name.clone(),
|
||||
assign_public_ip: value.assign_public_ip,
|
||||
use_public_ip: value.use_public_ip,
|
||||
port: value.port,
|
||||
use_https: value.use_https,
|
||||
user_data: value.user_data.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,255 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use derive_builder::Builder;
|
||||
use partial_derive2::Partial;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::AsRefStr;
|
||||
use typeshare::typeshare;
|
||||
|
||||
use crate::{
|
||||
deserializers::{
|
||||
option_string_list_deserializer, string_list_deserializer,
|
||||
},
|
||||
entities::I64,
|
||||
};
|
||||
|
||||
#[typeshare(serialized_as = "Partial<HetznerServerTemplateConfig>")]
|
||||
pub type _PartialHetznerServerTemplateConfig =
|
||||
PartialHetznerServerTemplateConfig;
|
||||
|
||||
/// Hetzner server config.
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Builder, Partial)]
|
||||
#[partial_derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[partial(skip_serializing_none, from, diff)]
|
||||
pub struct HetznerServerTemplateConfig {
|
||||
/// ID or name of the Image the Server is created from
|
||||
#[serde(default = "default_image")]
|
||||
#[builder(default = "default_image()")]
|
||||
#[partial_default(default_image())]
|
||||
pub image: String,
|
||||
/// ID or name of Datacenter to create Server in
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub datacenter: HetznerDatacenter,
|
||||
/// ID of the Placement Group the server should be in,
|
||||
/// Or 0 to not use placement group.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub placement_group: I64,
|
||||
/// ID or name of the Server type this Server should be created with
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub server_type: HetznerServerType,
|
||||
/// SSH key IDs ( integer ) or names ( string ) which should be injected into the Server at creation time
|
||||
#[serde(default, deserialize_with = "string_list_deserializer")]
|
||||
#[partial_attr(serde(
|
||||
default,
|
||||
deserialize_with = "option_string_list_deserializer"
|
||||
))]
|
||||
#[builder(default)]
|
||||
pub ssh_keys: Vec<String>,
|
||||
/// Network IDs which should be attached to the Server private network interface at the creation time
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub private_network_ids: Vec<I64>,
|
||||
/// Attach an IPv4 on the public NIC. If false, no IPv4 address will be attached.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub enable_public_ipv4: bool,
|
||||
/// Attach an IPv6 on the public NIC. If false, no IPv6 address will be attached.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub enable_public_ipv6: bool,
|
||||
/// Connect to the instance using it's public ip.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub use_public_ip: bool,
|
||||
/// The port periphery will be running on in AMI.
|
||||
/// Default: `8120`
|
||||
#[serde(default = "default_port")]
|
||||
#[builder(default = "default_port()")]
|
||||
#[partial_default(default_port())]
|
||||
pub port: i32,
|
||||
/// Whether Periphery will be running on https
|
||||
#[serde(default = "default_use_https")]
|
||||
#[builder(default = "default_use_https()")]
|
||||
#[partial_default(default_use_https())]
|
||||
pub use_https: bool,
|
||||
/// The firewalls to attach to the instance
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub firewall_ids: Vec<I64>,
|
||||
/// Labels for the server
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub labels: HashMap<String, String>,
|
||||
/// Specs for volumes to attach
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub volumes: Vec<HetznerVolumeSpecs>,
|
||||
/// Cloud-Init user data to use during Server creation. This field is limited to 32KiB.
|
||||
#[serde(default = "default_user_data")]
|
||||
#[builder(default = "default_user_data()")]
|
||||
#[partial_default(default_user_data())]
|
||||
pub user_data: String,
|
||||
}
|
||||
|
||||
impl HetznerServerTemplateConfig {
|
||||
pub fn builder() -> HetznerServerTemplateConfigBuilder {
|
||||
HetznerServerTemplateConfigBuilder::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn default_image() -> String {
|
||||
String::from("ubuntu-24.04")
|
||||
}
|
||||
|
||||
fn default_port() -> i32 {
|
||||
8120
|
||||
}
|
||||
|
||||
fn default_use_https() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_user_data() -> String {
|
||||
String::from("#cloud-config
|
||||
runcmd:
|
||||
- apt update
|
||||
- apt upgrade -y
|
||||
- curl -fsSL https://get.docker.com | sh
|
||||
- systemctl enable docker.service
|
||||
- systemctl enable containerd.service
|
||||
- curl -sSL 'https://raw.githubusercontent.com/moghtech/komodo/main/scripts/setup-periphery.py' | HOME=/root python3
|
||||
- systemctl enable periphery.service")
|
||||
}
|
||||
|
||||
impl Default for HetznerServerTemplateConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
port: default_port(),
|
||||
use_https: default_use_https(),
|
||||
image: default_image(),
|
||||
datacenter: Default::default(),
|
||||
private_network_ids: Default::default(),
|
||||
placement_group: Default::default(),
|
||||
enable_public_ipv4: Default::default(),
|
||||
enable_public_ipv6: Default::default(),
|
||||
firewall_ids: Default::default(),
|
||||
server_type: Default::default(),
|
||||
ssh_keys: Default::default(),
|
||||
user_data: default_user_data(),
|
||||
use_public_ip: Default::default(),
|
||||
labels: Default::default(),
|
||||
volumes: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Debug, Clone, PartialEq, Serialize, Deserialize, Builder,
|
||||
)]
|
||||
pub struct HetznerVolumeSpecs {
|
||||
/// A name for the volume
|
||||
pub name: String,
|
||||
/// Size of the volume in GB
|
||||
pub size_gb: I64,
|
||||
/// The format for the volume
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub format: HetznerVolumeFormat,
|
||||
/// Labels for the volume
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub labels: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize,
|
||||
)]
|
||||
pub enum HetznerVolumeFormat {
|
||||
#[default]
|
||||
Xfs,
|
||||
Ext4,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
Default,
|
||||
PartialEq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
AsRefStr,
|
||||
)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub enum HetznerServerType {
|
||||
// Shared
|
||||
#[default]
|
||||
/// CPX11 - AMD 2 Cores, 2 Gb Ram, 40 Gb disk
|
||||
SharedAmd2Core2Ram40Disk,
|
||||
/// CAX11 - Arm 2 Cores, 4 Gb Ram, 40 Gb disk
|
||||
SharedArm2Core4Ram40Disk,
|
||||
/// CX22 - Intel 2 Cores, 4 Gb Ram, 40 Gb disk
|
||||
SharedIntel2Core4Ram40Disk,
|
||||
/// CPX21 - AMD 3 Cores, 4 Gb Ram, 80 Gb disk
|
||||
SharedAmd3Core4Ram80Disk,
|
||||
/// CAX21 - Arm 4 Cores, 8 Gb Ram, 80 Gb disk
|
||||
SharedArm4Core8Ram80Disk,
|
||||
/// CX32 - Intel 4 Cores, 8 Gb Ram, 80 Gb disk
|
||||
SharedIntel4Core8Ram80Disk,
|
||||
/// CPX31 - AMD 4 Cores, 8 Gb Ram, 160 Gb disk
|
||||
SharedAmd4Core8Ram160Disk,
|
||||
/// CAX31 - Arm 8 Cores, 16 Gb Ram, 160 Gb disk
|
||||
SharedArm8Core16Ram160Disk,
|
||||
/// CX42 - Intel 8 Cores, 16 Gb Ram, 160 Gb disk
|
||||
SharedIntel8Core16Ram160Disk,
|
||||
/// CPX41 - AMD 8 Cores, 16 Gb Ram, 240 Gb disk
|
||||
SharedAmd8Core16Ram240Disk,
|
||||
/// CAX41 - Arm 16 Cores, 32 Gb Ram, 320 Gb disk
|
||||
SharedArm16Core32Ram320Disk,
|
||||
/// CX52 - Intel 16 Cores, 32 Gb Ram, 320 Gb disk
|
||||
SharedIntel16Core32Ram320Disk,
|
||||
/// CPX51 - AMD 16 Cores, 32 Gb Ram, 360 Gb disk
|
||||
SharedAmd16Core32Ram360Disk,
|
||||
|
||||
// Dedicated
|
||||
/// CCX13 - AMD 2 Cores, 8 Gb Ram, 80 Gb disk
|
||||
DedicatedAmd2Core8Ram80Disk,
|
||||
/// CCX23 - AMD 4 Cores, 16 Gb Ram, 160 Gb disk
|
||||
DedicatedAmd4Core16Ram160Disk,
|
||||
/// CCX33 - AMD 8 Cores, 32 Gb Ram, 240 Gb disk
|
||||
DedicatedAmd8Core32Ram240Disk,
|
||||
/// CCX43 - AMD 16 Cores, 64 Gb Ram, 360 Gb disk
|
||||
DedicatedAmd16Core64Ram360Disk,
|
||||
/// CCX53 - AMD 32 Cores, 128 Gb Ram, 600 Gb disk
|
||||
DedicatedAmd32Core128Ram600Disk,
|
||||
/// CCX63 - AMD 48 Cores, 192 Gb Ram, 960 Gb disk
|
||||
DedicatedAmd48Core192Ram960Disk,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
Default,
|
||||
PartialEq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
AsRefStr,
|
||||
)]
|
||||
pub enum HetznerDatacenter {
|
||||
#[default]
|
||||
Nuremberg1Dc3,
|
||||
Helsinki1Dc2,
|
||||
Falkenstein1Dc14,
|
||||
AshburnDc1,
|
||||
HillsboroDc1,
|
||||
SingaporeDc1,
|
||||
}
|
||||
@@ -1,312 +0,0 @@
|
||||
use bson::{Document, doc};
|
||||
use derive_default_builder::DefaultBuilder;
|
||||
use derive_variants::EnumVariants;
|
||||
use partial_derive2::{Diff, MaybeNone, PartialDiff};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{AsRefStr, Display, EnumString};
|
||||
use typeshare::typeshare;
|
||||
|
||||
use self::{
|
||||
aws::AwsServerTemplateConfig, hetzner::HetznerServerTemplateConfig,
|
||||
};
|
||||
|
||||
use super::{
|
||||
MergePartial,
|
||||
resource::{AddFilters, Resource, ResourceListItem, ResourceQuery},
|
||||
};
|
||||
|
||||
pub mod aws;
|
||||
pub mod hetzner;
|
||||
|
||||
#[typeshare]
|
||||
pub type ServerTemplate = Resource<ServerTemplateConfig, ()>;
|
||||
|
||||
#[typeshare]
|
||||
pub type ServerTemplateListItem =
|
||||
ResourceListItem<ServerTemplateListItemInfo>;
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ServerTemplateListItemInfo {
|
||||
/// The cloud provider
|
||||
pub provider: String,
|
||||
/// The instance type, eg c5.2xlarge on for Aws templates
|
||||
pub instance_type: Option<String>,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, EnumVariants)]
|
||||
#[variant_derive(
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
Display,
|
||||
EnumString,
|
||||
AsRefStr
|
||||
)]
|
||||
#[serde(tag = "type", content = "params")]
|
||||
pub enum ServerTemplateConfig {
|
||||
/// Template to launch an AWS EC2 instance
|
||||
Aws(aws::AwsServerTemplateConfig),
|
||||
/// Template to launch a Hetzner server
|
||||
Hetzner(hetzner::HetznerServerTemplateConfig),
|
||||
}
|
||||
|
||||
impl Default for ServerTemplateConfig {
|
||||
fn default() -> Self {
|
||||
Self::Aws(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, EnumVariants)]
|
||||
#[variant_derive(
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
Display,
|
||||
EnumString,
|
||||
AsRefStr
|
||||
)]
|
||||
#[serde(tag = "type", content = "params")]
|
||||
pub enum PartialServerTemplateConfig {
|
||||
Aws(#[serde(default)] aws::_PartialAwsServerTemplateConfig),
|
||||
Hetzner(
|
||||
#[serde(default)] hetzner::_PartialHetznerServerTemplateConfig,
|
||||
),
|
||||
}
|
||||
|
||||
impl Default for PartialServerTemplateConfig {
|
||||
fn default() -> Self {
|
||||
Self::Aws(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl MaybeNone for PartialServerTemplateConfig {
|
||||
fn is_none(&self) -> bool {
|
||||
match self {
|
||||
PartialServerTemplateConfig::Aws(config) => config.is_none(),
|
||||
PartialServerTemplateConfig::Hetzner(config) => {
|
||||
config.is_none()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ServerTemplateConfigDiff {
|
||||
Aws(aws::AwsServerTemplateConfigDiff),
|
||||
Hetzner(hetzner::HetznerServerTemplateConfigDiff),
|
||||
}
|
||||
|
||||
impl From<ServerTemplateConfigDiff> for PartialServerTemplateConfig {
|
||||
fn from(value: ServerTemplateConfigDiff) -> Self {
|
||||
match value {
|
||||
ServerTemplateConfigDiff::Aws(diff) => {
|
||||
PartialServerTemplateConfig::Aws(diff.into())
|
||||
}
|
||||
ServerTemplateConfigDiff::Hetzner(diff) => {
|
||||
PartialServerTemplateConfig::Hetzner(diff.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Diff for ServerTemplateConfigDiff {
|
||||
fn iter_field_diffs(
|
||||
&self,
|
||||
) -> impl Iterator<Item = partial_derive2::FieldDiff> {
|
||||
match self {
|
||||
ServerTemplateConfigDiff::Aws(diff) => {
|
||||
diff.iter_field_diffs().collect::<Vec<_>>().into_iter()
|
||||
}
|
||||
ServerTemplateConfigDiff::Hetzner(diff) => {
|
||||
diff.iter_field_diffs().collect::<Vec<_>>().into_iter()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl
|
||||
PartialDiff<PartialServerTemplateConfig, ServerTemplateConfigDiff>
|
||||
for ServerTemplateConfig
|
||||
{
|
||||
fn partial_diff(
|
||||
&self,
|
||||
partial: PartialServerTemplateConfig,
|
||||
) -> ServerTemplateConfigDiff {
|
||||
match self {
|
||||
ServerTemplateConfig::Aws(original) => match partial {
|
||||
PartialServerTemplateConfig::Aws(partial) => {
|
||||
ServerTemplateConfigDiff::Aws(
|
||||
original.partial_diff(partial),
|
||||
)
|
||||
}
|
||||
PartialServerTemplateConfig::Hetzner(partial) => {
|
||||
let default = HetznerServerTemplateConfig::default();
|
||||
ServerTemplateConfigDiff::Hetzner(
|
||||
default.partial_diff(partial),
|
||||
)
|
||||
}
|
||||
},
|
||||
ServerTemplateConfig::Hetzner(original) => match partial {
|
||||
PartialServerTemplateConfig::Hetzner(partial) => {
|
||||
ServerTemplateConfigDiff::Hetzner(
|
||||
original.partial_diff(partial),
|
||||
)
|
||||
}
|
||||
PartialServerTemplateConfig::Aws(partial) => {
|
||||
let default = AwsServerTemplateConfig::default();
|
||||
ServerTemplateConfigDiff::Aws(default.partial_diff(partial))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MaybeNone for ServerTemplateConfigDiff {
|
||||
fn is_none(&self) -> bool {
|
||||
match self {
|
||||
ServerTemplateConfigDiff::Aws(config) => config.is_none(),
|
||||
ServerTemplateConfigDiff::Hetzner(config) => config.is_none(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PartialServerTemplateConfig> for ServerTemplateConfig {
|
||||
fn from(
|
||||
value: PartialServerTemplateConfig,
|
||||
) -> ServerTemplateConfig {
|
||||
match value {
|
||||
PartialServerTemplateConfig::Aws(config) => {
|
||||
ServerTemplateConfig::Aws(config.into())
|
||||
}
|
||||
PartialServerTemplateConfig::Hetzner(config) => {
|
||||
ServerTemplateConfig::Hetzner(config.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ServerTemplateConfig> for PartialServerTemplateConfig {
|
||||
fn from(value: ServerTemplateConfig) -> Self {
|
||||
match value {
|
||||
ServerTemplateConfig::Aws(config) => {
|
||||
PartialServerTemplateConfig::Aws(config.into())
|
||||
}
|
||||
ServerTemplateConfig::Hetzner(config) => {
|
||||
PartialServerTemplateConfig::Hetzner(config.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MergePartial for ServerTemplateConfig {
|
||||
type Partial = PartialServerTemplateConfig;
|
||||
fn merge_partial(
|
||||
self,
|
||||
partial: PartialServerTemplateConfig,
|
||||
) -> ServerTemplateConfig {
|
||||
match partial {
|
||||
PartialServerTemplateConfig::Aws(partial) => match self {
|
||||
ServerTemplateConfig::Aws(config) => {
|
||||
let config = aws::AwsServerTemplateConfig {
|
||||
region: partial.region.unwrap_or(config.region),
|
||||
instance_type: partial
|
||||
.instance_type
|
||||
.unwrap_or(config.instance_type),
|
||||
volumes: partial.volumes.unwrap_or(config.volumes),
|
||||
ami_id: partial.ami_id.unwrap_or(config.ami_id),
|
||||
subnet_id: partial.subnet_id.unwrap_or(config.subnet_id),
|
||||
security_group_ids: partial
|
||||
.security_group_ids
|
||||
.unwrap_or(config.security_group_ids),
|
||||
key_pair_name: partial
|
||||
.key_pair_name
|
||||
.unwrap_or(config.key_pair_name),
|
||||
assign_public_ip: partial
|
||||
.assign_public_ip
|
||||
.unwrap_or(config.assign_public_ip),
|
||||
use_public_ip: partial
|
||||
.use_public_ip
|
||||
.unwrap_or(config.use_public_ip),
|
||||
port: partial.port.unwrap_or(config.port),
|
||||
use_https: partial.use_https.unwrap_or(config.use_https),
|
||||
user_data: partial.user_data.unwrap_or(config.user_data),
|
||||
};
|
||||
ServerTemplateConfig::Aws(config)
|
||||
}
|
||||
ServerTemplateConfig::Hetzner(_) => {
|
||||
ServerTemplateConfig::Aws(partial.into())
|
||||
}
|
||||
},
|
||||
PartialServerTemplateConfig::Hetzner(partial) => match self {
|
||||
ServerTemplateConfig::Hetzner(config) => {
|
||||
let config = hetzner::HetznerServerTemplateConfig {
|
||||
image: partial.image.unwrap_or(config.image),
|
||||
datacenter: partial
|
||||
.datacenter
|
||||
.unwrap_or(config.datacenter),
|
||||
private_network_ids: partial
|
||||
.private_network_ids
|
||||
.unwrap_or(config.private_network_ids),
|
||||
placement_group: partial
|
||||
.placement_group
|
||||
.unwrap_or(config.placement_group),
|
||||
enable_public_ipv4: partial
|
||||
.enable_public_ipv4
|
||||
.unwrap_or(config.enable_public_ipv4),
|
||||
enable_public_ipv6: partial
|
||||
.enable_public_ipv6
|
||||
.unwrap_or(config.enable_public_ipv6),
|
||||
firewall_ids: partial
|
||||
.firewall_ids
|
||||
.unwrap_or(config.firewall_ids),
|
||||
server_type: partial
|
||||
.server_type
|
||||
.unwrap_or(config.server_type),
|
||||
ssh_keys: partial.ssh_keys.unwrap_or(config.ssh_keys),
|
||||
user_data: partial.user_data.unwrap_or(config.user_data),
|
||||
use_public_ip: partial
|
||||
.use_public_ip
|
||||
.unwrap_or(config.use_public_ip),
|
||||
labels: partial.labels.unwrap_or(config.labels),
|
||||
volumes: partial.volumes.unwrap_or(config.volumes),
|
||||
port: partial.port.unwrap_or(config.port),
|
||||
use_https: partial.use_https.unwrap_or(config.use_https),
|
||||
};
|
||||
ServerTemplateConfig::Hetzner(config)
|
||||
}
|
||||
ServerTemplateConfig::Aws(_) => {
|
||||
ServerTemplateConfig::Hetzner(partial.into())
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
pub type ServerTemplateQuery =
|
||||
ResourceQuery<ServerTemplateQuerySpecifics>;
|
||||
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Default, DefaultBuilder,
|
||||
)]
|
||||
pub struct ServerTemplateQuerySpecifics {
|
||||
pub types: Vec<ServerTemplateConfigVariant>,
|
||||
}
|
||||
|
||||
impl AddFilters for ServerTemplateQuerySpecifics {
|
||||
fn add_filters(&self, filters: &mut Document) {
|
||||
let types =
|
||||
self.types.iter().map(|t| t.as_ref()).collect::<Vec<_>>();
|
||||
if !self.types.is_empty() {
|
||||
filters.insert("config.type", doc! { "$in": types });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,10 @@ pub struct SystemInformation {
|
||||
pub host_name: Option<String>,
|
||||
/// The CPU's brand
|
||||
pub cpu_brand: String,
|
||||
/// Whether terminals are disabled on this Periphery server
|
||||
pub terminals_disabled: bool,
|
||||
/// Whether container exec is disabled on this Periphery server
|
||||
pub container_exec_disabled: bool,
|
||||
}
|
||||
|
||||
/// System stats stored on the database.
|
||||
|
||||
@@ -10,7 +10,6 @@ use super::{
|
||||
deployment::_PartialDeploymentConfig, permission::PermissionLevel,
|
||||
procedure::_PartialProcedureConfig, repo::_PartialRepoConfig,
|
||||
server::_PartialServerConfig,
|
||||
server_template::PartialServerTemplateConfig,
|
||||
stack::_PartialStackConfig, sync::_PartialResourceSyncConfig,
|
||||
variable::Variable,
|
||||
};
|
||||
@@ -82,14 +81,6 @@ pub struct ResourcesToml {
|
||||
)]
|
||||
pub builders: Vec<ResourceToml<_PartialBuilderConfig>>,
|
||||
|
||||
#[serde(
|
||||
default,
|
||||
alias = "server_template",
|
||||
skip_serializing_if = "Vec::is_empty"
|
||||
)]
|
||||
pub server_templates:
|
||||
Vec<ResourceToml<PartialServerTemplateConfig>>,
|
||||
|
||||
#[serde(
|
||||
default,
|
||||
alias = "resource_sync",
|
||||
|
||||
@@ -39,6 +39,7 @@ pub mod busy;
|
||||
pub mod deserializers;
|
||||
pub mod entities;
|
||||
pub mod parsers;
|
||||
pub mod terminal;
|
||||
pub mod ws;
|
||||
|
||||
mod request;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use anyhow::{Context, anyhow};
|
||||
use reqwest::StatusCode;
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use serde_json::json;
|
||||
use serror::deserialize_error;
|
||||
@@ -208,7 +207,7 @@ impl KomodoClient {
|
||||
let res =
|
||||
req.send().await.context("failed to reach Komodo API")?;
|
||||
let status = res.status();
|
||||
if status == StatusCode::OK {
|
||||
if status.is_success() {
|
||||
match res.json().await {
|
||||
Ok(res) => Ok(res),
|
||||
Err(e) => Err(anyhow!("{e:#?}").context(status)),
|
||||
@@ -236,7 +235,7 @@ impl KomodoClient {
|
||||
.json(&body);
|
||||
let res = req.send().context("failed to reach Komodo API")?;
|
||||
let status = res.status();
|
||||
if status == StatusCode::OK {
|
||||
if status.is_success() {
|
||||
match res.json() {
|
||||
Ok(res) => Ok(res),
|
||||
Err(e) => Err(anyhow!("{e:#?}").context(status)),
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
use futures::{Stream, StreamExt, TryStreamExt};
|
||||
|
||||
pub struct TerminalStreamResponse(pub reqwest::Response);
|
||||
|
||||
impl TerminalStreamResponse {
|
||||
pub fn into_line_stream(
|
||||
self,
|
||||
) -> impl Stream<Item = Result<String, tokio_util::codec::LinesCodecError>>
|
||||
{
|
||||
tokio_util::codec::FramedRead::new(
|
||||
tokio_util::io::StreamReader::new(
|
||||
self.0.bytes_stream().map_err(std::io::Error::other),
|
||||
),
|
||||
tokio_util::codec::LinesCodec::new(),
|
||||
)
|
||||
.map(|line| line.map(|line| line + "\n"))
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user