Compare commits

...

14 Commits

Author SHA1 Message Date
mbecker20
e385c6e722 use ferretdb:1 2025-02-26 14:55:34 -08:00
Maxwell Becker
9ef25e7575 Create FUNDING.yml 2025-02-13 12:07:48 -08:00
boomam
f945a3014a Update index.mdx (#306)
Added small note on initial login steps.
2025-02-11 00:03:39 -08:00
mbecker20
fdad04d6cb fix KOMODO_DB_USERNAME compose files 2025-02-08 18:45:02 -08:00
mbecker20
c914f23aa8 update compose files re #180 2025-02-08 12:29:26 -08:00
Maarten Kossen
82b2e68cd3 Adding Resource Sync documentation. (#259) 2025-01-11 21:32:02 -08:00
rita7lopes
e274d6f7c8 Network Usage - Ingress Egress per interface and global usage (#229)
* Add network io stats

Add network usage graph and current status

Change network graphs to use network interface from drop down menu

Adjust the type to be able to get general network stats for the general
UI view

Working setup with a working builder

remove changes to these dockerfile

remove lock changes

* change network hashmap to Vector

* adjust all the network_usage_interface to be a vector rather than a hash map

* PR requested changes applied

* Change net_ingress_bytes and egress to network_ingress_bytes egress respectively

* final gen-client types

---------

Co-authored-by: mbecker20 <becker.maxh@gmail.com>
2024-12-21 08:15:21 -08:00
Maxwell Becker
ab8777460d Simplify periphery aio.Dockerfile 2024-12-14 09:13:01 -08:00
Maxwell Becker
7e030e702f Simplify aio.Dockerfile 2024-12-14 09:12:32 -08:00
Maxwell Becker
a869a74002 Fix test.compose.yaml (use aio.Dockerfiles) 2024-12-14 09:11:24 -08:00
mbecker20
1d31110f8c fix multi arch built var reference 2024-12-02 03:33:11 -05:00
mbecker20
bb63892e10 periphery -> periphery-x86_64 setup script 2024-12-02 03:31:55 -05:00
mbecker20
4e554eb2a7 add labels to binary / frontend images 2024-12-02 03:02:00 -05:00
Maxwell Becker
00968b6ea1 1.16.12 (#209)
* inc version

* Komodo interp in ui compose file

* fix auto update when image doesn't specify tag by defaulting to latest

* Pull image buttons don't need safety dialog

* WIP crosscompile

* rename

* entrypoint

* fix copy

* remove example/* from workspace

* add targets

* multiarch pkg config

* use specific COPY

* update deps

* multiarch build command

* pre compile deps

* cross compile

* enable-linger

* remove spammed log when server doesn't have docker

* add multiarch.Dockerfile

* fix casing

* fix tag

* try not let COPY fail

* try

* ARG TARGETPLATFORM

* use /app for consistency

* try

* delete cross-compile approach

* add multiarch core build

* multiarch Deno

* single arch multi arch

* typeshare cli note

* new typeshare

* remove note about aarch64 image

* test configs

* fix config file headers

* binaries dockerfile

* update cargo build

* docs

* simple

* just simple

* use -p

* add configurable binaries tag

* add multi-arch

* allow copy to fail

* fix binary paths

* frontend Dockerfiel

* use dedicated static frontend build

* auto retry getting instance state from aws

* retry 5 times

* cleanup

* simplify binary build

* try alpine and musl

* install alpine deps

* back to debian, try rustls

* move fully to rustls

* single arch builds using single binary image

* default IMAGE_TAG

* cleanup

* try caching deps

* single arch add frontend build

* rustls::crypto::ring::default_provider()

* back to simple

* comment dockerfile

* add select options prop, render checkboxes if present

* add allowSelectedIf to enable / disable rows where necessary

* rename allowSelectIf to isSelectable, allow false as global disable, disable checkboxes when not allowed

* rename isSelectable to disableRow (it works the oppsite way lol)

* selected resources hook, start deployment batch execute component

* add deployment group actions

* add deployment group actions

* add default (empty) group actions for other resources

* fix checkbox header styles

* explicitly check if disableRow is passed (this prop is cursed)

* don't disable row selection for deployments table

* don't need id for groupactions

* add group actions to resources page

* fix row checkbox (prop not cursed, i dumb)

* re-implement group action list using dropdown menu

* only make group actions clickable when at least one row selected

* add loading indicator

* gap betwen new resource and group actions

* refactor group actions

* remove "Batch" from action labels

* add group actions for relevant resources

* fix hardcode

* add selectOptions to relevant tables

* select by name not id

* expect selected to be names

* add note re selection state init for future reference

* multi select working nicely for all resources

* configure server health check timeout

* config message

* refresh processes remove dead processes

* simplify the build args

* default timeout seconds 3

---------

Co-authored-by: kv <karamvir.singh98@gmail.com>
2024-12-01 23:34:07 -08:00
85 changed files with 2101 additions and 791 deletions

2
.github/FUNDING.yml vendored Normal file
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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
View 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

View File

@@ -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

View File

@@ -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" ]

View 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" ]

View File

@@ -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" ]

View File

@@ -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,

View File

@@ -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(

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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(

View File

@@ -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<_>>();

View File

@@ -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 {

View File

@@ -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

View File

@@ -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" ]

View File

@@ -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" ]

View 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" ]

View 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" ]

View File

@@ -56,3 +56,4 @@ impl ResolveToString<GetSystemProcesses> for State {
.context("failed to serialize response to string")
}
}

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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(),

View File

@@ -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()

View File

@@ -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",

View File

@@ -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]. */

View File

@@ -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>,

View File

@@ -20,6 +20,4 @@ pub struct GetSystemStats {}
#[derive(Serialize, Deserialize, Debug, Clone, Request)]
#[response(Vec<SystemProcess>)]
pub struct GetSystemProcesses {}
//
pub struct GetSystemProcesses {}

View File

@@ -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(())
}

View File

@@ -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.

View File

@@ -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: {}

View File

@@ -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: {}

View File

@@ -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: {}

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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 {

View File

@@ -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;

View 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>
);
};

View File

@@ -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[]} />
),

View File

@@ -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",

View File

@@ -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[]} />
),

View File

@@ -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",

View File

@@ -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[]} />
),

View File

@@ -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,
},
{

View File

@@ -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[]} />
),

View File

@@ -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",

View File

@@ -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 })}

View File

@@ -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} />,

View File

@@ -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",

View File

@@ -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[]} />
),

View File

@@ -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",

View File

@@ -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[]} />
),

View File

@@ -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,
},
{

View File

@@ -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[]} />
),

View File

@@ -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",

View File

@@ -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[]}

View File

@@ -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",

View File

@@ -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,

View File

@@ -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);

View File

@@ -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[]} />
),

View File

@@ -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");
};
};

View File

@@ -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);

View File

@@ -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",

View File

@@ -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 })}

View File

@@ -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

View File

@@ -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",

View File

@@ -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));

View File

@@ -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"],

View File

@@ -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">

View File

@@ -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;

View File

@@ -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 (

View File

@@ -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}"))

View File

@@ -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)
}

View File

@@ -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"

View File

@@ -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"

View File

@@ -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())

View File

@@ -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:

View File

@@ -1,6 +1,6 @@
###########################
############################
# 🦎 KOMODO CORE CONFIG 🦎 #
###########################
############################
title = "Komodo Test"
host = "http://localhost:9121"
port = 9121

View File

@@ -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"