forked from github-starred/komodo
Compare commits
14 Commits
v1.16.11
...
v1.17.0-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e385c6e722 | ||
|
|
9ef25e7575 | ||
|
|
f945a3014a | ||
|
|
fdad04d6cb | ||
|
|
c914f23aa8 | ||
|
|
82b2e68cd3 | ||
|
|
e274d6f7c8 | ||
|
|
ab8777460d | ||
|
|
7e030e702f | ||
|
|
a869a74002 | ||
|
|
1d31110f8c | ||
|
|
bb63892e10 | ||
|
|
4e554eb2a7 | ||
|
|
00968b6ea1 |
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository
|
||||
open_collective: komodo
|
||||
1315
Cargo.lock
generated
1315
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
50
Cargo.toml
50
Cargo.toml
@@ -3,13 +3,12 @@ resolver = "2"
|
||||
members = [
|
||||
"bin/*",
|
||||
"lib/*",
|
||||
"example/*",
|
||||
"client/core/rs",
|
||||
"client/periphery/rs",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.16.11"
|
||||
version = "1.16.12"
|
||||
edition = "2021"
|
||||
authors = ["mbecker20 <becker.maxh@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
@@ -34,7 +33,7 @@ git = { path = "lib/git" }
|
||||
# MOGH
|
||||
run_command = { version = "0.0.6", features = ["async_tokio"] }
|
||||
serror = { version = "0.4.7", default-features = false }
|
||||
slack = { version = "0.2.0", package = "slack_client_rs" }
|
||||
slack = { version = "0.3.0", package = "slack_client_rs", default-features = false, features = ["rustls"] }
|
||||
derive_default_builder = "0.1.8"
|
||||
derive_empty_traits = "0.1.0"
|
||||
merge_config_files = "0.1.5"
|
||||
@@ -48,52 +47,53 @@ mungos = "1.1.0"
|
||||
svi = "1.0.1"
|
||||
|
||||
# ASYNC
|
||||
reqwest = { version = "0.12.8", features = ["json"] }
|
||||
tokio = { version = "1.38.1", features = ["full"] }
|
||||
reqwest = { version = "0.12.9", default-features = false, features = ["json", "rustls-tls"] }
|
||||
tokio = { version = "1.41.1", features = ["full"] }
|
||||
tokio-util = "0.7.12"
|
||||
futures = "0.3.31"
|
||||
futures-util = "0.3.31"
|
||||
|
||||
# SERVER
|
||||
axum-extra = { version = "0.9.4", features = ["typed-header"] }
|
||||
tower-http = { version = "0.6.1", features = ["fs", "cors"] }
|
||||
axum-server = { version = "0.7.1", features = ["tls-openssl"] }
|
||||
axum = { version = "0.7.7", features = ["ws", "json"] }
|
||||
axum-extra = { version = "0.9.6", features = ["typed-header"] }
|
||||
tower-http = { version = "0.6.2", features = ["fs", "cors"] }
|
||||
axum-server = { version = "0.7.1", features = ["tls-rustls"] }
|
||||
axum = { version = "0.7.9", features = ["ws", "json"] }
|
||||
tokio-tungstenite = "0.24.0"
|
||||
|
||||
# SER/DE
|
||||
ordered_hash_map = { version = "0.4.0", features = ["serde"] }
|
||||
serde = { version = "1.0.210", features = ["derive"] }
|
||||
serde = { version = "1.0.215", features = ["derive"] }
|
||||
strum = { version = "0.26.3", features = ["derive"] }
|
||||
serde_json = "1.0.132"
|
||||
serde_json = "1.0.133"
|
||||
serde_yaml = "0.9.34"
|
||||
toml = "0.8.19"
|
||||
|
||||
# ERROR
|
||||
anyhow = "1.0.91"
|
||||
thiserror = "1.0.65"
|
||||
anyhow = "1.0.93"
|
||||
thiserror = "2.0.3"
|
||||
|
||||
# LOGGING
|
||||
opentelemetry_sdk = { version = "0.25.0", features = ["rt-tokio"] }
|
||||
opentelemetry-otlp = { version = "0.27.0", features = ["tls-roots", "reqwest-rustls"] }
|
||||
opentelemetry_sdk = { version = "0.27.0", features = ["rt-tokio"] }
|
||||
tracing-subscriber = { version = "0.3.18", features = ["json"] }
|
||||
opentelemetry-semantic-conventions = "0.25.0"
|
||||
tracing-opentelemetry = "0.26.0"
|
||||
opentelemetry-otlp = "0.25.0"
|
||||
opentelemetry = "0.25.0"
|
||||
opentelemetry-semantic-conventions = "0.27.0"
|
||||
tracing-opentelemetry = "0.28.0"
|
||||
opentelemetry = "0.27.0"
|
||||
tracing = "0.1.40"
|
||||
|
||||
# CONFIG
|
||||
clap = { version = "4.5.20", features = ["derive"] }
|
||||
clap = { version = "4.5.21", features = ["derive"] }
|
||||
dotenvy = "0.15.7"
|
||||
envy = "0.4.2"
|
||||
|
||||
# CRYPTO / AUTH
|
||||
uuid = { version = "1.10.0", features = ["v4", "fast-rng", "serde"] }
|
||||
uuid = { version = "1.11.0", features = ["v4", "fast-rng", "serde"] }
|
||||
openidconnect = "3.5.0"
|
||||
urlencoding = "2.1.3"
|
||||
nom_pem = "4.0.0"
|
||||
bcrypt = "0.15.1"
|
||||
bcrypt = "0.16.0"
|
||||
base64 = "0.22.1"
|
||||
rustls = "0.23.18"
|
||||
hmac = "0.12.1"
|
||||
sha2 = "0.10.8"
|
||||
rand = "0.8.5"
|
||||
@@ -101,19 +101,19 @@ jwt = "0.16.0"
|
||||
hex = "0.4.3"
|
||||
|
||||
# SYSTEM
|
||||
bollard = "0.17.1"
|
||||
bollard = "0.18.1"
|
||||
sysinfo = "0.32.0"
|
||||
|
||||
# CLOUD
|
||||
aws-config = "1.5.9"
|
||||
aws-sdk-ec2 = "1.83.0"
|
||||
aws-config = "1.5.10"
|
||||
aws-sdk-ec2 = "1.91.0"
|
||||
|
||||
# MISC
|
||||
derive_builder = "0.20.2"
|
||||
typeshare = "1.0.4"
|
||||
octorust = "0.7.0"
|
||||
dashmap = "6.1.0"
|
||||
wildcard = "0.2.0"
|
||||
wildcard = "0.3.0"
|
||||
colored = "2.1.0"
|
||||
regex = "1.11.1"
|
||||
bson = "2.13.0"
|
||||
|
||||
27
bin/binaries.Dockerfile
Normal file
27
bin/binaries.Dockerfile
Normal file
@@ -0,0 +1,27 @@
|
||||
## Builds the Komodo Core and Periphery binaries
|
||||
## for a specific architecture.
|
||||
|
||||
FROM rust:1.82.0-bullseye AS builder
|
||||
|
||||
WORKDIR /builder
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY ./lib ./lib
|
||||
COPY ./client/core/rs ./client/core/rs
|
||||
COPY ./client/periphery ./client/periphery
|
||||
COPY ./bin/core ./bin/core
|
||||
COPY ./bin/periphery ./bin/periphery
|
||||
|
||||
# Compile bin
|
||||
RUN \
|
||||
cargo build -p komodo_core --release && \
|
||||
cargo build -p komodo_periphery --release
|
||||
|
||||
# Copy just the binaries to scratch image
|
||||
FROM scratch
|
||||
|
||||
COPY --from=builder /builder/target/release/core /core
|
||||
COPY --from=builder /builder/target/release/periphery /periphery
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/mbecker20/komodo
|
||||
LABEL org.opencontainers.image.description="Komodo Periphery"
|
||||
LABEL org.opencontainers.image.licenses=GPL-3.0
|
||||
@@ -59,6 +59,7 @@ dotenvy.workspace = true
|
||||
anyhow.workspace = true
|
||||
bcrypt.workspace = true
|
||||
base64.workspace = true
|
||||
rustls.workspace = true
|
||||
tokio.workspace = true
|
||||
serde.workspace = true
|
||||
regex.workspace = true
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
## This one produces smaller images,
|
||||
## but alpine uses `musl` instead of `glibc`.
|
||||
## This makes it take longer / more resources to build,
|
||||
## and may negatively affect runtime performance.
|
||||
## All in one, multi stage compile + runtime Docker build for your architecture.
|
||||
|
||||
# Build Core
|
||||
FROM rust:1.82.0-alpine AS core-builder
|
||||
FROM rust:1.82.0-bullseye AS core-builder
|
||||
|
||||
WORKDIR /builder
|
||||
RUN apk update && apk --no-cache add musl-dev openssl-dev openssl-libs-static
|
||||
COPY . .
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY ./lib ./lib
|
||||
COPY ./client/core/rs ./client/core/rs
|
||||
COPY ./client/periphery ./client/periphery
|
||||
COPY ./bin/core ./bin/core
|
||||
|
||||
# Compile app
|
||||
RUN cargo build -p komodo_core --release
|
||||
|
||||
# Build Frontend
|
||||
@@ -19,34 +22,34 @@ RUN cd client && yarn && yarn build && yarn link
|
||||
RUN cd frontend && yarn link komodo_client && yarn && yarn build
|
||||
|
||||
# Final Image
|
||||
FROM alpine:3.20
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
# Install Deps
|
||||
RUN apk update && apk add --no-cache --virtual .build-deps \
|
||||
openssl ca-certificates git git-lfs curl
|
||||
RUN apt update && \
|
||||
apt install -y git ca-certificates && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Setup an application directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy
|
||||
COPY ./config/core.config.toml /config/config.toml
|
||||
COPY --from=core-builder /builder/target/release/core /app
|
||||
COPY --from=frontend-builder /builder/frontend/dist /app/frontend
|
||||
COPY --from=core-builder /builder/target/release/core /usr/local/bin/core
|
||||
COPY --from=denoland/deno:bin /deno /usr/local/bin/deno
|
||||
|
||||
# Set $DENO_DIR and preload external Deno deps
|
||||
ENV DENO_DIR=/action-cache/deno
|
||||
RUN mkdir /action-cache && \
|
||||
cd /action-cache && \
|
||||
deno install jsr:@std/yaml jsr:@std/toml
|
||||
cd /action-cache && \
|
||||
deno install jsr:@std/yaml jsr:@std/toml
|
||||
|
||||
# Hint at the port
|
||||
EXPOSE 9120
|
||||
EXPOSE 9120
|
||||
|
||||
# Label for Ghcr
|
||||
LABEL org.opencontainers.image.source=https://github.com/mbecker20/komodo
|
||||
LABEL org.opencontainers.image.description="Komodo Core"
|
||||
LABEL org.opencontainers.image.licenses=GPL-3.0
|
||||
|
||||
# Using ENTRYPOINT allows cli args to be passed, eg using "command" in docker compose.
|
||||
ENTRYPOINT [ "/app/core" ]
|
||||
ENTRYPOINT [ "core" ]
|
||||
50
bin/core/multi-arch.Dockerfile
Normal file
50
bin/core/multi-arch.Dockerfile
Normal file
@@ -0,0 +1,50 @@
|
||||
## Assumes the latest binaries for x86_64 and aarch64 are already built (by binaries.Dockerfile).
|
||||
## Sets up the necessary runtime container dependencies for Komodo Core.
|
||||
## Since theres no heavy build here, QEMU multi-arch builds are fine for this image.
|
||||
|
||||
ARG BINARIES_IMAGE=ghcr.io/mbecker20/komodo-binaries:latest
|
||||
ARG FRONTEND_IMAGE=ghcr.io/mbecker20/komodo-frontend:latest
|
||||
ARG X86_64_BINARIES=${BINARIES_IMAGE}-x86_64
|
||||
ARG AARCH64_BINARIES=${BINARIES_IMAGE}-aarch64
|
||||
|
||||
# This is required to work with COPY --from
|
||||
FROM ${X86_64_BINARIES} AS x86_64
|
||||
FROM ${AARCH64_BINARIES} AS aarch64
|
||||
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/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy both binaries initially, but only keep appropriate one for the TARGETPLATFORM.
|
||||
COPY --from=x86_64 /core /app/arch/linux/amd64
|
||||
COPY --from=aarch64 /core /app/arch/linux/arm64
|
||||
ARG TARGETPLATFORM
|
||||
RUN mv /app/arch/${TARGETPLATFORM} /usr/local/bin/core && rm -r /app/arch
|
||||
|
||||
# Copy default config / static frontend / deno binary
|
||||
COPY ./config/core.config.toml /config/config.toml
|
||||
COPY --from=frontend /frontend /app/frontend
|
||||
COPY --from=denoland/deno:bin /deno /usr/local/bin/deno
|
||||
|
||||
# Set $DENO_DIR and preload external Deno deps
|
||||
ENV DENO_DIR=/action-cache/deno
|
||||
RUN mkdir /action-cache && \
|
||||
cd /action-cache && \
|
||||
deno install jsr:@std/yaml jsr:@std/toml
|
||||
|
||||
# Hint at the port
|
||||
EXPOSE 9120
|
||||
|
||||
# Label for Ghcr
|
||||
LABEL org.opencontainers.image.source=https://github.com/mbecker20/komodo
|
||||
LABEL org.opencontainers.image.description="Komodo Core"
|
||||
LABEL org.opencontainers.image.licenses=GPL-3.0
|
||||
|
||||
ENTRYPOINT [ "core" ]
|
||||
@@ -1,8 +1,10 @@
|
||||
# Build Core
|
||||
FROM rust:1.82.0-bullseye AS core-builder
|
||||
WORKDIR /builder
|
||||
COPY . .
|
||||
RUN cargo build -p komodo_core --release
|
||||
## Assumes the latest binaries for the required arch are already built (by binaries.Dockerfile).
|
||||
## Sets up the necessary runtime container dependencies for Komodo Core.
|
||||
|
||||
ARG BINARIES_IMAGE=ghcr.io/mbecker20/komodo-binaries:latest
|
||||
|
||||
# This is required to work with COPY --from
|
||||
FROM ${BINARIES_IMAGE} AS binaries
|
||||
|
||||
# Build Frontend
|
||||
FROM node:20.12-alpine AS frontend-builder
|
||||
@@ -12,21 +14,17 @@ COPY ./client/core/ts ./client
|
||||
RUN cd client && yarn && yarn build && yarn link
|
||||
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/*
|
||||
|
||||
# Setup an application directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy
|
||||
COPY ./config/core.config.toml /config/config.toml
|
||||
COPY --from=core-builder /builder/target/release/core /app
|
||||
COPY --from=frontend-builder /builder/frontend/dist /app/frontend
|
||||
COPY --from=binaries /core /usr/local/bin/core
|
||||
COPY --from=denoland/deno:bin /deno /usr/local/bin/deno
|
||||
|
||||
# Set $DENO_DIR and preload external Deno deps
|
||||
@@ -43,4 +41,4 @@ LABEL org.opencontainers.image.source=https://github.com/mbecker20/komodo
|
||||
LABEL org.opencontainers.image.description="Komodo Core"
|
||||
LABEL org.opencontainers.image.licenses=GPL-3.0
|
||||
|
||||
ENTRYPOINT [ "/app/core" ]
|
||||
ENTRYPOINT [ "core" ]
|
||||
@@ -118,6 +118,13 @@ impl Resolve<DeployStack, (User, Update)> for State {
|
||||
let mut global_replacers = HashSet::new();
|
||||
let mut secret_replacers = HashSet::new();
|
||||
|
||||
interpolate_variables_secrets_into_string(
|
||||
&vars_and_secrets,
|
||||
&mut stack.config.file_contents,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
interpolate_variables_secrets_into_string(
|
||||
&vars_and_secrets,
|
||||
&mut stack.config.environment,
|
||||
|
||||
@@ -212,21 +212,37 @@ async fn terminate_ec2_instance_inner(
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Automatically retries 5 times, waiting 2 sec in between
|
||||
#[instrument(level = "debug")]
|
||||
async fn get_ec2_instance_status(
|
||||
client: &Client,
|
||||
instance_id: &str,
|
||||
) -> anyhow::Result<Option<InstanceStatus>> {
|
||||
let status = client
|
||||
.describe_instance_status()
|
||||
.instance_ids(instance_id)
|
||||
.send()
|
||||
let mut try_count = 1;
|
||||
loop {
|
||||
match async {
|
||||
anyhow::Ok(
|
||||
client
|
||||
.describe_instance_status()
|
||||
.instance_ids(instance_id)
|
||||
.send()
|
||||
.await
|
||||
.context("failed to describe instance status from aws")?
|
||||
.instance_statuses()
|
||||
.first()
|
||||
.cloned(),
|
||||
)
|
||||
}
|
||||
.await
|
||||
.context("failed to get instance status from aws")?
|
||||
.instance_statuses()
|
||||
.first()
|
||||
.cloned();
|
||||
Ok(status)
|
||||
{
|
||||
Ok(res) => return Ok(res),
|
||||
Err(e) if try_count > 4 => return Err(e),
|
||||
Err(_) => {
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
try_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
@@ -248,28 +264,43 @@ async fn get_ec2_instance_state_name(
|
||||
Ok(Some(state))
|
||||
}
|
||||
|
||||
/// Automatically retries 5 times, waiting 2 sec in between
|
||||
#[instrument(level = "debug")]
|
||||
async fn get_ec2_instance_public_ip(
|
||||
client: &Client,
|
||||
instance_id: &str,
|
||||
) -> anyhow::Result<String> {
|
||||
let ip = client
|
||||
.describe_instances()
|
||||
.instance_ids(instance_id)
|
||||
.send()
|
||||
let mut try_count = 1;
|
||||
loop {
|
||||
match async {
|
||||
anyhow::Ok(
|
||||
client
|
||||
.describe_instances()
|
||||
.instance_ids(instance_id)
|
||||
.send()
|
||||
.await
|
||||
.context("failed to describe instances from aws")?
|
||||
.reservations()
|
||||
.first()
|
||||
.context("instance reservations is empty")?
|
||||
.instances()
|
||||
.first()
|
||||
.context("instances is empty")?
|
||||
.public_ip_address()
|
||||
.context("instance has no public ip")?
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
.await
|
||||
.context("failed to get instance status from aws")?
|
||||
.reservations()
|
||||
.first()
|
||||
.context("instance reservations is empty")?
|
||||
.instances()
|
||||
.first()
|
||||
.context("instances is empty")?
|
||||
.public_ip_address()
|
||||
.context("instance has no public ip")?
|
||||
.to_string();
|
||||
|
||||
Ok(ip)
|
||||
{
|
||||
Ok(res) => return Ok(res),
|
||||
Err(e) if try_count > 4 => return Err(e),
|
||||
Err(_) => {
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
try_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_unknown_instance_type(
|
||||
|
||||
@@ -55,6 +55,7 @@ pub async fn get_builder_periphery(
|
||||
} else {
|
||||
config.passkey
|
||||
},
|
||||
Duration::from_secs(3),
|
||||
);
|
||||
periphery
|
||||
.health_check()
|
||||
@@ -122,7 +123,7 @@ async fn get_aws_builder(
|
||||
let periphery_address =
|
||||
format!("{protocol}://{ip}:{}", config.port);
|
||||
let periphery =
|
||||
PeripheryClient::new(&periphery_address, &core_config().passkey);
|
||||
PeripheryClient::new(&periphery_address, &core_config().passkey, Duration::from_secs(3));
|
||||
|
||||
let start_connect_ts = komodo_timestamp();
|
||||
let mut res = Ok(GetVersionResponse {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::str::FromStr;
|
||||
use std::{str::FromStr, time::Duration};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use futures::future::join_all;
|
||||
@@ -145,6 +145,7 @@ pub fn periphery_client(
|
||||
let client = PeripheryClient::new(
|
||||
&server.config.address,
|
||||
&core_config().passkey,
|
||||
Duration::from_secs(server.config.timeout_seconds as u64),
|
||||
);
|
||||
|
||||
Ok(client)
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::{net::SocketAddr, str::FromStr};
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::Router;
|
||||
use axum_server::tls_openssl::OpenSSLConfig;
|
||||
use axum_server::tls_rustls::RustlsConfig;
|
||||
use tower_http::{
|
||||
cors::{Any, CorsLayer},
|
||||
services::{ServeDir, ServeFile},
|
||||
@@ -89,13 +89,17 @@ async fn app() -> anyhow::Result<()> {
|
||||
|
||||
if config.ssl_enabled {
|
||||
info!("🔒 Core SSL Enabled");
|
||||
rustls::crypto::ring::default_provider()
|
||||
.install_default()
|
||||
.expect("failed to install default rustls CryptoProvider");
|
||||
info!("Komodo Core starting on https://{socket_addr}");
|
||||
let ssl_config = OpenSSLConfig::from_pem_file(
|
||||
let ssl_config = RustlsConfig::from_pem_file(
|
||||
&config.ssl_cert_file,
|
||||
&config.ssl_key_file,
|
||||
)
|
||||
.context("Failed to parse ssl ")?;
|
||||
axum_server::bind_openssl(socket_addr, ssl_config)
|
||||
.await
|
||||
.context("Invalid ssl cert / key")?;
|
||||
axum_server::bind_rustls(socket_addr, ssl_config)
|
||||
.serve(app)
|
||||
.await?
|
||||
} else {
|
||||
|
||||
@@ -245,9 +245,6 @@ pub async fn update_cache_for_server(server: &Server) {
|
||||
.await;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"could not get docker lists | (update status cache) | {e:#}"
|
||||
);
|
||||
insert_deployments_status_unknown(deployments).await;
|
||||
insert_stacks_status_unknown(stacks).await;
|
||||
insert_server_status(
|
||||
|
||||
@@ -26,6 +26,9 @@ pub async fn record_server_stats(ts: i64) {
|
||||
disk_total_gb,
|
||||
disk_used_gb,
|
||||
disks: stats.disks.clone(),
|
||||
network_ingress_bytes: stats.network_ingress_bytes,
|
||||
network_egress_bytes: stats.network_egress_bytes,
|
||||
network_usage_interface: stats.network_usage_interface.clone(),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -77,7 +77,15 @@ pub async fn update_deployment_cache(
|
||||
};
|
||||
format!("{build_name}:{version}")
|
||||
}
|
||||
DeploymentImage::Image { image } => image,
|
||||
DeploymentImage::Image { image } => {
|
||||
// If image already has tag, leave it,
|
||||
// otherwise default the tag to latest
|
||||
if image.contains(':') {
|
||||
image
|
||||
} else {
|
||||
format!("{image}:latest")
|
||||
}
|
||||
}
|
||||
};
|
||||
let update_available = if let Some(ContainerListItem {
|
||||
image_id: Some(curr_image_id),
|
||||
@@ -242,10 +250,18 @@ pub async fn update_stack_cache(
|
||||
}
|
||||
}.is_match(&container.name)
|
||||
}).cloned();
|
||||
// If image already has tag, leave it,
|
||||
// otherwise default the tag to latest
|
||||
let image = image.clone();
|
||||
let image = if image.contains(':') {
|
||||
image
|
||||
} else {
|
||||
image + ":latest"
|
||||
};
|
||||
let update_available = if let Some(ContainerListItem { image_id: Some(curr_image_id), .. }) = &container {
|
||||
images
|
||||
.iter()
|
||||
.find(|i| &i.name == image)
|
||||
.find(|i| i.name == image)
|
||||
.map(|i| &i.id != curr_image_id)
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
|
||||
@@ -41,6 +41,7 @@ bollard.workspace = true
|
||||
sysinfo.workspace = true
|
||||
dotenvy.workspace = true
|
||||
anyhow.workspace = true
|
||||
rustls.workspace = true
|
||||
tokio.workspace = true
|
||||
serde.workspace = true
|
||||
axum.workspace = true
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
# Build Periphery
|
||||
## All in one, multi stage compile + runtime Docker build for your architecture.
|
||||
|
||||
FROM rust:1.82.0-bullseye AS builder
|
||||
|
||||
WORKDIR /builder
|
||||
COPY . .
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY ./lib ./lib
|
||||
COPY ./client/core/rs ./client/core/rs
|
||||
COPY ./client/periphery ./client/periphery
|
||||
COPY ./bin/periphery ./bin/periphery
|
||||
|
||||
# Compile app
|
||||
RUN cargo build -p komodo_periphery --release
|
||||
|
||||
# Final Image
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
# # Install Deps
|
||||
COPY ./bin/periphery/debian-deps.sh .
|
||||
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
|
||||
|
||||
# Setup an application directory
|
||||
WORKDIR /app
|
||||
COPY --from=builder /builder/target/release/periphery /usr/local/bin/periphery
|
||||
|
||||
# Copy
|
||||
COPY --from=builder /builder/target/release/periphery /app
|
||||
|
||||
# Hint at the port
|
||||
EXPOSE 8120
|
||||
|
||||
# Label for Ghcr
|
||||
LABEL org.opencontainers.image.source=https://github.com/mbecker20/komodo
|
||||
LABEL org.opencontainers.image.description="Komodo Periphery"
|
||||
LABEL org.opencontainers.image.licenses=GPL-3.0
|
||||
|
||||
# Using ENTRYPOINT allows cli args to be passed, eg using "command" in docker compose.
|
||||
ENTRYPOINT [ "/app/periphery" ]
|
||||
CMD [ "periphery" ]
|
||||
@@ -1,35 +0,0 @@
|
||||
## This one produces smaller images,
|
||||
## but alpine uses `musl` instead of `glibc`.
|
||||
## This makes it take longer / more resources to build,
|
||||
## and may negatively affect runtime performance.
|
||||
|
||||
# Build Periphery
|
||||
FROM rust:1.82.0-alpine AS builder
|
||||
WORKDIR /builder
|
||||
COPY . .
|
||||
RUN apk update && apk --no-cache add musl-dev openssl-dev openssl-libs-static
|
||||
RUN cargo build -p komodo_periphery --release
|
||||
|
||||
# Final Image
|
||||
FROM alpine:3.20
|
||||
|
||||
# Install Deps
|
||||
RUN apk update && apk add --no-cache --virtual .build-deps \
|
||||
docker-cli docker-cli-compose openssl ca-certificates git git-lfs bash
|
||||
|
||||
# Setup an application directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy
|
||||
COPY --from=builder /builder/target/release/periphery /app
|
||||
|
||||
# Hint at the port
|
||||
EXPOSE 8120
|
||||
|
||||
# Label for Ghcr
|
||||
LABEL org.opencontainers.image.source=https://github.com/mbecker20/komodo
|
||||
LABEL org.opencontainers.image.description="Komodo Periphery"
|
||||
LABEL org.opencontainers.image.licenses=GPL-3.0
|
||||
|
||||
# Using ENTRYPOINT allows cli args to be passed, eg using "command" in docker compose.
|
||||
ENTRYPOINT [ "/app/periphery" ]
|
||||
33
bin/periphery/multi-arch.Dockerfile
Normal file
33
bin/periphery/multi-arch.Dockerfile
Normal file
@@ -0,0 +1,33 @@
|
||||
## Assumes the latest binaries for x86_64 and aarch64 are already built (by binaries.Dockerfile).
|
||||
## Sets up the necessary runtime container dependencies for Komodo Periphery.
|
||||
## Since theres no heavy build here, QEMU multi-arch builds are fine for this image.
|
||||
|
||||
ARG BINARIES_IMAGE=ghcr.io/mbecker20/komodo-binaries:latest
|
||||
ARG X86_64_BINARIES=${BINARIES_IMAGE}-x86_64
|
||||
ARG AARCH64_BINARIES=${BINARIES_IMAGE}-aarch64
|
||||
|
||||
# This is required to work with COPY --from
|
||||
FROM ${X86_64_BINARIES} AS x86_64
|
||||
FROM ${AARCH64_BINARIES} AS aarch64
|
||||
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
COPY ./bin/periphery/debian-deps.sh .
|
||||
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
## Copy both binaries initially, but only keep appropriate one for the TARGETPLATFORM.
|
||||
COPY --from=x86_64 /periphery /app/arch/linux/amd64
|
||||
COPY --from=aarch64 /periphery /app/arch/linux/arm64
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
RUN mv /app/arch/${TARGETPLATFORM} /usr/local/bin/periphery && rm -r /app/arch
|
||||
|
||||
EXPOSE 8120
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/mbecker20/komodo
|
||||
LABEL org.opencontainers.image.description="Komodo Periphery"
|
||||
LABEL org.opencontainers.image.licenses=GPL-3.0
|
||||
|
||||
CMD [ "periphery" ]
|
||||
23
bin/periphery/single-arch.Dockerfile
Normal file
23
bin/periphery/single-arch.Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
||||
## Assumes the latest binaries for the required arch are already built (by binaries.Dockerfile).
|
||||
## Sets up the necessary runtime container dependencies for Komodo Periphery.
|
||||
|
||||
ARG BINARIES_IMAGE=ghcr.io/mbecker20/komodo-binaries:latest
|
||||
|
||||
# This is required to work with COPY --from
|
||||
FROM ${BINARIES_IMAGE} AS binaries
|
||||
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
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
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/mbecker20/komodo
|
||||
LABEL org.opencontainers.image.description="Komodo Periphery"
|
||||
LABEL org.opencontainers.image.licenses=GPL-3.0
|
||||
|
||||
CMD [ "periphery" ]
|
||||
@@ -56,3 +56,4 @@ impl ResolveToString<GetSystemProcesses> for State {
|
||||
.context("failed to serialize response to string")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -507,11 +507,33 @@ pub async fn write_stack(
|
||||
)
|
||||
.components()
|
||||
.collect::<PathBuf>();
|
||||
fs::write(&file_path, &stack.config.file_contents)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("failed to write compose file to {file_path:?}")
|
||||
})?;
|
||||
|
||||
let file_contents = if !stack.config.skip_secret_interp {
|
||||
let (contents, replacers) = svi::interpolate_variables(
|
||||
&stack.config.file_contents,
|
||||
&periphery_config().secrets,
|
||||
svi::Interpolator::DoubleBrackets,
|
||||
true,
|
||||
)
|
||||
.context("failed to interpolate secrets into file contents")?;
|
||||
if !replacers.is_empty() {
|
||||
res.logs().push(Log::simple(
|
||||
"Interpolate - Compose file",
|
||||
replacers
|
||||
.iter()
|
||||
.map(|(_, variable)| format!("<span class=\"text-muted-foreground\">replaced:</span> {variable}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
));
|
||||
}
|
||||
contents
|
||||
} else {
|
||||
stack.config.file_contents.clone()
|
||||
};
|
||||
|
||||
fs::write(&file_path, &file_contents).await.with_context(
|
||||
|| format!("failed to write compose file to {file_path:?}"),
|
||||
)?;
|
||||
|
||||
Ok((
|
||||
run_directory,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#[macro_use]
|
||||
extern crate tracing;
|
||||
|
||||
//
|
||||
use std::{net::SocketAddr, str::FromStr};
|
||||
|
||||
use anyhow::Context;
|
||||
use axum_server::tls_openssl::OpenSSLConfig;
|
||||
use axum_server::tls_rustls::RustlsConfig;
|
||||
|
||||
mod api;
|
||||
mod compose;
|
||||
@@ -36,14 +37,18 @@ async fn app() -> anyhow::Result<()> {
|
||||
|
||||
if config.ssl_enabled {
|
||||
info!("🔒 Periphery SSL Enabled");
|
||||
rustls::crypto::ring::default_provider()
|
||||
.install_default()
|
||||
.expect("failed to install default rustls CryptoProvider");
|
||||
ssl::ensure_certs().await;
|
||||
info!("Komodo Periphery starting on https://{}", socket_addr);
|
||||
let ssl_config = OpenSSLConfig::from_pem_file(
|
||||
let ssl_config = RustlsConfig::from_pem_file(
|
||||
&config.ssl_cert_file,
|
||||
&config.ssl_key_file,
|
||||
)
|
||||
.await
|
||||
.context("Invalid ssl cert / key")?;
|
||||
axum_server::bind_openssl(socket_addr, ssl_config)
|
||||
axum_server::bind_rustls(socket_addr, ssl_config)
|
||||
.serve(app)
|
||||
.await?
|
||||
} else {
|
||||
|
||||
@@ -2,9 +2,9 @@ use std::{cmp::Ordering, sync::OnceLock};
|
||||
|
||||
use async_timing_util::wait_until_timelength;
|
||||
use komodo_client::entities::stats::{
|
||||
SingleDiskUsage, SystemInformation, SystemProcess, SystemStats,
|
||||
SingleDiskUsage, SystemInformation, SystemProcess, SystemStats, SingleNetworkInterfaceUsage,
|
||||
};
|
||||
use sysinfo::System;
|
||||
use sysinfo::{ProcessesToUpdate, System};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::config::periphery_config;
|
||||
@@ -48,6 +48,7 @@ pub fn spawn_system_stats_polling_threads() {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
pub struct StatsClient {
|
||||
/// Cached system stats
|
||||
pub stats: SystemStats,
|
||||
@@ -57,6 +58,7 @@ pub struct StatsClient {
|
||||
// the handles used to get the stats
|
||||
system: sysinfo::System,
|
||||
disks: sysinfo::Disks,
|
||||
networks: sysinfo::Networks,
|
||||
}
|
||||
|
||||
const BYTES_PER_GB: f64 = 1073741824.0;
|
||||
@@ -67,6 +69,7 @@ impl Default for StatsClient {
|
||||
fn default() -> Self {
|
||||
let system = sysinfo::System::new_all();
|
||||
let disks = sysinfo::Disks::new_with_refreshed_list();
|
||||
let networks = sysinfo::Networks::new_with_refreshed_list();
|
||||
let stats = SystemStats {
|
||||
polling_rate: periphery_config().stats_polling_rate,
|
||||
..Default::default()
|
||||
@@ -75,6 +78,7 @@ impl Default for StatsClient {
|
||||
info: get_system_information(&system),
|
||||
system,
|
||||
disks,
|
||||
networks,
|
||||
stats,
|
||||
}
|
||||
}
|
||||
@@ -82,22 +86,55 @@ impl Default for StatsClient {
|
||||
|
||||
impl StatsClient {
|
||||
fn refresh(&mut self) {
|
||||
self.system.refresh_all();
|
||||
self.system.refresh_cpu_all();
|
||||
self.system.refresh_memory();
|
||||
self.system.refresh_processes(ProcessesToUpdate::All, true);
|
||||
self.disks.refresh();
|
||||
self.networks.refresh();
|
||||
}
|
||||
|
||||
fn refresh_lists(&mut self) {
|
||||
self.disks.refresh_list();
|
||||
self.networks.refresh_list();
|
||||
}
|
||||
|
||||
pub fn get_system_stats(&self) -> SystemStats {
|
||||
let total_mem = self.system.total_memory();
|
||||
let available_mem = self.system.available_memory();
|
||||
|
||||
let mut total_ingress: u64 = 0;
|
||||
let mut total_egress: u64 = 0;
|
||||
|
||||
// Fetch network data (Ingress and Egress)
|
||||
let network_usage: Vec<SingleNetworkInterfaceUsage> = self.networks
|
||||
.iter()
|
||||
.map(|(interface_name, network)| {
|
||||
let ingress = network.received();
|
||||
let egress = network.transmitted();
|
||||
|
||||
// Update total ingress and egress
|
||||
total_ingress += ingress;
|
||||
total_egress += egress;
|
||||
|
||||
// Return per-interface network stats
|
||||
SingleNetworkInterfaceUsage {
|
||||
name: interface_name.clone(),
|
||||
ingress_bytes: ingress as f64,
|
||||
egress_bytes: egress as f64,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
SystemStats {
|
||||
cpu_perc: self.system.global_cpu_usage(),
|
||||
mem_free_gb: self.system.free_memory() as f64 / BYTES_PER_GB,
|
||||
mem_used_gb: (total_mem - available_mem) as f64 / BYTES_PER_GB,
|
||||
mem_total_gb: total_mem as f64 / BYTES_PER_GB,
|
||||
// Added total ingress and egress
|
||||
network_ingress_bytes: total_ingress as f64,
|
||||
network_egress_bytes: total_egress as f64,
|
||||
network_usage_interface: network_usage,
|
||||
|
||||
disks: self.get_disks(),
|
||||
polling_rate: self.stats.polling_rate,
|
||||
refresh_ts: self.stats.refresh_ts,
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::deserializers::{
|
||||
use super::{
|
||||
alert::SeverityLevel,
|
||||
resource::{AddFilters, Resource, ResourceListItem, ResourceQuery},
|
||||
I64,
|
||||
};
|
||||
|
||||
#[typeshare]
|
||||
@@ -69,6 +70,13 @@ pub struct ServerConfig {
|
||||
#[partial_default(default_enabled())]
|
||||
pub enabled: bool,
|
||||
|
||||
/// The timeout used to reach the server in seconds.
|
||||
/// default: 2
|
||||
#[serde(default = "default_timeout_seconds")]
|
||||
#[builder(default = "default_timeout_seconds()")]
|
||||
#[partial_default(default_timeout_seconds())]
|
||||
pub timeout_seconds: I64,
|
||||
|
||||
/// Sometimes the system stats reports a mount path that is not desired.
|
||||
/// Use this field to filter it out from the report.
|
||||
#[serde(default, deserialize_with = "string_list_deserializer")]
|
||||
@@ -177,6 +185,10 @@ fn default_enabled() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn default_timeout_seconds() -> i64 {
|
||||
3
|
||||
}
|
||||
|
||||
fn default_stats_monitoring() -> bool {
|
||||
true
|
||||
}
|
||||
@@ -218,6 +230,7 @@ impl Default for ServerConfig {
|
||||
Self {
|
||||
address: Default::default(),
|
||||
enabled: default_enabled(),
|
||||
timeout_seconds: default_timeout_seconds(),
|
||||
ignore_mounts: Default::default(),
|
||||
stats_monitoring: default_stats_monitoring(),
|
||||
auto_prune: default_auto_prune(),
|
||||
|
||||
@@ -51,6 +51,15 @@ pub struct SystemStatsRecord {
|
||||
pub disk_total_gb: f64,
|
||||
/// Breakdown of individual disks, ie their usages, sizes, and mount points
|
||||
pub disks: Vec<SingleDiskUsage>,
|
||||
/// Network ingress usage in bytes
|
||||
#[serde(default)]
|
||||
pub network_ingress_bytes: f64,
|
||||
/// Network egress usage in bytes
|
||||
#[serde(default)]
|
||||
pub network_egress_bytes: f64,
|
||||
/// Network usage by interface name (ingress, egress in bytes)
|
||||
#[serde(default)]
|
||||
pub network_usage_interface: Vec<SingleNetworkInterfaceUsage>, // interface -> (ingress, egress)
|
||||
}
|
||||
|
||||
/// Realtime system stats data.
|
||||
@@ -71,7 +80,15 @@ pub struct SystemStats {
|
||||
pub mem_total_gb: f64,
|
||||
/// Breakdown of individual disks, ie their usages, sizes, and mount points
|
||||
pub disks: Vec<SingleDiskUsage>,
|
||||
|
||||
/// Network ingress usage in MB
|
||||
#[serde(default)]
|
||||
pub network_ingress_bytes: f64,
|
||||
/// Network egress usage in MB
|
||||
#[serde(default)]
|
||||
pub network_egress_bytes: f64,
|
||||
/// Network usage by interface name (ingress, egress in bytes)
|
||||
#[serde(default)]
|
||||
pub network_usage_interface: Vec<SingleNetworkInterfaceUsage>, // interface -> (ingress, egress)
|
||||
// metadata
|
||||
/// The rate the system stats are being polled from the system
|
||||
pub polling_rate: Timelength,
|
||||
@@ -95,6 +112,18 @@ pub struct SingleDiskUsage {
|
||||
pub total_gb: f64,
|
||||
}
|
||||
|
||||
/// Info for network interface usage.
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct SingleNetworkInterfaceUsage {
|
||||
/// The network interface name
|
||||
pub name: String,
|
||||
/// The ingress in bytes
|
||||
pub ingress_bytes: f64,
|
||||
/// The egress in bytes
|
||||
pub egress_bytes: f64,
|
||||
}
|
||||
|
||||
pub fn sum_disk_usage(disks: &[SingleDiskUsage]) -> TotalDiskUsage {
|
||||
disks
|
||||
.iter()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "komodo_client",
|
||||
"version": "1.16.11",
|
||||
"version": "1.16.12",
|
||||
"description": "Komodo client package",
|
||||
"homepage": "https://komo.do",
|
||||
"main": "dist/lib.js",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Generated by typeshare 1.12.0
|
||||
Generated by typeshare 1.13.2
|
||||
*/
|
||||
|
||||
export interface MongoIdObj {
|
||||
@@ -1483,6 +1483,11 @@ export interface ServerConfig {
|
||||
* default: true
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* The timeout used to reach the server in seconds.
|
||||
* default: 2
|
||||
*/
|
||||
timeout_seconds: I64;
|
||||
/**
|
||||
* Sometimes the system stats reports a mount path that is not desired.
|
||||
* Use this field to filter it out from the report.
|
||||
@@ -1804,6 +1809,16 @@ export interface SingleDiskUsage {
|
||||
total_gb: number;
|
||||
}
|
||||
|
||||
/** Info for network interface usage. */
|
||||
export interface SingleNetworkInterfaceUsage {
|
||||
/** The network interface name */
|
||||
name: string;
|
||||
/** The ingress in bytes */
|
||||
ingress_bytes: number;
|
||||
/** The egress in bytes */
|
||||
egress_bytes: number;
|
||||
}
|
||||
|
||||
export enum Timelength {
|
||||
OneSecond = "1-sec",
|
||||
FiveSeconds = "5-sec",
|
||||
@@ -1845,6 +1860,12 @@ export interface SystemStats {
|
||||
mem_total_gb: number;
|
||||
/** Breakdown of individual disks, ie their usages, sizes, and mount points */
|
||||
disks: SingleDiskUsage[];
|
||||
/** Network ingress usage in MB */
|
||||
network_ingress_bytes?: number;
|
||||
/** Network egress usage in MB */
|
||||
network_egress_bytes?: number;
|
||||
/** Network usage by interface name (ingress, egress in bytes) */
|
||||
network_usage_interface?: SingleNetworkInterfaceUsage[];
|
||||
/** The rate the system stats are being polled from the system */
|
||||
polling_rate: Timelength;
|
||||
/** Unix timestamp in milliseconds when stats were last polled */
|
||||
@@ -5090,6 +5111,12 @@ export interface SystemStatsRecord {
|
||||
disk_total_gb: number;
|
||||
/** Breakdown of individual disks, ie their usages, sizes, and mount points */
|
||||
disks: SingleDiskUsage[];
|
||||
/** Network ingress usage in bytes */
|
||||
network_ingress_bytes?: number;
|
||||
/** Network egress usage in bytes */
|
||||
network_egress_bytes?: number;
|
||||
/** Network usage by interface name (ingress, egress in bytes) */
|
||||
network_usage_interface?: SingleNetworkInterfaceUsage[];
|
||||
}
|
||||
|
||||
/** Response to [GetHistoricalServerStats]. */
|
||||
|
||||
@@ -170,7 +170,7 @@ pub struct ComposeUpResponse {
|
||||
/// Whether stack was successfully deployed
|
||||
pub deployed: bool,
|
||||
/// The stack services.
|
||||
///
|
||||
///
|
||||
/// Note. The "image" is after interpolation.
|
||||
#[serde(default)]
|
||||
pub services: Vec<StackServiceNames>,
|
||||
|
||||
@@ -20,6 +20,4 @@ pub struct GetSystemStats {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Request)]
|
||||
#[response(Vec<SystemProcess>)]
|
||||
pub struct GetSystemProcesses {}
|
||||
|
||||
//
|
||||
pub struct GetSystemProcesses {}
|
||||
@@ -23,16 +23,19 @@ fn periphery_http_client() -> &'static reqwest::Client {
|
||||
pub struct PeripheryClient {
|
||||
address: String,
|
||||
passkey: String,
|
||||
timeout: Duration,
|
||||
}
|
||||
|
||||
impl PeripheryClient {
|
||||
pub fn new(
|
||||
address: impl Into<String>,
|
||||
passkey: impl Into<String>,
|
||||
timeout: impl Into<Duration>,
|
||||
) -> PeripheryClient {
|
||||
PeripheryClient {
|
||||
address: address.into(),
|
||||
passkey: passkey.into(),
|
||||
timeout: timeout.into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +58,7 @@ impl PeripheryClient {
|
||||
#[tracing::instrument(level = "debug", skip(self))]
|
||||
pub async fn health_check(&self) -> anyhow::Result<()> {
|
||||
self
|
||||
.request_inner(api::GetHealth {}, Some(Duration::from_secs(1)))
|
||||
.request_inner(api::GetHealth {}, Some(self.timeout))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
###################################
|
||||
####################################
|
||||
# 🦎 KOMODO COMPOSE - VARIABLES 🦎 #
|
||||
###################################
|
||||
####################################
|
||||
|
||||
## These compose variables can be used with all Komodo deployment options.
|
||||
## Pass these variables to the compose up command using `--env-file komodo/compose.env`.
|
||||
## Additionally, they are passed to both Komodo Core and Komodo Periphery with `env_file: ./compose.env`,
|
||||
## so you can pass any additional environment variables to Core / Periphery directly in this file as well.
|
||||
|
||||
## 🚨 Uncomment below for arm64 support 🚨
|
||||
# COMPOSE_KOMODO_IMAGE_TAG=latest-aarch64
|
||||
## Stick to a specific version, or use `latest`
|
||||
COMPOSE_KOMODO_IMAGE_TAG=latest
|
||||
|
||||
## Note: 🚨 Podman does NOT support local logging driver 🚨. See Podman options here:
|
||||
## `https://docs.podman.io/en/v4.6.1/markdown/podman-run.1.html#log-driver-driver`
|
||||
COMPOSE_LOGGING_DRIVER=local # Enable log rotation with the local driver.
|
||||
|
||||
## DB credentials - Ignored for Sqlite
|
||||
DB_USERNAME=admin
|
||||
DB_PASSWORD=admin
|
||||
KOMODO_DB_USERNAME=admin
|
||||
KOMODO_DB_PASSWORD=admin
|
||||
|
||||
## Configure a secure passkey to authenticate between Core / Periphery.
|
||||
PASSKEY=a_random_passkey
|
||||
KOMODO_PASSKEY=a_random_passkey
|
||||
|
||||
#=-------------------------=#
|
||||
#= Komodo Core Environment =#
|
||||
@@ -52,8 +52,6 @@ KOMODO_MONITORING_INTERVAL="15-sec"
|
||||
## Default: 5-min
|
||||
KOMODO_RESOURCE_POLL_INTERVAL="5-min"
|
||||
|
||||
## Used to auth against periphery. Alt: KOMODO_PASSKEY_FILE
|
||||
KOMODO_PASSKEY=${PASSKEY}
|
||||
## Used to auth incoming webhooks. Alt: KOMODO_WEBHOOK_SECRET_FILE
|
||||
KOMODO_WEBHOOK_SECRET=a_random_secret
|
||||
## Used to generate jwt. Alt: KOMODO_JWT_SECRET_FILE
|
||||
@@ -115,8 +113,11 @@ KOMODO_HETZNER_TOKEN= # Alt: KOMODO_HETZNER_TOKEN_FILE
|
||||
## Full variable list + descriptions are available here:
|
||||
## 🦎 https://github.com/mbecker20/komodo/blob/main/config/periphery.config.toml 🦎
|
||||
|
||||
## Periphery passkeys must include KOMODO_PASSKEY to authenticate
|
||||
PERIPHERY_PASSKEYS=${PASSKEY}
|
||||
## Periphery passkeys must include KOMODO_PASSKEY to authenticate.
|
||||
PERIPHERY_PASSKEYS=${KOMODO_PASSKEY}
|
||||
|
||||
## Specify the root directory used by Periphery agent.
|
||||
PERIPHERY_ROOT_DIRECTORY=/etc/komodo
|
||||
|
||||
## Enable SSL using self signed certificates.
|
||||
## Connect to Periphery at https://address:8120.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
###############################
|
||||
################################
|
||||
# 🦎 KOMODO COMPOSE - MONGO 🦎 #
|
||||
###############################
|
||||
################################
|
||||
|
||||
## This compose file will deploy:
|
||||
## 1. MongoDB
|
||||
@@ -24,8 +24,8 @@ services:
|
||||
- mongo-data:/data/db
|
||||
- mongo-config:/data/configdb
|
||||
environment:
|
||||
MONGO_INITDB_ROOT_USERNAME: ${DB_USERNAME}
|
||||
MONGO_INITDB_ROOT_PASSWORD: ${DB_PASSWORD}
|
||||
MONGO_INITDB_ROOT_USERNAME: ${KOMODO_DB_USERNAME}
|
||||
MONGO_INITDB_ROOT_PASSWORD: ${KOMODO_DB_PASSWORD}
|
||||
|
||||
core:
|
||||
image: ghcr.io/mbecker20/komodo:${COMPOSE_KOMODO_IMAGE_TAG:-latest}
|
||||
@@ -43,8 +43,8 @@ services:
|
||||
env_file: ./compose.env
|
||||
environment:
|
||||
KOMODO_DATABASE_ADDRESS: mongo:27017
|
||||
KOMODO_DATABASE_USERNAME: ${DB_USERNAME}
|
||||
KOMODO_DATABASE_PASSWORD: ${DB_PASSWORD}
|
||||
KOMODO_DATABASE_USERNAME: ${KOMODO_DB_USERNAME}
|
||||
KOMODO_DATABASE_PASSWORD: ${KOMODO_DB_PASSWORD}
|
||||
volumes:
|
||||
## Core cache for repos for latest commit hash / contents
|
||||
- repo-cache:/repo-cache
|
||||
@@ -70,22 +70,21 @@ services:
|
||||
networks:
|
||||
- default
|
||||
env_file: ./compose.env
|
||||
environment:
|
||||
PERIPHERY_REPO_DIR: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/repos
|
||||
PERIPHERY_STACK_DIR: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/stacks
|
||||
PERIPHERY_SSL_KEY_FILE: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/ssl/key.pem
|
||||
PERIPHERY_SSL_CERT_FILE: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/ssl/cert.pem
|
||||
volumes:
|
||||
## Mount external docker socket
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
## Allow Periphery to see processes outside of container
|
||||
- /proc:/proc
|
||||
## use self signed certs in docker volume,
|
||||
## or mount your own signed certs.
|
||||
- ssl-certs:/etc/komodo/ssl
|
||||
## manage repos in a docker volume,
|
||||
## or change it to an accessible host directory.
|
||||
- repos:/etc/komodo/repos
|
||||
## manage stack files in a docker volume,
|
||||
## or change it to an accessible host directory.
|
||||
- stacks:/etc/komodo/stacks
|
||||
## Optionally mount a path to store compose files
|
||||
# - /path/to/compose:/host/compose
|
||||
## Specify the Periphery agent root directory.
|
||||
## Must be the same inside and outside the container,
|
||||
## or docker will get confused. See https://github.com/mbecker20/komodo/discussions/180.
|
||||
## Default: /etc/komodo.
|
||||
- ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}:${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}
|
||||
|
||||
volumes:
|
||||
# Mongo
|
||||
@@ -93,10 +92,6 @@ volumes:
|
||||
mongo-config:
|
||||
# Core
|
||||
repo-cache:
|
||||
# Periphery
|
||||
ssl-certs:
|
||||
repos:
|
||||
stacks:
|
||||
|
||||
networks:
|
||||
default: {}
|
||||
@@ -1,15 +1,15 @@
|
||||
##################################
|
||||
###################################
|
||||
# 🦎 KOMODO COMPOSE - POSTGRES 🦎 #
|
||||
##################################
|
||||
###################################
|
||||
|
||||
## This compose file will deploy:
|
||||
## 1. Postgres + FerretDB Mongo adapter
|
||||
## 1. Postgres + FerretDB Mongo adapter (https://www.ferretdb.com)
|
||||
## 2. Komodo Core
|
||||
## 3. Komodo Periphery
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
image: postgres:17
|
||||
labels:
|
||||
komodo.skip: # Prevent Komodo from stopping with StopAllContainers
|
||||
restart: unless-stopped
|
||||
@@ -22,12 +22,12 @@ services:
|
||||
volumes:
|
||||
- pg-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_USER=${DB_USERNAME}
|
||||
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||
- POSTGRES_USER=${KOMODO_DB_USERNAME}
|
||||
- POSTGRES_PASSWORD=${KOMODO_DB_PASSWORD}
|
||||
- POSTGRES_DB=${KOMODO_DATABASE_DB_NAME:-komodo}
|
||||
|
||||
ferretdb:
|
||||
image: ghcr.io/ferretdb/ferretdb
|
||||
image: ghcr.io/ferretdb/ferretdb:1
|
||||
labels:
|
||||
komodo.skip: # Prevent Komodo from stopping with StopAllContainers
|
||||
restart: unless-stopped
|
||||
@@ -57,7 +57,7 @@ services:
|
||||
- 9120:9120
|
||||
env_file: ./compose.env
|
||||
environment:
|
||||
KOMODO_DATABASE_URI: mongodb://${DB_USERNAME}:${DB_PASSWORD}@ferretdb:27017/${KOMODO_DATABASE_DB_NAME:-komodo}?authMechanism=PLAIN
|
||||
KOMODO_DATABASE_URI: mongodb://${KOMODO_DB_USERNAME}:${KOMODO_DB_PASSWORD}@ferretdb:27017/${KOMODO_DATABASE_DB_NAME:-komodo}?authMechanism=PLAIN
|
||||
volumes:
|
||||
## Core cache for repos for latest commit hash / contents
|
||||
- repo-cache:/repo-cache
|
||||
@@ -83,32 +83,27 @@ services:
|
||||
networks:
|
||||
- default
|
||||
env_file: ./compose.env
|
||||
environment:
|
||||
PERIPHERY_REPO_DIR: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/repos
|
||||
PERIPHERY_STACK_DIR: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/stacks
|
||||
PERIPHERY_SSL_KEY_FILE: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/ssl/key.pem
|
||||
PERIPHERY_SSL_CERT_FILE: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/ssl/cert.pem
|
||||
volumes:
|
||||
## Mount external docker socket
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
## Allow Periphery to see processes outside of container
|
||||
- /proc:/proc
|
||||
## use self signed certs in docker volume,
|
||||
## or mount your own signed certs.
|
||||
- ssl-certs:/etc/komodo/ssl
|
||||
## manage repos in a docker volume,
|
||||
## or change it to an accessible host directory.
|
||||
- repos:/etc/komodo/repos
|
||||
## manage stack files in a docker volume,
|
||||
## or change it to an accessible host directory.
|
||||
- stacks:/etc/komodo/stacks
|
||||
## Optionally mount a path to store compose files
|
||||
# - /path/to/compose:/host/compose
|
||||
## Specify the Periphery agent root directory.
|
||||
## Must be the same inside and outside the container,
|
||||
## or docker will get confused. See https://github.com/mbecker20/komodo/discussions/180.
|
||||
## Default: /etc/komodo.
|
||||
- ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}:${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}
|
||||
|
||||
volumes:
|
||||
# Postgres
|
||||
pg-data:
|
||||
# Core
|
||||
repo-cache:
|
||||
# Periphery
|
||||
ssl-certs:
|
||||
repos:
|
||||
stacks:
|
||||
|
||||
networks:
|
||||
default: {}
|
||||
@@ -1,15 +1,15 @@
|
||||
################################
|
||||
#################################
|
||||
# 🦎 KOMODO COMPOSE - SQLITE 🦎 #
|
||||
################################
|
||||
#################################
|
||||
|
||||
## This compose file will deploy:
|
||||
## 1. Sqlite + FerretDB Mongo adapter
|
||||
## 1. Sqlite + FerretDB Mongo adapter (https://www.ferretdb.com)
|
||||
## 2. Komodo Core
|
||||
## 3. Komodo Periphery
|
||||
|
||||
services:
|
||||
ferretdb:
|
||||
image: ghcr.io/ferretdb/ferretdb
|
||||
image: ghcr.io/ferretdb/ferretdb:1
|
||||
labels:
|
||||
komodo.skip: # Prevent Komodo from stopping with StopAllContainers
|
||||
restart: unless-stopped
|
||||
@@ -65,32 +65,27 @@ services:
|
||||
networks:
|
||||
- default
|
||||
env_file: ./compose.env
|
||||
environment:
|
||||
PERIPHERY_REPO_DIR: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/repos
|
||||
PERIPHERY_STACK_DIR: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/stacks
|
||||
PERIPHERY_SSL_KEY_FILE: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/ssl/key.pem
|
||||
PERIPHERY_SSL_CERT_FILE: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/ssl/cert.pem
|
||||
volumes:
|
||||
## Mount external docker socket
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
## Allow Periphery to see processes outside of container
|
||||
- /proc:/proc
|
||||
## use self signed certs in docker volume,
|
||||
## or mount your own signed certs.
|
||||
- ssl-certs:/etc/komodo/ssl
|
||||
## manage repos in a docker volume,
|
||||
## or change it to an accessible host directory.
|
||||
- repos:/etc/komodo/repos
|
||||
## manage stack files in a docker volume,
|
||||
## or change it to an accessible host directory.
|
||||
- stacks:/etc/komodo/stacks
|
||||
## Optionally mount a path to store compose files
|
||||
# - /path/to/compose:/host/compose
|
||||
## Specify the Periphery agent root directory.
|
||||
## Must be the same inside and outside the container,
|
||||
## or docker will get confused. See https://github.com/mbecker20/komodo/discussions/180.
|
||||
## Default: /etc/komodo.
|
||||
- ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}:${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}
|
||||
|
||||
volumes:
|
||||
# Sqlite
|
||||
sqlite-data:
|
||||
# Core
|
||||
repo-cache:
|
||||
# Periphery
|
||||
ssl-certs:
|
||||
repos:
|
||||
stacks:
|
||||
|
||||
networks:
|
||||
default: {}
|
||||
@@ -455,7 +455,7 @@ hetzner.token = ""
|
||||
## and will be hidden in the UI and logs.
|
||||
## These are available to use on any Periphery (Server),
|
||||
## but you can also limit access more by placing them in a single Periphery's config file instead.
|
||||
## These cannot be configured on the environment.
|
||||
## These cannot be configured in the Komodo Core environment, they must be passed in the file.
|
||||
|
||||
# [secrets]
|
||||
# SECRET_1 = "value_1"
|
||||
|
||||
@@ -42,6 +42,7 @@ To run a full Komodo instance from a non-container environment run commands in t
|
||||
* `run -r test-core` -- Build and run Core API
|
||||
* `run -r test-periphery` -- Build and run Periphery API
|
||||
* Build Frontend
|
||||
* Install **typeshare-cli**: `cargo install typeshare-cli`
|
||||
* **Run this once** -- `run -r link-client` -- generates TS client and links to the frontend
|
||||
* After running the above once:
|
||||
* `run -r gen-client` -- Rebuild client
|
||||
|
||||
@@ -14,9 +14,9 @@ Komodo is able to support Postgres and Sqlite by utilizing the [FerretDB Mongo A
|
||||
|
||||
### First login
|
||||
|
||||
Core should now be accessible on the specified port, so navigating to `http://<address>:<port>` will display the login page.
|
||||
|
||||
The first user to log in will be auto enabled and made an admin. Any additional users to create accounts will be disabled by default, and must be enabled by an admin.
|
||||
Core should now be accessible on the specified port, so navigating to `http://<address>:<port>` will display the login page.
|
||||
On first login, you need to click `sign-up`, _not_ login, to create an initial admin user for Komodo.
|
||||
Any additional users to create accounts will be disabled by default, and must be enabled by an admin.
|
||||
|
||||
### Https
|
||||
|
||||
|
||||
@@ -220,6 +220,20 @@ cp ./target/release/periphery /root/periphery
|
||||
"""
|
||||
```
|
||||
|
||||
### Resource sync
|
||||
|
||||
- [Resource sync config schema](https://docs.rs/komodo_client/latest/komodo_client/entities/sync/type.ResourceSync.html)
|
||||
|
||||
```toml
|
||||
[[resource_sync]]
|
||||
name = "resource-sync"
|
||||
[resource_sync.config]
|
||||
git_provider = "git.mogh.tech" # use an alternate git provider (default is github.com)
|
||||
git_account = "mbecker20"
|
||||
repo = "mbecker20/komodo"
|
||||
resource_path = ["stacks.toml", "repos.toml"]
|
||||
```
|
||||
|
||||
### User Group:
|
||||
|
||||
- [UserGroup schema](https://docs.rs/komodo_client/latest/komodo_client/entities/toml/struct.UserGroupToml.html)
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
FROM node:20.12-alpine
|
||||
FROM node:20.12-alpine as builder
|
||||
|
||||
WORKDIR /app
|
||||
WORKDIR /builder
|
||||
|
||||
COPY ./frontend ./frontend
|
||||
COPY ./client/core/ts ./client
|
||||
|
||||
ARG VITE_KOMODO_HOST
|
||||
ENV VITE_KOMODO_HOST ${VITE_KOMODO_HOST}
|
||||
# Optionally specify a specific Komodo host.
|
||||
ARG VITE_KOMODO_HOST=""
|
||||
ENV VITE_KOMODO_HOST=${VITE_KOMODO_HOST}
|
||||
|
||||
# Build and link the client
|
||||
RUN cd client && yarn && yarn build && yarn link
|
||||
RUN cd frontend && yarn link komodo_client && yarn && yarn build
|
||||
|
||||
ENV PORT 4174
|
||||
# Copy just the static frontend to scratch image
|
||||
FROM scratch
|
||||
|
||||
CMD cd frontend && yarn preview --host --port ${PORT}
|
||||
COPY --from=builder /builder/frontend/dist /frontend
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/mbecker20/komodo
|
||||
LABEL org.opencontainers.image.description="Komodo Periphery"
|
||||
LABEL org.opencontainers.image.licenses=GPL-3.0
|
||||
26
frontend/public/client/types.d.ts
vendored
26
frontend/public/client/types.d.ts
vendored
@@ -1580,6 +1580,11 @@ export interface ServerConfig {
|
||||
* default: true
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* The timeout used to reach the server in seconds.
|
||||
* default: 2
|
||||
*/
|
||||
timeout_seconds: I64;
|
||||
/**
|
||||
* Sometimes the system stats reports a mount path that is not desired.
|
||||
* Use this field to filter it out from the report.
|
||||
@@ -1889,6 +1894,15 @@ export interface SingleDiskUsage {
|
||||
/** Total size of the disk in GB */
|
||||
total_gb: number;
|
||||
}
|
||||
/** Info for network interface usage. */
|
||||
export interface SingleNetworkInterfaceUsage {
|
||||
/** The network interface name */
|
||||
name: string;
|
||||
/** The ingress in bytes */
|
||||
ingress_bytes: number;
|
||||
/** The egress in bytes */
|
||||
egress_bytes: number;
|
||||
}
|
||||
export declare enum Timelength {
|
||||
OneSecond = "1-sec",
|
||||
FiveSeconds = "5-sec",
|
||||
@@ -1929,6 +1943,12 @@ export interface SystemStats {
|
||||
mem_total_gb: number;
|
||||
/** Breakdown of individual disks, ie their usages, sizes, and mount points */
|
||||
disks: SingleDiskUsage[];
|
||||
/** Network ingress usage in MB */
|
||||
network_ingress_bytes?: number;
|
||||
/** Network egress usage in MB */
|
||||
network_egress_bytes?: number;
|
||||
/** Network usage by interface name (ingress, egress in bytes) */
|
||||
network_usage_interface?: SingleNetworkInterfaceUsage[];
|
||||
/** The rate the system stats are being polled from the system */
|
||||
polling_rate: Timelength;
|
||||
/** Unix timestamp in milliseconds when stats were last polled */
|
||||
@@ -4837,6 +4857,12 @@ export interface SystemStatsRecord {
|
||||
disk_total_gb: number;
|
||||
/** Breakdown of individual disks, ie their usages, sizes, and mount points */
|
||||
disks: SingleDiskUsage[];
|
||||
/** Network ingress usage in bytes */
|
||||
network_ingress_bytes?: number;
|
||||
/** Network egress usage in bytes */
|
||||
network_egress_bytes?: number;
|
||||
/** Network usage by interface name (ingress, egress in bytes) */
|
||||
network_usage_interface?: SingleNetworkInterfaceUsage[];
|
||||
}
|
||||
/** Response to [GetHistoricalServerStats]. */
|
||||
export interface GetHistoricalServerStatsResponse {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Generated by typeshare 1.12.0
|
||||
Generated by typeshare 1.13.2
|
||||
*/
|
||||
/** The levels of permission that a User or UserGroup can have on a resource. */
|
||||
export var PermissionLevel;
|
||||
|
||||
193
frontend/src/components/group-actions.tsx
Normal file
193
frontend/src/components/group-actions.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import { useSelectedResources, useExecute, useWrite } from "@lib/hooks";
|
||||
import { UsableResource } from "@types";
|
||||
import { Button } from "@ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@ui/dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@ui/dropdown-menu";
|
||||
import { Input } from "@ui/input";
|
||||
import { Types } from "komodo_client";
|
||||
import { ChevronDown, CheckCircle } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { ConfirmButton } from "./util";
|
||||
import { useToast } from "@ui/use-toast";
|
||||
import { usableResourceExecuteKey } from "@lib/utils";
|
||||
|
||||
export const GroupActions = <
|
||||
T extends Types.ExecuteRequest["type"] | Types.WriteRequest["type"],
|
||||
>({
|
||||
type,
|
||||
actions,
|
||||
}: {
|
||||
type: UsableResource;
|
||||
actions: T[];
|
||||
}) => {
|
||||
const [action, setAction] = useState<T>();
|
||||
const [selected] = useSelectedResources(type);
|
||||
|
||||
return (
|
||||
<>
|
||||
<GroupActionDropdownMenu
|
||||
type={type}
|
||||
actions={actions}
|
||||
onSelect={setAction}
|
||||
disabled={!selected.length}
|
||||
/>
|
||||
<GroupActionDialog
|
||||
type={type}
|
||||
action={action}
|
||||
onClose={() => setAction(undefined)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const GroupActionDropdownMenu = <
|
||||
T extends Types.ExecuteRequest["type"] | Types.WriteRequest["type"],
|
||||
>({
|
||||
type,
|
||||
actions,
|
||||
onSelect,
|
||||
disabled,
|
||||
}: {
|
||||
type: UsableResource;
|
||||
actions: T[];
|
||||
onSelect: (item: T) => void;
|
||||
disabled: boolean;
|
||||
}) => (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild disabled={disabled}>
|
||||
<Button variant="outline" className="w-40 justify-between">
|
||||
Group Actions <ChevronDown className="w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
className={type === "Server" ? "w-56" : "w-40"}
|
||||
>
|
||||
{type === "ResourceSync" && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => onSelect("RefreshResourceSyncPending" as any)}
|
||||
>
|
||||
<Button variant="secondary" className="w-full">
|
||||
Refresh
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{actions.map((action) => (
|
||||
<DropdownMenuItem key={action} onClick={() => onSelect(action)}>
|
||||
<Button variant="secondary" className="w-full">
|
||||
{action.replaceAll("Batch", "").replaceAll(type, "")}
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuItem onClick={() => onSelect(`Delete${type}` as any)}>
|
||||
<Button variant="destructive" className="w-full">
|
||||
Delete
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
||||
const GroupActionDialog = ({
|
||||
type,
|
||||
action,
|
||||
onClose,
|
||||
}: {
|
||||
type: UsableResource;
|
||||
action:
|
||||
| (Types.ExecuteRequest["type"] | Types.WriteRequest["type"])
|
||||
| undefined;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const { toast } = useToast();
|
||||
const [selected, setSelected] = useSelectedResources(type);
|
||||
const [text, setText] = useState("");
|
||||
|
||||
const { mutate: execute, isPending: executePending } = useExecute(
|
||||
action! as Types.ExecuteRequest["type"],
|
||||
{
|
||||
onSuccess: onClose,
|
||||
}
|
||||
);
|
||||
const { mutate: write, isPending: writePending } = useWrite(
|
||||
action! as Types.WriteRequest["type"],
|
||||
{
|
||||
onSuccess: onClose,
|
||||
}
|
||||
);
|
||||
|
||||
if (!action) return;
|
||||
|
||||
const formatted = action.replaceAll("Batch", "").replaceAll(type, "");
|
||||
const isPending = executePending || writePending;
|
||||
|
||||
return (
|
||||
<Dialog open={!!action} onOpenChange={(o) => !o && onClose()}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Group Execute - {formatted}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-8 flex flex-col gap-4">
|
||||
<ul className="p-4 bg-accent text-sm list-disc list-inside">
|
||||
{selected.map((resource) => (
|
||||
<li key={resource}>{resource}</li>
|
||||
))}
|
||||
</ul>
|
||||
{!action.startsWith("Refresh") && (
|
||||
<>
|
||||
<p
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(formatted);
|
||||
toast({ title: `Copied "${formatted}" to clipboard!` });
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
Please enter <b>{formatted}</b> below to confirm this action.
|
||||
<br />
|
||||
<span className="text-xs text-muted-foreground">
|
||||
You may click the action in bold to copy it
|
||||
</span>
|
||||
</p>
|
||||
<Input value={text} onChange={(e) => setText(e.target.value)} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<ConfirmButton
|
||||
title="Confirm"
|
||||
icon={<CheckCircle className="w-4 h-4" />}
|
||||
onClick={() => {
|
||||
for (const resource of selected) {
|
||||
if (action.startsWith("Delete")) {
|
||||
write({ id: resource } as any);
|
||||
} else if (action.startsWith("Refresh")) {
|
||||
write({ [usableResourceExecuteKey(type)]: resource } as any);
|
||||
} else {
|
||||
execute({
|
||||
[usableResourceExecuteKey(type)]: resource,
|
||||
} as any);
|
||||
}
|
||||
}
|
||||
if (action.startsWith("Delete")) {
|
||||
setSelected([]);
|
||||
}
|
||||
}}
|
||||
disabled={action.startsWith("Refresh") ? false : text !== formatted}
|
||||
loading={isPending}
|
||||
/>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -17,6 +17,7 @@ import { cn } from "@lib/utils";
|
||||
import { Types } from "komodo_client";
|
||||
import { DashboardPieChart } from "@pages/home/dashboard";
|
||||
import { RenameResource } from "@components/config/util";
|
||||
import { GroupActions } from "@components/group-actions";
|
||||
|
||||
const useAction = (id?: string) =>
|
||||
useRead("ListActions", {}).data?.find((d) => d.id === id);
|
||||
@@ -61,6 +62,8 @@ export const ActionComponents: RequiredResourceComponents = {
|
||||
|
||||
New: () => <NewResource type="Action" />,
|
||||
|
||||
GroupActions: () => <GroupActions type="Action" actions={["RunAction"]} />,
|
||||
|
||||
Table: ({ resources }) => (
|
||||
<ActionTable actions={resources as Types.ActionListItem[]} />
|
||||
),
|
||||
|
||||
@@ -3,16 +3,23 @@ import { TableTags } from "@components/tags";
|
||||
import { ResourceLink } from "../common";
|
||||
import { ActionComponents } from ".";
|
||||
import { Types } from "komodo_client";
|
||||
import { useSelectedResources } from "@lib/hooks";
|
||||
|
||||
export const ActionTable = ({
|
||||
actions,
|
||||
}: {
|
||||
actions: Types.ActionListItem[];
|
||||
}) => {
|
||||
const [_, setSelectedResources] = useSelectedResources("Action");
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
tableKey="actions"
|
||||
data={actions}
|
||||
selectOptions={{
|
||||
selectKey: ({ name }) => name,
|
||||
onSelect: setSelectedResources,
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
|
||||
@@ -9,6 +9,7 @@ import { AlerterTable } from "./table";
|
||||
import { Types } from "komodo_client";
|
||||
import { ResourcePageHeader } from "@components/util";
|
||||
import { RenameResource } from "@components/config/util";
|
||||
import { GroupActions } from "@components/group-actions";
|
||||
|
||||
const useAlerter = (id?: string) =>
|
||||
useRead("ListAlerters", {}).data?.find((d) => d.id === id);
|
||||
@@ -43,6 +44,8 @@ export const AlerterComponents: RequiredResourceComponents = {
|
||||
return is_admin && <NewResource type="Alerter" />;
|
||||
},
|
||||
|
||||
GroupActions: () => <GroupActions type="Alerter" actions={[]} />,
|
||||
|
||||
Table: ({ resources }) => (
|
||||
<AlerterTable alerters={resources as Types.AlerterListItem[]} />
|
||||
),
|
||||
|
||||
@@ -2,16 +2,22 @@ import { DataTable, SortableHeader } from "@ui/data-table";
|
||||
import { ResourceLink } from "../common";
|
||||
import { TableTags } from "@components/tags";
|
||||
import { Types } from "komodo_client";
|
||||
import { useSelectedResources } from "@lib/hooks";
|
||||
|
||||
export const AlerterTable = ({
|
||||
alerters,
|
||||
}: {
|
||||
alerters: Types.AlerterListItem[];
|
||||
}) => {
|
||||
const [_, setSelectedResources] = useSelectedResources("Alerter");
|
||||
return (
|
||||
<DataTable
|
||||
tableKey="alerters"
|
||||
data={alerters}
|
||||
selectOptions={{
|
||||
selectKey: ({ name }) => name,
|
||||
onSelect: setSelectedResources,
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useToast } from "@ui/use-toast";
|
||||
import { Button } from "@ui/button";
|
||||
import { useBuilder } from "../builder";
|
||||
import { RenameResource } from "@components/config/util";
|
||||
import { GroupActions } from "@components/group-actions";
|
||||
|
||||
export const useBuild = (id?: string) =>
|
||||
useRead("ListBuilds", {}, { refetchInterval: 10_000 }).data?.find(
|
||||
@@ -129,6 +130,8 @@ export const BuildComponents: RequiredResourceComponents = {
|
||||
);
|
||||
},
|
||||
|
||||
GroupActions: () => <GroupActions type="Build" actions={["RunBuild"]} />,
|
||||
|
||||
Table: ({ resources }) => (
|
||||
<BuildTable builds={resources as Types.BuildListItem[]} />
|
||||
),
|
||||
|
||||
@@ -4,12 +4,19 @@ import { fmt_version } from "@lib/formatting";
|
||||
import { ResourceLink } from "../common";
|
||||
import { BuildComponents } from ".";
|
||||
import { Types } from "komodo_client";
|
||||
import { useSelectedResources } from "@lib/hooks";
|
||||
|
||||
export const BuildTable = ({ builds }: { builds: Types.BuildListItem[] }) => {
|
||||
const [_, setSelectedResources] = useSelectedResources("Build");
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
tableKey="builds"
|
||||
data={builds}
|
||||
selectOptions={{
|
||||
selectKey: ({ name }) => name,
|
||||
onSelect: setSelectedResources,
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
@@ -36,9 +43,7 @@ export const BuildTable = ({ builds }: { builds: Types.BuildListItem[] }) => {
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="State" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<BuildComponents.State id={row.original.id} />
|
||||
),
|
||||
cell: ({ row }) => <BuildComponents.State id={row.original.id} />,
|
||||
size: 120,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -20,6 +20,7 @@ import { DeleteResource, ResourceLink } from "../common";
|
||||
import { BuilderTable } from "./table";
|
||||
import { ResourcePageHeader } from "@components/util";
|
||||
import { RenameResource } from "@components/config/util";
|
||||
import { GroupActions } from "@components/group-actions";
|
||||
|
||||
export const useBuilder = (id?: string) =>
|
||||
useRead("ListBuilders", {}, { refetchInterval: 10_000 }).data?.find(
|
||||
@@ -63,6 +64,7 @@ export const BuilderComponents: RequiredResourceComponents = {
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
|
||||
New: () => {
|
||||
const is_admin = useUser().data?.admin;
|
||||
const nav = useNavigate();
|
||||
@@ -113,6 +115,8 @@ export const BuilderComponents: RequiredResourceComponents = {
|
||||
);
|
||||
},
|
||||
|
||||
GroupActions: () => <GroupActions type="Builder" actions={[]} />,
|
||||
|
||||
Table: ({ resources }) => (
|
||||
<BuilderTable builders={resources as Types.BuilderListItem[]} />
|
||||
),
|
||||
|
||||
@@ -3,16 +3,22 @@ import { ResourceLink } from "../common";
|
||||
import { TableTags } from "@components/tags";
|
||||
import { BuilderInstanceType } from ".";
|
||||
import { Types } from "komodo_client";
|
||||
import { useSelectedResources } from "@lib/hooks";
|
||||
|
||||
export const BuilderTable = ({
|
||||
builders,
|
||||
}: {
|
||||
builders: Types.BuilderListItem[];
|
||||
}) => {
|
||||
const [_, setSelectedResources] = useSelectedResources("Builder");
|
||||
return (
|
||||
<DataTable
|
||||
tableKey="builders"
|
||||
data={builders}
|
||||
selectOptions={{
|
||||
selectKey: ({ name }) => name,
|
||||
onSelect: setSelectedResources,
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
|
||||
@@ -159,8 +159,7 @@ export const PullDeployment = ({ id }: DeploymentId) => {
|
||||
if (!deployment) return null;
|
||||
|
||||
return (
|
||||
<ActionWithDialog
|
||||
name={deployment.name}
|
||||
<ConfirmButton
|
||||
title="Pull Image"
|
||||
icon={<Download className="h-4 w-4" />}
|
||||
onClick={() => pull({ deployment: id })}
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
import { RenameResource } from "@components/config/util";
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@ui/hover-card";
|
||||
import { Card } from "@ui/card";
|
||||
import { GroupActions } from "@components/group-actions";
|
||||
|
||||
// const configOrLog = atomWithStorage("config-or-log-v1", "Config");
|
||||
|
||||
@@ -174,6 +175,19 @@ export const DeploymentComponents: RequiredResourceComponents = {
|
||||
);
|
||||
},
|
||||
|
||||
GroupActions: () => (
|
||||
<GroupActions
|
||||
type="Deployment"
|
||||
actions={[
|
||||
"PullDeployment",
|
||||
"Deploy",
|
||||
"RestartDeployment",
|
||||
"StopDeployment",
|
||||
"DestroyDeployment",
|
||||
]}
|
||||
/>
|
||||
),
|
||||
|
||||
Icon: ({ id }) => <DeploymentIcon id={id} size={4} />,
|
||||
BigIcon: ({ id }) => <DeploymentIcon id={id} size={8} />,
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { TableTags } from "@components/tags";
|
||||
import { Types } from "komodo_client";
|
||||
import { DataTable, SortableHeader } from "@ui/data-table";
|
||||
import { useRead } from "@lib/hooks";
|
||||
import { useRead, useSelectedResources } from "@lib/hooks";
|
||||
import { ResourceLink } from "../common";
|
||||
import { DeploymentComponents } from ".";
|
||||
import { HardDrive } from "lucide-react";
|
||||
@@ -17,10 +17,17 @@ export const DeploymentTable = ({
|
||||
(id: string) => servers?.find((server) => server.id === id)?.name,
|
||||
[servers]
|
||||
);
|
||||
|
||||
const [_, setSelectedResources] = useSelectedResources("Deployment");
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
tableKey="deployments"
|
||||
data={deployments}
|
||||
selectOptions={{
|
||||
selectKey: ({ name }) => name,
|
||||
onSelect: setSelectedResources,
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
|
||||
@@ -17,6 +17,7 @@ import { cn } from "@lib/utils";
|
||||
import { Types } from "komodo_client";
|
||||
import { DashboardPieChart } from "@pages/home/dashboard";
|
||||
import { RenameResource } from "@components/config/util";
|
||||
import { GroupActions } from "@components/group-actions";
|
||||
|
||||
const useProcedure = (id?: string) =>
|
||||
useRead("ListProcedures", {}).data?.find((d) => d.id === id);
|
||||
@@ -63,6 +64,8 @@ export const ProcedureComponents: RequiredResourceComponents = {
|
||||
|
||||
New: () => <NewResource type="Procedure" />,
|
||||
|
||||
GroupActions: () => <GroupActions type="Procedure" actions={["RunProcedure"]} />,
|
||||
|
||||
Table: ({ resources }) => (
|
||||
<ProcedureTable procedures={resources as Types.ProcedureListItem[]} />
|
||||
),
|
||||
|
||||
@@ -3,16 +3,23 @@ import { TableTags } from "@components/tags";
|
||||
import { ResourceLink } from "../common";
|
||||
import { ProcedureComponents } from ".";
|
||||
import { Types } from "komodo_client";
|
||||
import { useSelectedResources } from "@lib/hooks";
|
||||
|
||||
export const ProcedureTable = ({
|
||||
procedures,
|
||||
}: {
|
||||
procedures: Types.ProcedureListItem[];
|
||||
}) => {
|
||||
const [_, setSelectedResources] = useSelectedResources("Procedure");
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
tableKey="procedures"
|
||||
data={procedures}
|
||||
selectOptions={{
|
||||
selectKey: ({ name }) => name,
|
||||
onSelect: setSelectedResources,
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
|
||||
@@ -21,6 +21,7 @@ import { useToast } from "@ui/use-toast";
|
||||
import { Button } from "@ui/button";
|
||||
import { useBuilder } from "../builder";
|
||||
import { RenameResource } from "@components/config/util";
|
||||
import { GroupActions } from "@components/group-actions";
|
||||
|
||||
export const useRepo = (id?: string) =>
|
||||
useRead("ListRepos", {}, { refetchInterval: 10_000 }).data?.find(
|
||||
@@ -70,6 +71,13 @@ export const RepoComponents: RequiredResourceComponents = {
|
||||
|
||||
New: ({ server_id }) => <NewResource type="Repo" server_id={server_id} />,
|
||||
|
||||
GroupActions: () => (
|
||||
<GroupActions
|
||||
type="Repo"
|
||||
actions={["PullRepo", "CloneRepo", "BuildRepo"]}
|
||||
/>
|
||||
),
|
||||
|
||||
Table: ({ resources }) => (
|
||||
<RepoTable repos={resources as Types.RepoListItem[]} />
|
||||
),
|
||||
|
||||
@@ -3,12 +3,19 @@ import { ResourceLink } from "../common";
|
||||
import { TableTags } from "@components/tags";
|
||||
import { RepoComponents } from ".";
|
||||
import { Types } from "komodo_client";
|
||||
import { useSelectedResources } from "@lib/hooks";
|
||||
|
||||
export const RepoTable = ({ repos }: { repos: Types.RepoListItem[] }) => {
|
||||
const [_, setSelectedResources] = useSelectedResources("Repo");
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
tableKey="repos"
|
||||
data={repos}
|
||||
selectOptions={{
|
||||
selectKey: ({ name }) => name,
|
||||
onSelect: setSelectedResources,
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
@@ -37,9 +44,7 @@ export const RepoTable = ({ repos }: { repos: Types.RepoListItem[] }) => {
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="State" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<RepoComponents.State id={row.original.id} />
|
||||
),
|
||||
cell: ({ row }) => <RepoComponents.State id={row.original.id} />,
|
||||
size: 120,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -22,6 +22,7 @@ import { ResourceSyncInfo } from "./info";
|
||||
import { ResourceSyncPending } from "./pending";
|
||||
import { Badge } from "@ui/badge";
|
||||
import { RenameResource } from "@components/config/util";
|
||||
import { GroupActions } from "@components/group-actions";
|
||||
|
||||
export const useResourceSync = (id?: string) =>
|
||||
useRead("ListResourceSyncs", {}, { refetchInterval: 10_000 }).data?.find(
|
||||
@@ -49,8 +50,8 @@ const ConfigInfoPending = ({ id }: { id: string }) => {
|
||||
const hideInfo = sync?.config?.files_on_host
|
||||
? false
|
||||
: sync?.config?.file_contents
|
||||
? true
|
||||
: false;
|
||||
? true
|
||||
: false;
|
||||
|
||||
const showPending =
|
||||
sync && (!sync_no_changes(sync) || sync.info?.pending_error);
|
||||
@@ -59,10 +60,10 @@ const ConfigInfoPending = ({ id }: { id: string }) => {
|
||||
_view === "Info" && hideInfo
|
||||
? "Config"
|
||||
: _view === "Pending" && !showPending
|
||||
? sync?.config?.files_on_host || sync?.config?.repo
|
||||
? "Info"
|
||||
: "Config"
|
||||
: _view;
|
||||
? sync?.config?.files_on_host || sync?.config?.repo
|
||||
? "Info"
|
||||
: "Config"
|
||||
: _view;
|
||||
|
||||
const title = (
|
||||
<TabsList className="justify-start w-fit">
|
||||
@@ -144,6 +145,10 @@ export const ResourceSyncComponents: RequiredResourceComponents = {
|
||||
);
|
||||
},
|
||||
|
||||
GroupActions: () => (
|
||||
<GroupActions type="ResourceSync" actions={["RunSync"]} />
|
||||
),
|
||||
|
||||
Table: ({ resources }) => (
|
||||
<ResourceSyncTable syncs={resources as Types.ResourceSyncListItem[]} />
|
||||
),
|
||||
|
||||
@@ -3,16 +3,22 @@ import { ResourceLink } from "../common";
|
||||
import { TableTags } from "@components/tags";
|
||||
import { Types } from "komodo_client";
|
||||
import { ResourceSyncComponents } from ".";
|
||||
import { useSelectedResources } from "@lib/hooks";
|
||||
|
||||
export const ResourceSyncTable = ({
|
||||
syncs,
|
||||
}: {
|
||||
syncs: Types.ResourceSyncListItem[];
|
||||
}) => {
|
||||
const [_, setSelectedResources] = useSelectedResources("ResourceSync");
|
||||
return (
|
||||
<DataTable
|
||||
tableKey="syncs"
|
||||
data={syncs}
|
||||
selectOptions={{
|
||||
selectKey: ({ name }) => name,
|
||||
onSelect: setSelectedResources,
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
|
||||
@@ -21,6 +21,7 @@ import { ServerTemplateTable } from "./table";
|
||||
import { LaunchServer } from "./actions";
|
||||
import { ResourcePageHeader } from "@components/util";
|
||||
import { RenameResource } from "@components/config/util";
|
||||
import { GroupActions } from "@components/group-actions";
|
||||
|
||||
export const useServerTemplate = (id?: string) =>
|
||||
useRead("ListServerTemplates", {}).data?.find((d) => d.id === id);
|
||||
@@ -96,6 +97,8 @@ export const ServerTemplateComponents: RequiredResourceComponents = {
|
||||
);
|
||||
},
|
||||
|
||||
GroupActions: () => <GroupActions type="ServerTemplate" actions={[]} />,
|
||||
|
||||
Table: ({ resources }) => (
|
||||
<ServerTemplateTable
|
||||
serverTemplates={resources as Types.ServerTemplateListItem[]}
|
||||
|
||||
@@ -2,16 +2,22 @@ import { DataTable, SortableHeader } from "@ui/data-table";
|
||||
import { ResourceLink } from "../common";
|
||||
import { TableTags } from "@components/tags";
|
||||
import { Types } from "komodo_client";
|
||||
import { useSelectedResources } from "@lib/hooks";
|
||||
|
||||
export const ServerTemplateTable = ({
|
||||
serverTemplates,
|
||||
}: {
|
||||
serverTemplates: Types.ServerTemplateListItem[];
|
||||
}) => {
|
||||
const [_, setSelectedResources] = useSelectedResources("ServerTemplate");
|
||||
return (
|
||||
<DataTable
|
||||
tableKey="server-templates"
|
||||
data={serverTemplates}
|
||||
selectOptions={{
|
||||
selectKey: ({ name }) => name,
|
||||
onSelect: setSelectedResources,
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
|
||||
@@ -72,6 +72,17 @@ export const ServerConfig = ({
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Timeout",
|
||||
labelHidden: true,
|
||||
components: {
|
||||
timeout_seconds: {
|
||||
// boldLabel: true,
|
||||
description:
|
||||
"The timeout used with the server health check, in seconds.",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Disks",
|
||||
labelHidden: true,
|
||||
|
||||
@@ -8,3 +8,10 @@ const statsGranularityAtom = atomWithStorage<Types.Timelength>(
|
||||
);
|
||||
|
||||
export const useStatsGranularity = () => useAtom(statsGranularityAtom);
|
||||
|
||||
const selectedNetworkInterfaceAtom = atomWithStorage<string | undefined>(
|
||||
"selected-network-interface-v0",
|
||||
undefined // Default value is `undefined` (Global view)
|
||||
);
|
||||
|
||||
export const useSelectedNetworkInterface = () => useAtom(selectedNetworkInterfaceAtom);
|
||||
|
||||
@@ -37,6 +37,7 @@ import { ResourceComponents } from "..";
|
||||
import { ServerInfo } from "./info";
|
||||
import { ServerStats } from "./stats";
|
||||
import { RenameResource } from "@components/config/util";
|
||||
import { GroupActions } from "@components/group-actions";
|
||||
|
||||
export const useServer = (id?: string) =>
|
||||
useRead("ListServers", {}, { refetchInterval: 10_000 }).data?.find(
|
||||
@@ -202,6 +203,21 @@ export const ServerComponents: RequiredResourceComponents = {
|
||||
return <NewResource type="Server" />;
|
||||
},
|
||||
|
||||
GroupActions: () => (
|
||||
<GroupActions
|
||||
type="Server"
|
||||
actions={[
|
||||
"PruneContainers",
|
||||
"PruneNetworks",
|
||||
"PruneVolumes",
|
||||
"PruneImages",
|
||||
"PruneSystem",
|
||||
"RestartAllContainers",
|
||||
"StopAllContainers",
|
||||
]}
|
||||
/>
|
||||
),
|
||||
|
||||
Table: ({ resources }) => (
|
||||
<ServerTable servers={resources as Types.ServerListItem[]} />
|
||||
),
|
||||
|
||||
@@ -2,14 +2,14 @@ import { hex_color_by_intention } from "@lib/color";
|
||||
import { useRead } from "@lib/hooks";
|
||||
import { Types } from "komodo_client";
|
||||
import { useMemo } from "react";
|
||||
import { useStatsGranularity } from "./hooks";
|
||||
import { useStatsGranularity, useSelectedNetworkInterface } from "./hooks";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { AxisOptions, Chart } from "react-charts";
|
||||
import { convertTsMsToLocalUnixTsInMs } from "@lib/utils";
|
||||
import { useTheme } from "@ui/theme";
|
||||
import { fmt_utc_date } from "@lib/formatting";
|
||||
|
||||
type StatType = "cpu" | "mem" | "disk";
|
||||
type StatType = "cpu" | "mem" | "disk" | "network_ingress" | "network_egress" | "network_interface_ingress" | "network_interface_egress";
|
||||
|
||||
type StatDatapoint = { date: number; value: number };
|
||||
|
||||
@@ -22,11 +22,13 @@ export const StatChart = ({
|
||||
type: StatType;
|
||||
className?: string;
|
||||
}) => {
|
||||
const [selectedInterface] = useSelectedNetworkInterface();
|
||||
const [granularity] = useStatsGranularity();
|
||||
|
||||
const { data, isPending } = useRead("GetHistoricalServerStats", {
|
||||
server: server_id,
|
||||
granularity,
|
||||
selectedInterface,
|
||||
});
|
||||
|
||||
const stats = useMemo(
|
||||
@@ -35,7 +37,7 @@ export const StatChart = ({
|
||||
.map((stat) => {
|
||||
return {
|
||||
date: convertTsMsToLocalUnixTsInMs(stat.ts),
|
||||
value: getStat(stat, type),
|
||||
value: getStat(stat, type, selectedInterface),
|
||||
};
|
||||
})
|
||||
.reverse(),
|
||||
@@ -70,6 +72,9 @@ export const InnerStatChart = ({
|
||||
? "dark"
|
||||
: "light"
|
||||
: _theme;
|
||||
const BYTES_PER_GB = 1073741824.0;
|
||||
const BYTES_PER_MB = 1048576.0;
|
||||
const BYTES_PER_KB = 1024.0;
|
||||
const min = stats?.[0]?.date ?? 0;
|
||||
const max = stats?.[stats.length - 1]?.date ?? 0;
|
||||
const diff = max - min;
|
||||
@@ -90,25 +95,65 @@ export const InnerStatChart = ({
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Determine the dynamic scaling for network-related types
|
||||
const maxStatValue = Math.max(...(stats?.map((d) => d.value) ?? [0]));
|
||||
|
||||
const { unit, maxUnitValue } = useMemo(() => {
|
||||
if (type === "network_ingress" || type === "network_egress") {
|
||||
if (maxStatValue <= BYTES_PER_KB) {
|
||||
return { unit: "KB", maxUnitValue: BYTES_PER_KB };
|
||||
} else if (maxStatValue <= BYTES_PER_MB) {
|
||||
return { unit: "MB", maxUnitValue: BYTES_PER_MB };
|
||||
} else if (maxStatValue <= BYTES_PER_GB) {
|
||||
return { unit: "GB", maxUnitValue: BYTES_PER_GB };
|
||||
} else {
|
||||
return { unit: "TB", maxUnitValue: BYTES_PER_GB * 1024 }; // Larger scale for high values
|
||||
}
|
||||
}
|
||||
return { unit: "", maxUnitValue: 100 }; // Default for CPU, memory, disk
|
||||
}, [type, maxStatValue]);
|
||||
|
||||
const valueAxis = useMemo(
|
||||
(): AxisOptions<StatDatapoint>[] => [
|
||||
{
|
||||
getValue: (datum) => datum.value,
|
||||
elementType: "area",
|
||||
min: 0,
|
||||
max: 100,
|
||||
max: maxUnitValue,
|
||||
formatters: {
|
||||
tooltip: (value?: number) => (
|
||||
<div className="text-lg font-mono">
|
||||
{(value ?? 0) >= 10 ? value?.toFixed(2) : "0" + value?.toFixed(2)}
|
||||
%
|
||||
{(type === "network_ingress" || type === "network_egress") && unit
|
||||
? `${(value ?? 0) / (maxUnitValue / 1024)} ${unit}`
|
||||
: `${value?.toFixed(2)}%`}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[]
|
||||
[type, maxUnitValue, unit]
|
||||
);
|
||||
|
||||
// const valueAxis = useMemo(
|
||||
// (): AxisOptions<StatDatapoint>[] => [
|
||||
// {
|
||||
// getValue: (datum) => datum.value,
|
||||
// elementType: "area",
|
||||
// min: 0,
|
||||
// max: 100,
|
||||
// formatters: {
|
||||
// tooltip: (value?: number) => (
|
||||
// <div className="text-lg font-mono">
|
||||
// {(value ?? 0) >= 10 ? value?.toFixed(2) : "0" + value?.toFixed(2)}
|
||||
// %
|
||||
// </div>
|
||||
// ),
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// []
|
||||
// );
|
||||
return (
|
||||
<Chart
|
||||
options={{
|
||||
@@ -133,68 +178,26 @@ export const InnerStatChart = ({
|
||||
/>
|
||||
);
|
||||
|
||||
// const container_ref = useRef<HTMLDivElement>(null);
|
||||
// const line_ref = useRef<IChartApi>();
|
||||
// const series_ref = useRef<ISeriesApi<"Area">>();
|
||||
// const lineColor = getColor(type);
|
||||
|
||||
// const handleResize = () =>
|
||||
// line_ref.current?.applyOptions({
|
||||
// width: container_ref.current?.clientWidth,
|
||||
// });
|
||||
|
||||
// useEffect(() => {
|
||||
// if (!stats) return;
|
||||
// if (line_ref.current) line_ref.current.remove();
|
||||
|
||||
// const init = () => {
|
||||
// if (!container_ref.current) return;
|
||||
|
||||
// // INIT LINE
|
||||
// line_ref.current = createChart(container_ref.current, {
|
||||
// width: container_ref.current.clientWidth,
|
||||
// height: container_ref.current.clientHeight,
|
||||
// layout: {
|
||||
// background: { type: ColorType.Solid, color: "transparent" },
|
||||
// // textColor: "grey",
|
||||
// fontSize: 12,
|
||||
// },
|
||||
// grid: {
|
||||
// horzLines: { color: "#3f454d25" },
|
||||
// vertLines: { color: "#3f454d25" },
|
||||
// },
|
||||
// timeScale: { timeVisible: true },
|
||||
// handleScale: false,
|
||||
// handleScroll: false,
|
||||
// });
|
||||
// line_ref.current.timeScale().fitContent();
|
||||
|
||||
// // INIT SERIES
|
||||
// series_ref.current = line_ref.current.addAreaSeries({
|
||||
// priceLineVisible: false,
|
||||
// title: `${type} %`,
|
||||
// lineColor,
|
||||
// topColor: `${lineColor}B3`,
|
||||
// bottomColor: `${lineColor}0D`,
|
||||
// });
|
||||
// series_ref.current.setData(stats);
|
||||
// };
|
||||
|
||||
// // Run the effect
|
||||
// init();
|
||||
// window.addEventListener("resize", handleResize);
|
||||
// return () => {
|
||||
// window.removeEventListener("resize", handleResize);
|
||||
// };
|
||||
// }, [stats]);
|
||||
|
||||
// return <div className="w-full max-w-full h-full" ref={container_ref} />;
|
||||
};
|
||||
|
||||
const getStat = (stat: Types.SystemStatsRecord, type: StatType) => {
|
||||
const getStat = (stat: Types.SystemStatsRecord, type: StatType, selectedInterface?: string) => {
|
||||
if (type === "cpu") return stat.cpu_perc || 0;
|
||||
if (type === "mem") return (100 * stat.mem_used_gb) / stat.mem_total_gb;
|
||||
if (type === "disk") return (100 * stat.disk_used_gb) / stat.disk_total_gb;
|
||||
if (type === "network_ingress") return stat.network_ingress_bytes || 0;
|
||||
if (type === "network_egress") return stat.network_egress_bytes || 0;
|
||||
if (type === "network_interface_ingress")
|
||||
return selectedInterface
|
||||
? stat.network_usage_interface?.find(
|
||||
(networkInterface) => networkInterface.name === selectedInterface
|
||||
)?.ingress_bytes || 0
|
||||
: stat.network_ingress_bytes || 0;
|
||||
if (type === "network_interface_egress")
|
||||
return selectedInterface
|
||||
? stat.network_usage_interface?.find(
|
||||
(networkInterface) => networkInterface.name === selectedInterface
|
||||
)?.egress_bytes || 0
|
||||
: stat.network_egress_bytes || 0;
|
||||
return 0;
|
||||
};
|
||||
|
||||
@@ -202,5 +205,7 @@ const getColor = (type: StatType) => {
|
||||
if (type === "cpu") return hex_color_by_intention("Good");
|
||||
if (type === "mem") return hex_color_by_intention("Warning");
|
||||
if (type === "disk") return hex_color_by_intention("Neutral");
|
||||
if (type === "network_interface_ingress") return hex_color_by_intention("Critical");
|
||||
if (type === "network_interface_egress") return hex_color_by_intention("Unknown");
|
||||
return hex_color_by_intention("Unknown");
|
||||
};
|
||||
};
|
||||
@@ -14,7 +14,7 @@ import { DataTable, SortableHeader } from "@ui/data-table";
|
||||
import { ReactNode, useMemo, useState } from "react";
|
||||
import { Input } from "@ui/input";
|
||||
import { StatChart } from "./stat-chart";
|
||||
import { useStatsGranularity } from "./hooks";
|
||||
import { useStatsGranularity, useSelectedNetworkInterface } from "./hooks";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -32,6 +32,8 @@ export const ServerStats = ({
|
||||
titleOther?: ReactNode;
|
||||
}) => {
|
||||
const [interval, setInterval] = useStatsGranularity();
|
||||
const [networkInterface, setNetworkInterface] = useSelectedNetworkInterface();
|
||||
|
||||
const stats = useRead(
|
||||
"GetSystemStats",
|
||||
{ server: id },
|
||||
@@ -94,6 +96,7 @@ export const ServerStats = ({
|
||||
<div className="flex flex-col lg:flex-row gap-4">
|
||||
<CPU stats={stats} />
|
||||
<RAM stats={stats} />
|
||||
<NETWORK stats={stats} />
|
||||
<DISK stats={stats} />
|
||||
</div>
|
||||
</Section>
|
||||
@@ -101,7 +104,9 @@ export const ServerStats = ({
|
||||
<Section
|
||||
title="Historical"
|
||||
actions={
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="flex gap-4 items-center">
|
||||
{/* Granularity Dropdown */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-muted-foreground">Interval:</div>
|
||||
<Select
|
||||
value={interval}
|
||||
@@ -131,6 +136,36 @@ export const ServerStats = ({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Network Interface Dropdown */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-muted-foreground">Interface:</div>
|
||||
<Select
|
||||
value={networkInterface ?? "all"} // Show "all" if networkInterface is undefined
|
||||
onValueChange={(interfaceName) => {
|
||||
if (interfaceName === "all") {
|
||||
setNetworkInterface(undefined); // Set undefined for "All" option
|
||||
} else {
|
||||
setNetworkInterface(interfaceName);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[150px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All</SelectItem>
|
||||
{/* Iterate over the vector and access the `name` property */}
|
||||
{(stats?.network_usage_interface ?? []).map((networkInterface) => (
|
||||
<SelectItem key={networkInterface.name} value={networkInterface.name}>
|
||||
{networkInterface.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
@@ -141,6 +176,8 @@ export const ServerStats = ({
|
||||
type="disk"
|
||||
className="w-full h-[250px]"
|
||||
/>
|
||||
<StatChart server_id={id} type="network_ingress" className="w-full h-[250px]" />
|
||||
<StatChart server_id={id} type="network_egress" className="w-full h-[250px]" />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
@@ -347,6 +384,49 @@ const RAM = ({ stats }: { stats: Types.SystemStats | undefined }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
const BYTES_PER_KB = 1024;
|
||||
const BYTES_PER_MB = 1024 * BYTES_PER_KB;
|
||||
const BYTES_PER_GB = 1024 * BYTES_PER_MB;
|
||||
|
||||
if (bytes >= BYTES_PER_GB) {
|
||||
return { value: bytes / BYTES_PER_GB, unit: "GB" };
|
||||
} else if (bytes >= BYTES_PER_MB) {
|
||||
return { value: bytes / BYTES_PER_MB, unit: "MB" };
|
||||
} else if (bytes >= BYTES_PER_KB) {
|
||||
return { value: bytes / BYTES_PER_KB, unit: "KB" };
|
||||
} else {
|
||||
return { value: bytes, unit: "bytes" };
|
||||
}
|
||||
};
|
||||
|
||||
const NETWORK = ({ stats }: { stats: Types.SystemStats | undefined }) => {
|
||||
const ingress = stats?.network_ingress_bytes ?? 0;
|
||||
const egress = stats?.network_egress_bytes ?? 0;
|
||||
|
||||
const formattedIngress = formatBytes(ingress);
|
||||
const formattedEgress = formatBytes(egress);
|
||||
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardHeader className="flex-row justify-between">
|
||||
<CardTitle>Network Usage</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<p className="font-medium">Ingress</p>
|
||||
<span className="text-sm text-gray-600">{formattedIngress.value.toFixed(2)} {formattedIngress.unit}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="font-medium">Egress</p>
|
||||
<span className="text-sm text-gray-600">{formattedEgress.value.toFixed(2)} {formattedEgress.unit}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const DISK = ({ stats }: { stats: Types.SystemStats | undefined }) => {
|
||||
const used = stats?.disks.reduce((acc, curr) => (acc += curr.used_gb), 0);
|
||||
const total = stats?.disks.reduce((acc, curr) => (acc += curr.total_gb), 0);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TableTags } from "@components/tags";
|
||||
import { useRead } from "@lib/hooks";
|
||||
import { useRead, useSelectedResources } from "@lib/hooks";
|
||||
import { DataTable, SortableHeader } from "@ui/data-table";
|
||||
import { ServerComponents } from ".";
|
||||
import { ResourceLink } from "../common";
|
||||
@@ -11,6 +11,7 @@ export const ServerTable = ({
|
||||
}: {
|
||||
servers: Types.ServerListItem[];
|
||||
}) => {
|
||||
const [_, setSelectedResources] = useSelectedResources("Server");
|
||||
const deployments = useRead("ListDeployments", {}).data;
|
||||
const stacks = useRead("ListStacks", {}).data;
|
||||
const repos = useRead("ListRepos", {}).data;
|
||||
@@ -28,6 +29,10 @@ export const ServerTable = ({
|
||||
<DataTable
|
||||
tableKey="servers"
|
||||
data={servers}
|
||||
selectOptions={{
|
||||
selectKey: ({ name }) => name,
|
||||
onSelect: setSelectedResources,
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
@@ -71,9 +76,7 @@ export const ServerTable = ({
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="State" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<ServerComponents.State id={row.original.id} />
|
||||
),
|
||||
cell: ({ row }) => <ServerComponents.State id={row.original.id} />,
|
||||
},
|
||||
{
|
||||
header: "Tags",
|
||||
|
||||
@@ -134,8 +134,7 @@ export const PullStack = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionWithDialog
|
||||
name={`${stack?.name}${service ? ` - ${service}` : ""}`}
|
||||
<ConfirmButton
|
||||
title={`Pull Image${service ? "" : "s"}`}
|
||||
icon={<Download className="h-4 w-4" />}
|
||||
onClick={() => pull({ stack: id, service })}
|
||||
|
||||
@@ -38,6 +38,7 @@ import { DashboardPieChart } from "@pages/home/dashboard";
|
||||
import { ResourcePageHeader, StatusBadge } from "@components/util";
|
||||
import { StackConfig } from "./config";
|
||||
import { RenameResource } from "@components/config/util";
|
||||
import { GroupActions } from "@components/group-actions";
|
||||
|
||||
export const useStack = (id?: string) =>
|
||||
useRead("ListStacks", {}, { refetchInterval: 10_000 }).data?.find(
|
||||
@@ -152,6 +153,19 @@ export const StackComponents: RequiredResourceComponents = {
|
||||
);
|
||||
},
|
||||
|
||||
GroupActions: () => (
|
||||
<GroupActions
|
||||
type="Stack"
|
||||
actions={[
|
||||
"PullStack",
|
||||
"DeployStack",
|
||||
"RestartStack",
|
||||
"StopStack",
|
||||
"DestroyStack",
|
||||
]}
|
||||
/>
|
||||
),
|
||||
|
||||
New: ({ server_id: _server_id }) => {
|
||||
const servers = useRead("ListServers", {}).data;
|
||||
const server_id = _server_id
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRead } from "@lib/hooks";
|
||||
import { useRead, useSelectedResources } from "@lib/hooks";
|
||||
import { DataTable, SortableHeader } from "@ui/data-table";
|
||||
import { ResourceLink } from "../common";
|
||||
import { TableTags } from "@components/tags";
|
||||
@@ -12,10 +12,17 @@ export const StackTable = ({ stacks }: { stacks: Types.StackListItem[] }) => {
|
||||
(id: string) => servers?.find((server) => server.id === id)?.name,
|
||||
[servers]
|
||||
);
|
||||
|
||||
const [_, setSelectedResources] = useSelectedResources("Stack");
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
tableKey="Stacks"
|
||||
data={stacks}
|
||||
selectOptions={{
|
||||
selectKey: ({ name }) => name,
|
||||
onSelect: setSelectedResources,
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import { UsableResource } from "@types";
|
||||
import { useToast } from "@ui/use-toast";
|
||||
import { atom, useAtom } from "jotai";
|
||||
import { atomFamily } from "jotai/utils";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { RESOURCE_TARGETS } from "./utils";
|
||||
@@ -466,19 +467,28 @@ export const useWebhookIdOrName = () => {
|
||||
|
||||
export type Dimensions = { width: number; height: number };
|
||||
export const useWindowDimensions = () => {
|
||||
const [dimensions, setDimensions] = useState<Dimensions>({ width: 0, height: 0 });
|
||||
const [dimensions, setDimensions] = useState<Dimensions>({
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
useEffect(() => {
|
||||
const callback = () => {
|
||||
setDimensions({
|
||||
width: window.screen.availWidth,
|
||||
height: window.screen.availHeight,
|
||||
});
|
||||
}
|
||||
};
|
||||
callback();
|
||||
window.addEventListener("resize", callback);
|
||||
return () => {
|
||||
window.removeEventListener("resize", callback);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
return dimensions;
|
||||
}
|
||||
};
|
||||
|
||||
const selected_resources = atomFamily((_: UsableResource) =>
|
||||
atom<string[]>([])
|
||||
);
|
||||
export const useSelectedResources = (type: UsableResource) =>
|
||||
useAtom(selected_resources(type));
|
||||
|
||||
@@ -138,6 +138,12 @@ export const usableResourcePath = (resource: UsableResource) => {
|
||||
return `${resource.toLowerCase()}s`;
|
||||
};
|
||||
|
||||
export const usableResourceExecuteKey = (resource: UsableResource) => {
|
||||
if (resource === "ServerTemplate") return "template";
|
||||
if (resource === "ResourceSync") return "sync";
|
||||
return `${resource.toLowerCase()}`;
|
||||
};
|
||||
|
||||
export const sanitizeOnlySpan = (log: string) => {
|
||||
return sanitizeHtml(log, {
|
||||
allowedTags: ["span"],
|
||||
|
||||
@@ -24,8 +24,8 @@ export const Resources = () => {
|
||||
type === "ServerTemplate"
|
||||
? "Server Template"
|
||||
: type === "ResourceSync"
|
||||
? "Resource Sync"
|
||||
: type;
|
||||
? "Resource Sync"
|
||||
: type;
|
||||
useSetTitle(name + "s");
|
||||
const [search, set] = useState("");
|
||||
const resources = useRead(`List${type}s`, {}).data;
|
||||
@@ -57,11 +57,10 @@ export const Resources = () => {
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
{is_admin || !disable_non_admin_create ? (
|
||||
<Components.New />
|
||||
) : (
|
||||
<div></div>
|
||||
)}
|
||||
<div className="flex gap-4">
|
||||
{(is_admin || !disable_non_admin_create) && <Components.New />}
|
||||
<Components.GroupActions />
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<TagsFilter />
|
||||
<div className="relative">
|
||||
|
||||
3
frontend/src/types.d.ts
vendored
3
frontend/src/types.d.ts
vendored
@@ -25,6 +25,9 @@ export interface RequiredResourceComponents {
|
||||
/** A table component to view resource list */
|
||||
Table: React.FC<{ resources: Types.ResourceListItem<unknown>[] }>;
|
||||
|
||||
/** Dropdown menu to trigger group actions for selected resources */
|
||||
GroupActions: React.FC;
|
||||
|
||||
/** Icon for the component */
|
||||
Icon: OptionalIdComponent;
|
||||
BigIcon: OptionalIdComponent;
|
||||
|
||||
@@ -2,10 +2,12 @@ import { cn } from "@lib/utils";
|
||||
import {
|
||||
Column,
|
||||
ColumnDef,
|
||||
SortingState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
Row,
|
||||
RowSelectionState,
|
||||
SortingState,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
@@ -19,6 +21,7 @@ import {
|
||||
} from "@ui/table";
|
||||
import { ArrowDown, ArrowUp, Minus } from "lucide-react";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import { Checkbox } from "./checkbox";
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
/** Unique key given to table so sorting can be remembered on local storage */
|
||||
@@ -29,6 +32,11 @@ interface DataTableProps<TData, TValue> {
|
||||
noResults?: ReactNode;
|
||||
defaultSort?: SortingState;
|
||||
sortDescFirst?: boolean;
|
||||
selectOptions?: {
|
||||
selectKey: (row: TData) => string;
|
||||
onSelect: (selected: string[]) => void;
|
||||
disableRow?: boolean | ((row: Row<TData>) => boolean);
|
||||
};
|
||||
}
|
||||
|
||||
export function DataTable<TData, TValue>({
|
||||
@@ -39,9 +47,14 @@ export function DataTable<TData, TValue>({
|
||||
noResults,
|
||||
sortDescFirst = false,
|
||||
defaultSort = [],
|
||||
selectOptions,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const [sorting, setSorting] = useState<SortingState>(defaultSort);
|
||||
|
||||
// intentionally not initialized to clear selected values on table mount
|
||||
// could add some prop for adding default selected state to preserve between mounts
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns: columns.filter((c) => c) as any,
|
||||
@@ -50,8 +63,12 @@ export function DataTable<TData, TValue>({
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
state: {
|
||||
sorting,
|
||||
rowSelection,
|
||||
},
|
||||
sortDescFirst,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getRowId: selectOptions?.selectKey,
|
||||
enableRowSelection: selectOptions?.disableRow,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -66,12 +83,31 @@ export function DataTable<TData, TValue>({
|
||||
}
|
||||
}, [tableKey, sorting]);
|
||||
|
||||
useEffect(() => {
|
||||
selectOptions?.onSelect(Object.keys(rowSelection));
|
||||
}, [rowSelection]);
|
||||
|
||||
return (
|
||||
<div className="rounded-md border bg-card text-card-foreground shadow py-1 px-1">
|
||||
<Table className="xl:table-fixed border-separate border-spacing-0">
|
||||
<TableHeader className="sticky top-0">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
{table.getHeaderGroups().map((headerGroup, i) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{/* placeholder header */}
|
||||
{i === 0 && selectOptions && (
|
||||
<TableHead className="w-8 relative whitespace-nowrap bg-background border-b border-r last:border-r-0">
|
||||
<Checkbox
|
||||
className="ml-2"
|
||||
disabled={selectOptions.disableRow === true}
|
||||
checked={
|
||||
table.getIsSomeRowsSelected()
|
||||
? "indeterminate"
|
||||
: table.getIsAllRowsSelected()
|
||||
}
|
||||
onCheckedChange={() => table.toggleAllRowsSelected()}
|
||||
/>
|
||||
</TableHead>
|
||||
)}
|
||||
{headerGroup.headers.map((header) => {
|
||||
const size = header.column.getSize();
|
||||
return (
|
||||
@@ -105,6 +141,18 @@ export function DataTable<TData, TValue>({
|
||||
onRowClick && "cursor-pointer"
|
||||
)}
|
||||
>
|
||||
{selectOptions && (
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
disabled={!row.getCanSelect()}
|
||||
className="ml-2"
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(c) =>
|
||||
c !== "indeterminate" && row.toggleSelected()
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
)}
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
const size = cell.column.getSize();
|
||||
return (
|
||||
|
||||
@@ -56,7 +56,7 @@ pub async fn write_file(
|
||||
|
||||
if !replacers.is_empty() {
|
||||
logs.push(Log::simple(
|
||||
"interpolate periphery secrets",
|
||||
"Interpolate - Environment",
|
||||
replacers
|
||||
.iter()
|
||||
.map(|(_, variable)| format!("<span class=\"text-muted-foreground\">replaced:</span> {variable}"))
|
||||
|
||||
@@ -3,8 +3,7 @@ use std::time::Duration;
|
||||
use opentelemetry::{global, trace::TracerProvider, KeyValue};
|
||||
use opentelemetry_otlp::WithExportConfig;
|
||||
use opentelemetry_sdk::{
|
||||
runtime,
|
||||
trace::{BatchConfig, Sampler, Tracer},
|
||||
trace::{Sampler, Tracer},
|
||||
Resource,
|
||||
};
|
||||
use opentelemetry_semantic_conventions::{
|
||||
@@ -23,22 +22,22 @@ fn resource(service_name: String) -> Resource {
|
||||
}
|
||||
|
||||
pub fn tracer(endpoint: &str, service_name: String) -> Tracer {
|
||||
let provider = opentelemetry_otlp::new_pipeline()
|
||||
.tracing()
|
||||
.with_trace_config(
|
||||
let provider = opentelemetry_sdk::trace::TracerProvider::builder()
|
||||
.with_config(
|
||||
opentelemetry_sdk::trace::Config::default()
|
||||
.with_sampler(Sampler::AlwaysOn)
|
||||
.with_resource(resource(service_name.clone())),
|
||||
)
|
||||
.with_batch_config(BatchConfig::default())
|
||||
.with_exporter(
|
||||
opentelemetry_otlp::new_exporter()
|
||||
.tonic()
|
||||
.with_batch_exporter(
|
||||
opentelemetry_otlp::SpanExporter::builder()
|
||||
.with_tonic()
|
||||
.with_endpoint(endpoint)
|
||||
.with_timeout(Duration::from_secs(3)),
|
||||
.with_timeout(Duration::from_secs(3))
|
||||
.build()
|
||||
.unwrap(),
|
||||
opentelemetry_sdk::runtime::Tokio,
|
||||
)
|
||||
.install_batch(runtime::Tokio)
|
||||
.unwrap();
|
||||
.build();
|
||||
global::set_tracer_provider(provider.clone());
|
||||
provider.tracer(service_name)
|
||||
}
|
||||
|
||||
15
runfile.toml
15
runfile.toml
@@ -46,13 +46,24 @@ cmd = """
|
||||
docker compose -p komodo-dev -f test.compose.yaml build"""
|
||||
|
||||
[test-core]
|
||||
description = "runs core --release pointing to test.core.config.toml"
|
||||
cmd = "KOMODO_CONFIG_PATH=test.core.config.toml cargo run -p komodo_core --release"
|
||||
description = "runs core --release pointing to .komodo/core.config.toml"
|
||||
cmd = "KOMODO_CONFIG_PATH=.komodo/core.config.toml cargo run -p komodo_core --release"
|
||||
|
||||
[test-periphery]
|
||||
description = "runs periphery --release pointing to test.periphery.config.toml"
|
||||
cmd = "PERIPHERY_CONFIG_PATH=test.periphery.config.toml cargo run -p komodo_periphery --release"
|
||||
|
||||
[create-multiarch-builder]
|
||||
cmd = "docker buildx create --name builder --use --bootstrap"
|
||||
|
||||
[build-multiarch-periphery]
|
||||
cmd = """
|
||||
docker build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
-f bin/periphery/cross-compile.Dockerfile \
|
||||
.
|
||||
"""
|
||||
|
||||
[docsite-start]
|
||||
path = "docsite"
|
||||
cmd = "yarn start"
|
||||
|
||||
@@ -37,6 +37,8 @@ Will install to paths:
|
||||
*Note*. Ensure the user running periphery has write permissions to the configured folders `repo_dir`, `stack_dir`, and `ssl_key_file` / `ssl_cert_file` parent folder.
|
||||
This allows periphery to clone repos, write compose files, and generate ssl certs.
|
||||
|
||||
*Note*. To ensure periphery stays running when your user logs out, use `sudo loginctl enable-linger $USER`.
|
||||
|
||||
For example in `periphery.config.toml`, running under `ubuntu` user:
|
||||
```toml
|
||||
repo_dir = "/home/ubuntu/.komodo/repos"
|
||||
|
||||
@@ -66,11 +66,13 @@ def copy_binary(user_install, bin_dir, version):
|
||||
if os.path.isfile(bin_path):
|
||||
os.remove(bin_path)
|
||||
|
||||
periphery_bin = "periphery"
|
||||
periphery_bin = "periphery-x86_64"
|
||||
arch = platform.machine().lower()
|
||||
if arch == "aarch64" or arch == "amd64":
|
||||
print("aarch64 detected")
|
||||
periphery_bin = "periphery-aarch64"
|
||||
else:
|
||||
print("using x86_64 binary")
|
||||
|
||||
# download the binary to bin path
|
||||
print(os.popen(f'curl -sSL https://github.com/mbecker20/komodo/releases/download/{version}/{periphery_bin} > {bin_path}').read())
|
||||
|
||||
@@ -2,7 +2,7 @@ services:
|
||||
core:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: bin/core/debian.Dockerfile
|
||||
dockerfile: bin/core/aio.Dockerfile
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: local
|
||||
@@ -20,7 +20,7 @@ services:
|
||||
periphery:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: bin/periphery/debian.Dockerfile
|
||||
dockerfile: bin/periphery/aio.Dockerfile
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: local
|
||||
@@ -53,4 +53,4 @@ volumes:
|
||||
data:
|
||||
repo-cache:
|
||||
repos:
|
||||
stacks:
|
||||
stacks:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
###########################
|
||||
############################
|
||||
# 🦎 KOMODO CORE CONFIG 🦎 #
|
||||
###########################
|
||||
############################
|
||||
title = "Komodo Test"
|
||||
host = "http://localhost:9121"
|
||||
port = 9121
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
################################
|
||||
#################################
|
||||
# 🦎 KOMODO PERIPHERY CONFIG 🦎 #
|
||||
################################
|
||||
#################################
|
||||
port = 8121
|
||||
repo_dir = ".komodo/repos"
|
||||
stack_dir = ".komodo/stacks"
|
||||
@@ -11,7 +11,7 @@ include_disk_mounts = []
|
||||
############
|
||||
# Security #
|
||||
############
|
||||
ssl_enabled = false
|
||||
ssl_enabled = true
|
||||
ssl_key_file = ".komodo/ssl/key.pem"
|
||||
ssl_cert_file = ".komodo/ssl/cert.pem"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user