Compare commits

..

4 Commits

Author SHA1 Message Date
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
75 changed files with 1787 additions and 666 deletions

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,23 @@
## 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
# Pre compile dependencies
COPY ./bin/core/Cargo.toml ./bin/core/Cargo.toml
RUN mkdir ./bin/core/src && \
echo "fn main() {}" >> ./bin/core/src/main.rs && \
cargo build -p komodo_core --release && \
rm -r ./bin/core
COPY ./bin/core ./bin/core
# Compile app
RUN cargo build -p komodo_core --release
# Build Frontend
@@ -19,34 +29,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

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

@@ -0,0 +1,36 @@
## All in one, multi stage compile + runtime Docker build for your 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
# Pre compile dependencies
COPY ./bin/periphery/Cargo.toml ./bin/periphery/Cargo.toml
RUN mkdir ./bin/periphery/src && \
echo "fn main() {}" >> ./bin/periphery/src/main.rs && \
cargo build -p komodo_periphery --release && \
rm -r ./bin/periphery
COPY ./bin/periphery ./bin/periphery
# Compile app
RUN cargo build -p komodo_periphery --release
# Final Image
FROM debian:bullseye-slim
COPY ./bin/periphery/debian-deps.sh .
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
COPY --from=builder /builder/target/release/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

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

@@ -1,29 +0,0 @@
# Build Periphery
FROM rust:1.82.0-bullseye AS builder
WORKDIR /builder
COPY . .
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
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

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

@@ -4,7 +4,7 @@ use async_timing_util::wait_until_timelength;
use komodo_client::entities::stats::{
SingleDiskUsage, SystemInformation, SystemProcess, SystemStats,
};
use sysinfo::System;
use sysinfo::{ProcessesToUpdate, System};
use tokio::sync::RwLock;
use crate::config::periphery_config;
@@ -82,7 +82,9 @@ 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();
}

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

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

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

@@ -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,14 +1,14 @@
###################################
####################################
# 🦎 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`

View File

@@ -1,6 +1,6 @@
###############################
################################
# 🦎 KOMODO COMPOSE - MONGO 🦎 #
###############################
################################
## This compose file will deploy:
## 1. MongoDB

View File

@@ -1,6 +1,6 @@
##################################
###################################
# 🦎 KOMODO COMPOSE - POSTGRES 🦎 #
##################################
###################################
## This compose file will deploy:
## 1. Postgres + FerretDB Mongo adapter

View File

@@ -1,6 +1,6 @@
################################
#################################
# 🦎 KOMODO COMPOSE - SQLITE 🦎 #
################################
#################################
## This compose file will deploy:
## 1. Sqlite + FerretDB Mongo adapter

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

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

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

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

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

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