mirror of
https://github.com/moghtech/komodo.git
synced 2026-04-25 18:58:28 -05:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d31110f8c | ||
|
|
bb63892e10 | ||
|
|
4e554eb2a7 | ||
|
|
00968b6ea1 |
1315
Cargo.lock
generated
1315
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
50
Cargo.toml
50
Cargo.toml
@@ -3,13 +3,12 @@ resolver = "2"
|
||||
members = [
|
||||
"bin/*",
|
||||
"lib/*",
|
||||
"example/*",
|
||||
"client/core/rs",
|
||||
"client/periphery/rs",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.16.11"
|
||||
version = "1.16.12"
|
||||
edition = "2021"
|
||||
authors = ["mbecker20 <becker.maxh@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
@@ -34,7 +33,7 @@ git = { path = "lib/git" }
|
||||
# MOGH
|
||||
run_command = { version = "0.0.6", features = ["async_tokio"] }
|
||||
serror = { version = "0.4.7", default-features = false }
|
||||
slack = { version = "0.2.0", package = "slack_client_rs" }
|
||||
slack = { version = "0.3.0", package = "slack_client_rs", default-features = false, features = ["rustls"] }
|
||||
derive_default_builder = "0.1.8"
|
||||
derive_empty_traits = "0.1.0"
|
||||
merge_config_files = "0.1.5"
|
||||
@@ -48,52 +47,53 @@ mungos = "1.1.0"
|
||||
svi = "1.0.1"
|
||||
|
||||
# ASYNC
|
||||
reqwest = { version = "0.12.8", features = ["json"] }
|
||||
tokio = { version = "1.38.1", features = ["full"] }
|
||||
reqwest = { version = "0.12.9", default-features = false, features = ["json", "rustls-tls"] }
|
||||
tokio = { version = "1.41.1", features = ["full"] }
|
||||
tokio-util = "0.7.12"
|
||||
futures = "0.3.31"
|
||||
futures-util = "0.3.31"
|
||||
|
||||
# SERVER
|
||||
axum-extra = { version = "0.9.4", features = ["typed-header"] }
|
||||
tower-http = { version = "0.6.1", features = ["fs", "cors"] }
|
||||
axum-server = { version = "0.7.1", features = ["tls-openssl"] }
|
||||
axum = { version = "0.7.7", features = ["ws", "json"] }
|
||||
axum-extra = { version = "0.9.6", features = ["typed-header"] }
|
||||
tower-http = { version = "0.6.2", features = ["fs", "cors"] }
|
||||
axum-server = { version = "0.7.1", features = ["tls-rustls"] }
|
||||
axum = { version = "0.7.9", features = ["ws", "json"] }
|
||||
tokio-tungstenite = "0.24.0"
|
||||
|
||||
# SER/DE
|
||||
ordered_hash_map = { version = "0.4.0", features = ["serde"] }
|
||||
serde = { version = "1.0.210", features = ["derive"] }
|
||||
serde = { version = "1.0.215", features = ["derive"] }
|
||||
strum = { version = "0.26.3", features = ["derive"] }
|
||||
serde_json = "1.0.132"
|
||||
serde_json = "1.0.133"
|
||||
serde_yaml = "0.9.34"
|
||||
toml = "0.8.19"
|
||||
|
||||
# ERROR
|
||||
anyhow = "1.0.91"
|
||||
thiserror = "1.0.65"
|
||||
anyhow = "1.0.93"
|
||||
thiserror = "2.0.3"
|
||||
|
||||
# LOGGING
|
||||
opentelemetry_sdk = { version = "0.25.0", features = ["rt-tokio"] }
|
||||
opentelemetry-otlp = { version = "0.27.0", features = ["tls-roots", "reqwest-rustls"] }
|
||||
opentelemetry_sdk = { version = "0.27.0", features = ["rt-tokio"] }
|
||||
tracing-subscriber = { version = "0.3.18", features = ["json"] }
|
||||
opentelemetry-semantic-conventions = "0.25.0"
|
||||
tracing-opentelemetry = "0.26.0"
|
||||
opentelemetry-otlp = "0.25.0"
|
||||
opentelemetry = "0.25.0"
|
||||
opentelemetry-semantic-conventions = "0.27.0"
|
||||
tracing-opentelemetry = "0.28.0"
|
||||
opentelemetry = "0.27.0"
|
||||
tracing = "0.1.40"
|
||||
|
||||
# CONFIG
|
||||
clap = { version = "4.5.20", features = ["derive"] }
|
||||
clap = { version = "4.5.21", features = ["derive"] }
|
||||
dotenvy = "0.15.7"
|
||||
envy = "0.4.2"
|
||||
|
||||
# CRYPTO / AUTH
|
||||
uuid = { version = "1.10.0", features = ["v4", "fast-rng", "serde"] }
|
||||
uuid = { version = "1.11.0", features = ["v4", "fast-rng", "serde"] }
|
||||
openidconnect = "3.5.0"
|
||||
urlencoding = "2.1.3"
|
||||
nom_pem = "4.0.0"
|
||||
bcrypt = "0.15.1"
|
||||
bcrypt = "0.16.0"
|
||||
base64 = "0.22.1"
|
||||
rustls = "0.23.18"
|
||||
hmac = "0.12.1"
|
||||
sha2 = "0.10.8"
|
||||
rand = "0.8.5"
|
||||
@@ -101,19 +101,19 @@ jwt = "0.16.0"
|
||||
hex = "0.4.3"
|
||||
|
||||
# SYSTEM
|
||||
bollard = "0.17.1"
|
||||
bollard = "0.18.1"
|
||||
sysinfo = "0.32.0"
|
||||
|
||||
# CLOUD
|
||||
aws-config = "1.5.9"
|
||||
aws-sdk-ec2 = "1.83.0"
|
||||
aws-config = "1.5.10"
|
||||
aws-sdk-ec2 = "1.91.0"
|
||||
|
||||
# MISC
|
||||
derive_builder = "0.20.2"
|
||||
typeshare = "1.0.4"
|
||||
octorust = "0.7.0"
|
||||
dashmap = "6.1.0"
|
||||
wildcard = "0.2.0"
|
||||
wildcard = "0.3.0"
|
||||
colored = "2.1.0"
|
||||
regex = "1.11.1"
|
||||
bson = "2.13.0"
|
||||
|
||||
27
bin/binaries.Dockerfile
Normal file
27
bin/binaries.Dockerfile
Normal file
@@ -0,0 +1,27 @@
|
||||
## Builds the Komodo Core and Periphery binaries
|
||||
## for a specific architecture.
|
||||
|
||||
FROM rust:1.82.0-bullseye AS builder
|
||||
|
||||
WORKDIR /builder
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY ./lib ./lib
|
||||
COPY ./client/core/rs ./client/core/rs
|
||||
COPY ./client/periphery ./client/periphery
|
||||
COPY ./bin/core ./bin/core
|
||||
COPY ./bin/periphery ./bin/periphery
|
||||
|
||||
# Compile bin
|
||||
RUN \
|
||||
cargo build -p komodo_core --release && \
|
||||
cargo build -p komodo_periphery --release
|
||||
|
||||
# Copy just the binaries to scratch image
|
||||
FROM scratch
|
||||
|
||||
COPY --from=builder /builder/target/release/core /core
|
||||
COPY --from=builder /builder/target/release/periphery /periphery
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/mbecker20/komodo
|
||||
LABEL org.opencontainers.image.description="Komodo Periphery"
|
||||
LABEL org.opencontainers.image.licenses=GPL-3.0
|
||||
@@ -59,6 +59,7 @@ dotenvy.workspace = true
|
||||
anyhow.workspace = true
|
||||
bcrypt.workspace = true
|
||||
base64.workspace = true
|
||||
rustls.workspace = true
|
||||
tokio.workspace = true
|
||||
serde.workspace = true
|
||||
regex.workspace = true
|
||||
|
||||
@@ -1,13 +1,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" ]
|
||||
50
bin/core/multi-arch.Dockerfile
Normal file
50
bin/core/multi-arch.Dockerfile
Normal file
@@ -0,0 +1,50 @@
|
||||
## Assumes the latest binaries for x86_64 and aarch64 are already built (by binaries.Dockerfile).
|
||||
## Sets up the necessary runtime container dependencies for Komodo Core.
|
||||
## Since theres no heavy build here, QEMU multi-arch builds are fine for this image.
|
||||
|
||||
ARG BINARIES_IMAGE=ghcr.io/mbecker20/komodo-binaries:latest
|
||||
ARG FRONTEND_IMAGE=ghcr.io/mbecker20/komodo-frontend:latest
|
||||
ARG X86_64_BINARIES=${BINARIES_IMAGE}-x86_64
|
||||
ARG AARCH64_BINARIES=${BINARIES_IMAGE}-aarch64
|
||||
|
||||
# This is required to work with COPY --from
|
||||
FROM ${X86_64_BINARIES} AS x86_64
|
||||
FROM ${AARCH64_BINARIES} AS aarch64
|
||||
FROM ${FRONTEND_IMAGE} AS frontend
|
||||
|
||||
# Final Image
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
# Install Deps
|
||||
RUN apt update && \
|
||||
apt install -y git ca-certificates && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy both binaries initially, but only keep appropriate one for the TARGETPLATFORM.
|
||||
COPY --from=x86_64 /core /app/arch/linux/amd64
|
||||
COPY --from=aarch64 /core /app/arch/linux/arm64
|
||||
ARG TARGETPLATFORM
|
||||
RUN mv /app/arch/${TARGETPLATFORM} /usr/local/bin/core && rm -r /app/arch
|
||||
|
||||
# Copy default config / static frontend / deno binary
|
||||
COPY ./config/core.config.toml /config/config.toml
|
||||
COPY --from=frontend /frontend /app/frontend
|
||||
COPY --from=denoland/deno:bin /deno /usr/local/bin/deno
|
||||
|
||||
# Set $DENO_DIR and preload external Deno deps
|
||||
ENV DENO_DIR=/action-cache/deno
|
||||
RUN mkdir /action-cache && \
|
||||
cd /action-cache && \
|
||||
deno install jsr:@std/yaml jsr:@std/toml
|
||||
|
||||
# Hint at the port
|
||||
EXPOSE 9120
|
||||
|
||||
# Label for Ghcr
|
||||
LABEL org.opencontainers.image.source=https://github.com/mbecker20/komodo
|
||||
LABEL org.opencontainers.image.description="Komodo Core"
|
||||
LABEL org.opencontainers.image.licenses=GPL-3.0
|
||||
|
||||
ENTRYPOINT [ "core" ]
|
||||
@@ -1,8 +1,10 @@
|
||||
# Build Core
|
||||
FROM rust:1.82.0-bullseye AS core-builder
|
||||
WORKDIR /builder
|
||||
COPY . .
|
||||
RUN cargo build -p komodo_core --release
|
||||
## Assumes the latest binaries for the required arch are already built (by binaries.Dockerfile).
|
||||
## Sets up the necessary runtime container dependencies for Komodo Core.
|
||||
|
||||
ARG BINARIES_IMAGE=ghcr.io/mbecker20/komodo-binaries:latest
|
||||
|
||||
# This is required to work with COPY --from
|
||||
FROM ${BINARIES_IMAGE} AS binaries
|
||||
|
||||
# Build Frontend
|
||||
FROM node:20.12-alpine AS frontend-builder
|
||||
@@ -12,21 +14,17 @@ COPY ./client/core/ts ./client
|
||||
RUN cd client && yarn && yarn build && yarn link
|
||||
RUN cd frontend && yarn link komodo_client && yarn && yarn build
|
||||
|
||||
# Final Image
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
# Install Deps
|
||||
RUN apt update && \
|
||||
apt install -y git ca-certificates && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Setup an application directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy
|
||||
COPY ./config/core.config.toml /config/config.toml
|
||||
COPY --from=core-builder /builder/target/release/core /app
|
||||
COPY --from=frontend-builder /builder/frontend/dist /app/frontend
|
||||
COPY --from=binaries /core /usr/local/bin/core
|
||||
COPY --from=denoland/deno:bin /deno /usr/local/bin/deno
|
||||
|
||||
# Set $DENO_DIR and preload external Deno deps
|
||||
@@ -43,4 +41,4 @@ LABEL org.opencontainers.image.source=https://github.com/mbecker20/komodo
|
||||
LABEL org.opencontainers.image.description="Komodo Core"
|
||||
LABEL org.opencontainers.image.licenses=GPL-3.0
|
||||
|
||||
ENTRYPOINT [ "/app/core" ]
|
||||
ENTRYPOINT [ "core" ]
|
||||
@@ -118,6 +118,13 @@ impl Resolve<DeployStack, (User, Update)> for State {
|
||||
let mut global_replacers = HashSet::new();
|
||||
let mut secret_replacers = HashSet::new();
|
||||
|
||||
interpolate_variables_secrets_into_string(
|
||||
&vars_and_secrets,
|
||||
&mut stack.config.file_contents,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
interpolate_variables_secrets_into_string(
|
||||
&vars_and_secrets,
|
||||
&mut stack.config.environment,
|
||||
|
||||
@@ -212,21 +212,37 @@ async fn terminate_ec2_instance_inner(
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Automatically retries 5 times, waiting 2 sec in between
|
||||
#[instrument(level = "debug")]
|
||||
async fn get_ec2_instance_status(
|
||||
client: &Client,
|
||||
instance_id: &str,
|
||||
) -> anyhow::Result<Option<InstanceStatus>> {
|
||||
let status = client
|
||||
.describe_instance_status()
|
||||
.instance_ids(instance_id)
|
||||
.send()
|
||||
let mut try_count = 1;
|
||||
loop {
|
||||
match async {
|
||||
anyhow::Ok(
|
||||
client
|
||||
.describe_instance_status()
|
||||
.instance_ids(instance_id)
|
||||
.send()
|
||||
.await
|
||||
.context("failed to describe instance status from aws")?
|
||||
.instance_statuses()
|
||||
.first()
|
||||
.cloned(),
|
||||
)
|
||||
}
|
||||
.await
|
||||
.context("failed to get instance status from aws")?
|
||||
.instance_statuses()
|
||||
.first()
|
||||
.cloned();
|
||||
Ok(status)
|
||||
{
|
||||
Ok(res) => return Ok(res),
|
||||
Err(e) if try_count > 4 => return Err(e),
|
||||
Err(_) => {
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
try_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
@@ -248,28 +264,43 @@ async fn get_ec2_instance_state_name(
|
||||
Ok(Some(state))
|
||||
}
|
||||
|
||||
/// Automatically retries 5 times, waiting 2 sec in between
|
||||
#[instrument(level = "debug")]
|
||||
async fn get_ec2_instance_public_ip(
|
||||
client: &Client,
|
||||
instance_id: &str,
|
||||
) -> anyhow::Result<String> {
|
||||
let ip = client
|
||||
.describe_instances()
|
||||
.instance_ids(instance_id)
|
||||
.send()
|
||||
let mut try_count = 1;
|
||||
loop {
|
||||
match async {
|
||||
anyhow::Ok(
|
||||
client
|
||||
.describe_instances()
|
||||
.instance_ids(instance_id)
|
||||
.send()
|
||||
.await
|
||||
.context("failed to describe instances from aws")?
|
||||
.reservations()
|
||||
.first()
|
||||
.context("instance reservations is empty")?
|
||||
.instances()
|
||||
.first()
|
||||
.context("instances is empty")?
|
||||
.public_ip_address()
|
||||
.context("instance has no public ip")?
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
.await
|
||||
.context("failed to get instance status from aws")?
|
||||
.reservations()
|
||||
.first()
|
||||
.context("instance reservations is empty")?
|
||||
.instances()
|
||||
.first()
|
||||
.context("instances is empty")?
|
||||
.public_ip_address()
|
||||
.context("instance has no public ip")?
|
||||
.to_string();
|
||||
|
||||
Ok(ip)
|
||||
{
|
||||
Ok(res) => return Ok(res),
|
||||
Err(e) if try_count > 4 => return Err(e),
|
||||
Err(_) => {
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
try_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_unknown_instance_type(
|
||||
|
||||
@@ -55,6 +55,7 @@ pub async fn get_builder_periphery(
|
||||
} else {
|
||||
config.passkey
|
||||
},
|
||||
Duration::from_secs(3),
|
||||
);
|
||||
periphery
|
||||
.health_check()
|
||||
@@ -122,7 +123,7 @@ async fn get_aws_builder(
|
||||
let periphery_address =
|
||||
format!("{protocol}://{ip}:{}", config.port);
|
||||
let periphery =
|
||||
PeripheryClient::new(&periphery_address, &core_config().passkey);
|
||||
PeripheryClient::new(&periphery_address, &core_config().passkey, Duration::from_secs(3));
|
||||
|
||||
let start_connect_ts = komodo_timestamp();
|
||||
let mut res = Ok(GetVersionResponse {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::str::FromStr;
|
||||
use std::{str::FromStr, time::Duration};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use futures::future::join_all;
|
||||
@@ -145,6 +145,7 @@ pub fn periphery_client(
|
||||
let client = PeripheryClient::new(
|
||||
&server.config.address,
|
||||
&core_config().passkey,
|
||||
Duration::from_secs(server.config.timeout_seconds as u64),
|
||||
);
|
||||
|
||||
Ok(client)
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::{net::SocketAddr, str::FromStr};
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::Router;
|
||||
use axum_server::tls_openssl::OpenSSLConfig;
|
||||
use axum_server::tls_rustls::RustlsConfig;
|
||||
use tower_http::{
|
||||
cors::{Any, CorsLayer},
|
||||
services::{ServeDir, ServeFile},
|
||||
@@ -89,13 +89,17 @@ async fn app() -> anyhow::Result<()> {
|
||||
|
||||
if config.ssl_enabled {
|
||||
info!("🔒 Core SSL Enabled");
|
||||
rustls::crypto::ring::default_provider()
|
||||
.install_default()
|
||||
.expect("failed to install default rustls CryptoProvider");
|
||||
info!("Komodo Core starting on https://{socket_addr}");
|
||||
let ssl_config = OpenSSLConfig::from_pem_file(
|
||||
let ssl_config = RustlsConfig::from_pem_file(
|
||||
&config.ssl_cert_file,
|
||||
&config.ssl_key_file,
|
||||
)
|
||||
.context("Failed to parse ssl ")?;
|
||||
axum_server::bind_openssl(socket_addr, ssl_config)
|
||||
.await
|
||||
.context("Invalid ssl cert / key")?;
|
||||
axum_server::bind_rustls(socket_addr, ssl_config)
|
||||
.serve(app)
|
||||
.await?
|
||||
} else {
|
||||
|
||||
@@ -245,9 +245,6 @@ pub async fn update_cache_for_server(server: &Server) {
|
||||
.await;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"could not get docker lists | (update status cache) | {e:#}"
|
||||
);
|
||||
insert_deployments_status_unknown(deployments).await;
|
||||
insert_stacks_status_unknown(stacks).await;
|
||||
insert_server_status(
|
||||
|
||||
@@ -77,7 +77,15 @@ pub async fn update_deployment_cache(
|
||||
};
|
||||
format!("{build_name}:{version}")
|
||||
}
|
||||
DeploymentImage::Image { image } => image,
|
||||
DeploymentImage::Image { image } => {
|
||||
// If image already has tag, leave it,
|
||||
// otherwise default the tag to latest
|
||||
if image.contains(':') {
|
||||
image
|
||||
} else {
|
||||
format!("{image}:latest")
|
||||
}
|
||||
}
|
||||
};
|
||||
let update_available = if let Some(ContainerListItem {
|
||||
image_id: Some(curr_image_id),
|
||||
@@ -242,10 +250,18 @@ pub async fn update_stack_cache(
|
||||
}
|
||||
}.is_match(&container.name)
|
||||
}).cloned();
|
||||
// If image already has tag, leave it,
|
||||
// otherwise default the tag to latest
|
||||
let image = image.clone();
|
||||
let image = if image.contains(':') {
|
||||
image
|
||||
} else {
|
||||
image + ":latest"
|
||||
};
|
||||
let update_available = if let Some(ContainerListItem { image_id: Some(curr_image_id), .. }) = &container {
|
||||
images
|
||||
.iter()
|
||||
.find(|i| &i.name == image)
|
||||
.find(|i| i.name == image)
|
||||
.map(|i| &i.id != curr_image_id)
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
|
||||
@@ -41,6 +41,7 @@ bollard.workspace = true
|
||||
sysinfo.workspace = true
|
||||
dotenvy.workspace = true
|
||||
anyhow.workspace = true
|
||||
rustls.workspace = true
|
||||
tokio.workspace = true
|
||||
serde.workspace = true
|
||||
axum.workspace = true
|
||||
|
||||
36
bin/periphery/aio.Dockerfile
Normal file
36
bin/periphery/aio.Dockerfile
Normal 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" ]
|
||||
@@ -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" ]
|
||||
@@ -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" ]
|
||||
33
bin/periphery/multi-arch.Dockerfile
Normal file
33
bin/periphery/multi-arch.Dockerfile
Normal file
@@ -0,0 +1,33 @@
|
||||
## Assumes the latest binaries for x86_64 and aarch64 are already built (by binaries.Dockerfile).
|
||||
## Sets up the necessary runtime container dependencies for Komodo Periphery.
|
||||
## Since theres no heavy build here, QEMU multi-arch builds are fine for this image.
|
||||
|
||||
ARG BINARIES_IMAGE=ghcr.io/mbecker20/komodo-binaries:latest
|
||||
ARG X86_64_BINARIES=${BINARIES_IMAGE}-x86_64
|
||||
ARG AARCH64_BINARIES=${BINARIES_IMAGE}-aarch64
|
||||
|
||||
# This is required to work with COPY --from
|
||||
FROM ${X86_64_BINARIES} AS x86_64
|
||||
FROM ${AARCH64_BINARIES} AS aarch64
|
||||
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
COPY ./bin/periphery/debian-deps.sh .
|
||||
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
## Copy both binaries initially, but only keep appropriate one for the TARGETPLATFORM.
|
||||
COPY --from=x86_64 /periphery /app/arch/linux/amd64
|
||||
COPY --from=aarch64 /periphery /app/arch/linux/arm64
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
RUN mv /app/arch/${TARGETPLATFORM} /usr/local/bin/periphery && rm -r /app/arch
|
||||
|
||||
EXPOSE 8120
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/mbecker20/komodo
|
||||
LABEL org.opencontainers.image.description="Komodo Periphery"
|
||||
LABEL org.opencontainers.image.licenses=GPL-3.0
|
||||
|
||||
CMD [ "periphery" ]
|
||||
23
bin/periphery/single-arch.Dockerfile
Normal file
23
bin/periphery/single-arch.Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
||||
## Assumes the latest binaries for the required arch are already built (by binaries.Dockerfile).
|
||||
## Sets up the necessary runtime container dependencies for Komodo Periphery.
|
||||
|
||||
ARG BINARIES_IMAGE=ghcr.io/mbecker20/komodo-binaries:latest
|
||||
|
||||
# This is required to work with COPY --from
|
||||
FROM ${BINARIES_IMAGE} AS binaries
|
||||
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
COPY ./bin/periphery/debian-deps.sh .
|
||||
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=binaries /periphery /usr/local/bin/periphery
|
||||
|
||||
EXPOSE 8120
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/mbecker20/komodo
|
||||
LABEL org.opencontainers.image.description="Komodo Periphery"
|
||||
LABEL org.opencontainers.image.licenses=GPL-3.0
|
||||
|
||||
CMD [ "periphery" ]
|
||||
@@ -507,11 +507,33 @@ pub async fn write_stack(
|
||||
)
|
||||
.components()
|
||||
.collect::<PathBuf>();
|
||||
fs::write(&file_path, &stack.config.file_contents)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("failed to write compose file to {file_path:?}")
|
||||
})?;
|
||||
|
||||
let file_contents = if !stack.config.skip_secret_interp {
|
||||
let (contents, replacers) = svi::interpolate_variables(
|
||||
&stack.config.file_contents,
|
||||
&periphery_config().secrets,
|
||||
svi::Interpolator::DoubleBrackets,
|
||||
true,
|
||||
)
|
||||
.context("failed to interpolate secrets into file contents")?;
|
||||
if !replacers.is_empty() {
|
||||
res.logs().push(Log::simple(
|
||||
"Interpolate - Compose file",
|
||||
replacers
|
||||
.iter()
|
||||
.map(|(_, variable)| format!("<span class=\"text-muted-foreground\">replaced:</span> {variable}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
));
|
||||
}
|
||||
contents
|
||||
} else {
|
||||
stack.config.file_contents.clone()
|
||||
};
|
||||
|
||||
fs::write(&file_path, &file_contents).await.with_context(
|
||||
|| format!("failed to write compose file to {file_path:?}"),
|
||||
)?;
|
||||
|
||||
Ok((
|
||||
run_directory,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#[macro_use]
|
||||
extern crate tracing;
|
||||
|
||||
//
|
||||
use std::{net::SocketAddr, str::FromStr};
|
||||
|
||||
use anyhow::Context;
|
||||
use axum_server::tls_openssl::OpenSSLConfig;
|
||||
use axum_server::tls_rustls::RustlsConfig;
|
||||
|
||||
mod api;
|
||||
mod compose;
|
||||
@@ -36,14 +37,18 @@ async fn app() -> anyhow::Result<()> {
|
||||
|
||||
if config.ssl_enabled {
|
||||
info!("🔒 Periphery SSL Enabled");
|
||||
rustls::crypto::ring::default_provider()
|
||||
.install_default()
|
||||
.expect("failed to install default rustls CryptoProvider");
|
||||
ssl::ensure_certs().await;
|
||||
info!("Komodo Periphery starting on https://{}", socket_addr);
|
||||
let ssl_config = OpenSSLConfig::from_pem_file(
|
||||
let ssl_config = RustlsConfig::from_pem_file(
|
||||
&config.ssl_cert_file,
|
||||
&config.ssl_key_file,
|
||||
)
|
||||
.await
|
||||
.context("Invalid ssl cert / key")?;
|
||||
axum_server::bind_openssl(socket_addr, ssl_config)
|
||||
axum_server::bind_rustls(socket_addr, ssl_config)
|
||||
.serve(app)
|
||||
.await?
|
||||
} else {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "komodo_client",
|
||||
"version": "1.16.11",
|
||||
"version": "1.16.12",
|
||||
"description": "Komodo client package",
|
||||
"homepage": "https://komo.do",
|
||||
"main": "dist/lib.js",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Generated by typeshare 1.12.0
|
||||
Generated by typeshare 1.13.2
|
||||
*/
|
||||
|
||||
export interface MongoIdObj {
|
||||
@@ -1483,6 +1483,11 @@ export interface ServerConfig {
|
||||
* default: true
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* The timeout used to reach the server in seconds.
|
||||
* default: 2
|
||||
*/
|
||||
timeout_seconds: I64;
|
||||
/**
|
||||
* Sometimes the system stats reports a mount path that is not desired.
|
||||
* Use this field to filter it out from the report.
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -23,16 +23,19 @@ fn periphery_http_client() -> &'static reqwest::Client {
|
||||
pub struct PeripheryClient {
|
||||
address: String,
|
||||
passkey: String,
|
||||
timeout: Duration,
|
||||
}
|
||||
|
||||
impl PeripheryClient {
|
||||
pub fn new(
|
||||
address: impl Into<String>,
|
||||
passkey: impl Into<String>,
|
||||
timeout: impl Into<Duration>,
|
||||
) -> PeripheryClient {
|
||||
PeripheryClient {
|
||||
address: address.into(),
|
||||
passkey: passkey.into(),
|
||||
timeout: timeout.into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +58,7 @@ impl PeripheryClient {
|
||||
#[tracing::instrument(level = "debug", skip(self))]
|
||||
pub async fn health_check(&self) -> anyhow::Result<()> {
|
||||
self
|
||||
.request_inner(api::GetHealth {}, Some(Duration::from_secs(1)))
|
||||
.request_inner(api::GetHealth {}, Some(self.timeout))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,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`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
###############################
|
||||
################################
|
||||
# 🦎 KOMODO COMPOSE - MONGO 🦎 #
|
||||
###############################
|
||||
################################
|
||||
|
||||
## This compose file will deploy:
|
||||
## 1. MongoDB
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
##################################
|
||||
###################################
|
||||
# 🦎 KOMODO COMPOSE - POSTGRES 🦎 #
|
||||
##################################
|
||||
###################################
|
||||
|
||||
## This compose file will deploy:
|
||||
## 1. Postgres + FerretDB Mongo adapter
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
################################
|
||||
#################################
|
||||
# 🦎 KOMODO COMPOSE - SQLITE 🦎 #
|
||||
################################
|
||||
#################################
|
||||
|
||||
## This compose file will deploy:
|
||||
## 1. Sqlite + FerretDB Mongo adapter
|
||||
|
||||
@@ -455,7 +455,7 @@ hetzner.token = ""
|
||||
## and will be hidden in the UI and logs.
|
||||
## These are available to use on any Periphery (Server),
|
||||
## but you can also limit access more by placing them in a single Periphery's config file instead.
|
||||
## These cannot be configured on the environment.
|
||||
## These cannot be configured in the Komodo Core environment, they must be passed in the file.
|
||||
|
||||
# [secrets]
|
||||
# SECRET_1 = "value_1"
|
||||
|
||||
@@ -42,6 +42,7 @@ To run a full Komodo instance from a non-container environment run commands in t
|
||||
* `run -r test-core` -- Build and run Core API
|
||||
* `run -r test-periphery` -- Build and run Periphery API
|
||||
* Build Frontend
|
||||
* Install **typeshare-cli**: `cargo install typeshare-cli`
|
||||
* **Run this once** -- `run -r link-client` -- generates TS client and links to the frontend
|
||||
* After running the above once:
|
||||
* `run -r gen-client` -- Rebuild client
|
||||
|
||||
@@ -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
|
||||
5
frontend/public/client/types.d.ts
vendored
5
frontend/public/client/types.d.ts
vendored
@@ -1580,6 +1580,11 @@ export interface ServerConfig {
|
||||
* default: true
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* The timeout used to reach the server in seconds.
|
||||
* default: 2
|
||||
*/
|
||||
timeout_seconds: I64;
|
||||
/**
|
||||
* Sometimes the system stats reports a mount path that is not desired.
|
||||
* Use this field to filter it out from the report.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Generated by typeshare 1.12.0
|
||||
Generated by typeshare 1.13.2
|
||||
*/
|
||||
/** The levels of permission that a User or UserGroup can have on a resource. */
|
||||
export var PermissionLevel;
|
||||
|
||||
193
frontend/src/components/group-actions.tsx
Normal file
193
frontend/src/components/group-actions.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import { useSelectedResources, useExecute, useWrite } from "@lib/hooks";
|
||||
import { UsableResource } from "@types";
|
||||
import { Button } from "@ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@ui/dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@ui/dropdown-menu";
|
||||
import { Input } from "@ui/input";
|
||||
import { Types } from "komodo_client";
|
||||
import { ChevronDown, CheckCircle } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { ConfirmButton } from "./util";
|
||||
import { useToast } from "@ui/use-toast";
|
||||
import { usableResourceExecuteKey } from "@lib/utils";
|
||||
|
||||
export const GroupActions = <
|
||||
T extends Types.ExecuteRequest["type"] | Types.WriteRequest["type"],
|
||||
>({
|
||||
type,
|
||||
actions,
|
||||
}: {
|
||||
type: UsableResource;
|
||||
actions: T[];
|
||||
}) => {
|
||||
const [action, setAction] = useState<T>();
|
||||
const [selected] = useSelectedResources(type);
|
||||
|
||||
return (
|
||||
<>
|
||||
<GroupActionDropdownMenu
|
||||
type={type}
|
||||
actions={actions}
|
||||
onSelect={setAction}
|
||||
disabled={!selected.length}
|
||||
/>
|
||||
<GroupActionDialog
|
||||
type={type}
|
||||
action={action}
|
||||
onClose={() => setAction(undefined)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const GroupActionDropdownMenu = <
|
||||
T extends Types.ExecuteRequest["type"] | Types.WriteRequest["type"],
|
||||
>({
|
||||
type,
|
||||
actions,
|
||||
onSelect,
|
||||
disabled,
|
||||
}: {
|
||||
type: UsableResource;
|
||||
actions: T[];
|
||||
onSelect: (item: T) => void;
|
||||
disabled: boolean;
|
||||
}) => (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild disabled={disabled}>
|
||||
<Button variant="outline" className="w-40 justify-between">
|
||||
Group Actions <ChevronDown className="w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
className={type === "Server" ? "w-56" : "w-40"}
|
||||
>
|
||||
{type === "ResourceSync" && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => onSelect("RefreshResourceSyncPending" as any)}
|
||||
>
|
||||
<Button variant="secondary" className="w-full">
|
||||
Refresh
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{actions.map((action) => (
|
||||
<DropdownMenuItem key={action} onClick={() => onSelect(action)}>
|
||||
<Button variant="secondary" className="w-full">
|
||||
{action.replaceAll("Batch", "").replaceAll(type, "")}
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuItem onClick={() => onSelect(`Delete${type}` as any)}>
|
||||
<Button variant="destructive" className="w-full">
|
||||
Delete
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
||||
const GroupActionDialog = ({
|
||||
type,
|
||||
action,
|
||||
onClose,
|
||||
}: {
|
||||
type: UsableResource;
|
||||
action:
|
||||
| (Types.ExecuteRequest["type"] | Types.WriteRequest["type"])
|
||||
| undefined;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const { toast } = useToast();
|
||||
const [selected, setSelected] = useSelectedResources(type);
|
||||
const [text, setText] = useState("");
|
||||
|
||||
const { mutate: execute, isPending: executePending } = useExecute(
|
||||
action! as Types.ExecuteRequest["type"],
|
||||
{
|
||||
onSuccess: onClose,
|
||||
}
|
||||
);
|
||||
const { mutate: write, isPending: writePending } = useWrite(
|
||||
action! as Types.WriteRequest["type"],
|
||||
{
|
||||
onSuccess: onClose,
|
||||
}
|
||||
);
|
||||
|
||||
if (!action) return;
|
||||
|
||||
const formatted = action.replaceAll("Batch", "").replaceAll(type, "");
|
||||
const isPending = executePending || writePending;
|
||||
|
||||
return (
|
||||
<Dialog open={!!action} onOpenChange={(o) => !o && onClose()}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Group Execute - {formatted}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-8 flex flex-col gap-4">
|
||||
<ul className="p-4 bg-accent text-sm list-disc list-inside">
|
||||
{selected.map((resource) => (
|
||||
<li key={resource}>{resource}</li>
|
||||
))}
|
||||
</ul>
|
||||
{!action.startsWith("Refresh") && (
|
||||
<>
|
||||
<p
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(formatted);
|
||||
toast({ title: `Copied "${formatted}" to clipboard!` });
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
Please enter <b>{formatted}</b> below to confirm this action.
|
||||
<br />
|
||||
<span className="text-xs text-muted-foreground">
|
||||
You may click the action in bold to copy it
|
||||
</span>
|
||||
</p>
|
||||
<Input value={text} onChange={(e) => setText(e.target.value)} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<ConfirmButton
|
||||
title="Confirm"
|
||||
icon={<CheckCircle className="w-4 h-4" />}
|
||||
onClick={() => {
|
||||
for (const resource of selected) {
|
||||
if (action.startsWith("Delete")) {
|
||||
write({ id: resource } as any);
|
||||
} else if (action.startsWith("Refresh")) {
|
||||
write({ [usableResourceExecuteKey(type)]: resource } as any);
|
||||
} else {
|
||||
execute({
|
||||
[usableResourceExecuteKey(type)]: resource,
|
||||
} as any);
|
||||
}
|
||||
}
|
||||
if (action.startsWith("Delete")) {
|
||||
setSelected([]);
|
||||
}
|
||||
}}
|
||||
disabled={action.startsWith("Refresh") ? false : text !== formatted}
|
||||
loading={isPending}
|
||||
/>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -17,6 +17,7 @@ import { cn } from "@lib/utils";
|
||||
import { Types } from "komodo_client";
|
||||
import { DashboardPieChart } from "@pages/home/dashboard";
|
||||
import { RenameResource } from "@components/config/util";
|
||||
import { GroupActions } from "@components/group-actions";
|
||||
|
||||
const useAction = (id?: string) =>
|
||||
useRead("ListActions", {}).data?.find((d) => d.id === id);
|
||||
@@ -61,6 +62,8 @@ export const ActionComponents: RequiredResourceComponents = {
|
||||
|
||||
New: () => <NewResource type="Action" />,
|
||||
|
||||
GroupActions: () => <GroupActions type="Action" actions={["RunAction"]} />,
|
||||
|
||||
Table: ({ resources }) => (
|
||||
<ActionTable actions={resources as Types.ActionListItem[]} />
|
||||
),
|
||||
|
||||
@@ -3,16 +3,23 @@ import { TableTags } from "@components/tags";
|
||||
import { ResourceLink } from "../common";
|
||||
import { ActionComponents } from ".";
|
||||
import { Types } from "komodo_client";
|
||||
import { useSelectedResources } from "@lib/hooks";
|
||||
|
||||
export const ActionTable = ({
|
||||
actions,
|
||||
}: {
|
||||
actions: Types.ActionListItem[];
|
||||
}) => {
|
||||
const [_, setSelectedResources] = useSelectedResources("Action");
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
tableKey="actions"
|
||||
data={actions}
|
||||
selectOptions={{
|
||||
selectKey: ({ name }) => name,
|
||||
onSelect: setSelectedResources,
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
|
||||
@@ -9,6 +9,7 @@ import { AlerterTable } from "./table";
|
||||
import { Types } from "komodo_client";
|
||||
import { ResourcePageHeader } from "@components/util";
|
||||
import { RenameResource } from "@components/config/util";
|
||||
import { GroupActions } from "@components/group-actions";
|
||||
|
||||
const useAlerter = (id?: string) =>
|
||||
useRead("ListAlerters", {}).data?.find((d) => d.id === id);
|
||||
@@ -43,6 +44,8 @@ export const AlerterComponents: RequiredResourceComponents = {
|
||||
return is_admin && <NewResource type="Alerter" />;
|
||||
},
|
||||
|
||||
GroupActions: () => <GroupActions type="Alerter" actions={[]} />,
|
||||
|
||||
Table: ({ resources }) => (
|
||||
<AlerterTable alerters={resources as Types.AlerterListItem[]} />
|
||||
),
|
||||
|
||||
@@ -2,16 +2,22 @@ import { DataTable, SortableHeader } from "@ui/data-table";
|
||||
import { ResourceLink } from "../common";
|
||||
import { TableTags } from "@components/tags";
|
||||
import { Types } from "komodo_client";
|
||||
import { useSelectedResources } from "@lib/hooks";
|
||||
|
||||
export const AlerterTable = ({
|
||||
alerters,
|
||||
}: {
|
||||
alerters: Types.AlerterListItem[];
|
||||
}) => {
|
||||
const [_, setSelectedResources] = useSelectedResources("Alerter");
|
||||
return (
|
||||
<DataTable
|
||||
tableKey="alerters"
|
||||
data={alerters}
|
||||
selectOptions={{
|
||||
selectKey: ({ name }) => name,
|
||||
onSelect: setSelectedResources,
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useToast } from "@ui/use-toast";
|
||||
import { Button } from "@ui/button";
|
||||
import { useBuilder } from "../builder";
|
||||
import { RenameResource } from "@components/config/util";
|
||||
import { GroupActions } from "@components/group-actions";
|
||||
|
||||
export const useBuild = (id?: string) =>
|
||||
useRead("ListBuilds", {}, { refetchInterval: 10_000 }).data?.find(
|
||||
@@ -129,6 +130,8 @@ export const BuildComponents: RequiredResourceComponents = {
|
||||
);
|
||||
},
|
||||
|
||||
GroupActions: () => <GroupActions type="Build" actions={["RunBuild"]} />,
|
||||
|
||||
Table: ({ resources }) => (
|
||||
<BuildTable builds={resources as Types.BuildListItem[]} />
|
||||
),
|
||||
|
||||
@@ -4,12 +4,19 @@ import { fmt_version } from "@lib/formatting";
|
||||
import { ResourceLink } from "../common";
|
||||
import { BuildComponents } from ".";
|
||||
import { Types } from "komodo_client";
|
||||
import { useSelectedResources } from "@lib/hooks";
|
||||
|
||||
export const BuildTable = ({ builds }: { builds: Types.BuildListItem[] }) => {
|
||||
const [_, setSelectedResources] = useSelectedResources("Build");
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
tableKey="builds"
|
||||
data={builds}
|
||||
selectOptions={{
|
||||
selectKey: ({ name }) => name,
|
||||
onSelect: setSelectedResources,
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
@@ -36,9 +43,7 @@ export const BuildTable = ({ builds }: { builds: Types.BuildListItem[] }) => {
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="State" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<BuildComponents.State id={row.original.id} />
|
||||
),
|
||||
cell: ({ row }) => <BuildComponents.State id={row.original.id} />,
|
||||
size: 120,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -20,6 +20,7 @@ import { DeleteResource, ResourceLink } from "../common";
|
||||
import { BuilderTable } from "./table";
|
||||
import { ResourcePageHeader } from "@components/util";
|
||||
import { RenameResource } from "@components/config/util";
|
||||
import { GroupActions } from "@components/group-actions";
|
||||
|
||||
export const useBuilder = (id?: string) =>
|
||||
useRead("ListBuilders", {}, { refetchInterval: 10_000 }).data?.find(
|
||||
@@ -63,6 +64,7 @@ export const BuilderComponents: RequiredResourceComponents = {
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
|
||||
New: () => {
|
||||
const is_admin = useUser().data?.admin;
|
||||
const nav = useNavigate();
|
||||
@@ -113,6 +115,8 @@ export const BuilderComponents: RequiredResourceComponents = {
|
||||
);
|
||||
},
|
||||
|
||||
GroupActions: () => <GroupActions type="Builder" actions={[]} />,
|
||||
|
||||
Table: ({ resources }) => (
|
||||
<BuilderTable builders={resources as Types.BuilderListItem[]} />
|
||||
),
|
||||
|
||||
@@ -3,16 +3,22 @@ import { ResourceLink } from "../common";
|
||||
import { TableTags } from "@components/tags";
|
||||
import { BuilderInstanceType } from ".";
|
||||
import { Types } from "komodo_client";
|
||||
import { useSelectedResources } from "@lib/hooks";
|
||||
|
||||
export const BuilderTable = ({
|
||||
builders,
|
||||
}: {
|
||||
builders: Types.BuilderListItem[];
|
||||
}) => {
|
||||
const [_, setSelectedResources] = useSelectedResources("Builder");
|
||||
return (
|
||||
<DataTable
|
||||
tableKey="builders"
|
||||
data={builders}
|
||||
selectOptions={{
|
||||
selectKey: ({ name }) => name,
|
||||
onSelect: setSelectedResources,
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
|
||||
@@ -159,8 +159,7 @@ export const PullDeployment = ({ id }: DeploymentId) => {
|
||||
if (!deployment) return null;
|
||||
|
||||
return (
|
||||
<ActionWithDialog
|
||||
name={deployment.name}
|
||||
<ConfirmButton
|
||||
title="Pull Image"
|
||||
icon={<Download className="h-4 w-4" />}
|
||||
onClick={() => pull({ deployment: id })}
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
import { RenameResource } from "@components/config/util";
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@ui/hover-card";
|
||||
import { Card } from "@ui/card";
|
||||
import { GroupActions } from "@components/group-actions";
|
||||
|
||||
// const configOrLog = atomWithStorage("config-or-log-v1", "Config");
|
||||
|
||||
@@ -174,6 +175,19 @@ export const DeploymentComponents: RequiredResourceComponents = {
|
||||
);
|
||||
},
|
||||
|
||||
GroupActions: () => (
|
||||
<GroupActions
|
||||
type="Deployment"
|
||||
actions={[
|
||||
"PullDeployment",
|
||||
"Deploy",
|
||||
"RestartDeployment",
|
||||
"StopDeployment",
|
||||
"DestroyDeployment",
|
||||
]}
|
||||
/>
|
||||
),
|
||||
|
||||
Icon: ({ id }) => <DeploymentIcon id={id} size={4} />,
|
||||
BigIcon: ({ id }) => <DeploymentIcon id={id} size={8} />,
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { TableTags } from "@components/tags";
|
||||
import { Types } from "komodo_client";
|
||||
import { DataTable, SortableHeader } from "@ui/data-table";
|
||||
import { useRead } from "@lib/hooks";
|
||||
import { useRead, useSelectedResources } from "@lib/hooks";
|
||||
import { ResourceLink } from "../common";
|
||||
import { DeploymentComponents } from ".";
|
||||
import { HardDrive } from "lucide-react";
|
||||
@@ -17,10 +17,17 @@ export const DeploymentTable = ({
|
||||
(id: string) => servers?.find((server) => server.id === id)?.name,
|
||||
[servers]
|
||||
);
|
||||
|
||||
const [_, setSelectedResources] = useSelectedResources("Deployment");
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
tableKey="deployments"
|
||||
data={deployments}
|
||||
selectOptions={{
|
||||
selectKey: ({ name }) => name,
|
||||
onSelect: setSelectedResources,
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
|
||||
@@ -17,6 +17,7 @@ import { cn } from "@lib/utils";
|
||||
import { Types } from "komodo_client";
|
||||
import { DashboardPieChart } from "@pages/home/dashboard";
|
||||
import { RenameResource } from "@components/config/util";
|
||||
import { GroupActions } from "@components/group-actions";
|
||||
|
||||
const useProcedure = (id?: string) =>
|
||||
useRead("ListProcedures", {}).data?.find((d) => d.id === id);
|
||||
@@ -63,6 +64,8 @@ export const ProcedureComponents: RequiredResourceComponents = {
|
||||
|
||||
New: () => <NewResource type="Procedure" />,
|
||||
|
||||
GroupActions: () => <GroupActions type="Procedure" actions={["RunProcedure"]} />,
|
||||
|
||||
Table: ({ resources }) => (
|
||||
<ProcedureTable procedures={resources as Types.ProcedureListItem[]} />
|
||||
),
|
||||
|
||||
@@ -3,16 +3,23 @@ import { TableTags } from "@components/tags";
|
||||
import { ResourceLink } from "../common";
|
||||
import { ProcedureComponents } from ".";
|
||||
import { Types } from "komodo_client";
|
||||
import { useSelectedResources } from "@lib/hooks";
|
||||
|
||||
export const ProcedureTable = ({
|
||||
procedures,
|
||||
}: {
|
||||
procedures: Types.ProcedureListItem[];
|
||||
}) => {
|
||||
const [_, setSelectedResources] = useSelectedResources("Procedure");
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
tableKey="procedures"
|
||||
data={procedures}
|
||||
selectOptions={{
|
||||
selectKey: ({ name }) => name,
|
||||
onSelect: setSelectedResources,
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
|
||||
@@ -21,6 +21,7 @@ import { useToast } from "@ui/use-toast";
|
||||
import { Button } from "@ui/button";
|
||||
import { useBuilder } from "../builder";
|
||||
import { RenameResource } from "@components/config/util";
|
||||
import { GroupActions } from "@components/group-actions";
|
||||
|
||||
export const useRepo = (id?: string) =>
|
||||
useRead("ListRepos", {}, { refetchInterval: 10_000 }).data?.find(
|
||||
@@ -70,6 +71,13 @@ export const RepoComponents: RequiredResourceComponents = {
|
||||
|
||||
New: ({ server_id }) => <NewResource type="Repo" server_id={server_id} />,
|
||||
|
||||
GroupActions: () => (
|
||||
<GroupActions
|
||||
type="Repo"
|
||||
actions={["PullRepo", "CloneRepo", "BuildRepo"]}
|
||||
/>
|
||||
),
|
||||
|
||||
Table: ({ resources }) => (
|
||||
<RepoTable repos={resources as Types.RepoListItem[]} />
|
||||
),
|
||||
|
||||
@@ -3,12 +3,19 @@ import { ResourceLink } from "../common";
|
||||
import { TableTags } from "@components/tags";
|
||||
import { RepoComponents } from ".";
|
||||
import { Types } from "komodo_client";
|
||||
import { useSelectedResources } from "@lib/hooks";
|
||||
|
||||
export const RepoTable = ({ repos }: { repos: Types.RepoListItem[] }) => {
|
||||
const [_, setSelectedResources] = useSelectedResources("Repo");
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
tableKey="repos"
|
||||
data={repos}
|
||||
selectOptions={{
|
||||
selectKey: ({ name }) => name,
|
||||
onSelect: setSelectedResources,
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
@@ -37,9 +44,7 @@ export const RepoTable = ({ repos }: { repos: Types.RepoListItem[] }) => {
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="State" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<RepoComponents.State id={row.original.id} />
|
||||
),
|
||||
cell: ({ row }) => <RepoComponents.State id={row.original.id} />,
|
||||
size: 120,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -22,6 +22,7 @@ import { ResourceSyncInfo } from "./info";
|
||||
import { ResourceSyncPending } from "./pending";
|
||||
import { Badge } from "@ui/badge";
|
||||
import { RenameResource } from "@components/config/util";
|
||||
import { GroupActions } from "@components/group-actions";
|
||||
|
||||
export const useResourceSync = (id?: string) =>
|
||||
useRead("ListResourceSyncs", {}, { refetchInterval: 10_000 }).data?.find(
|
||||
@@ -49,8 +50,8 @@ const ConfigInfoPending = ({ id }: { id: string }) => {
|
||||
const hideInfo = sync?.config?.files_on_host
|
||||
? false
|
||||
: sync?.config?.file_contents
|
||||
? true
|
||||
: false;
|
||||
? true
|
||||
: false;
|
||||
|
||||
const showPending =
|
||||
sync && (!sync_no_changes(sync) || sync.info?.pending_error);
|
||||
@@ -59,10 +60,10 @@ const ConfigInfoPending = ({ id }: { id: string }) => {
|
||||
_view === "Info" && hideInfo
|
||||
? "Config"
|
||||
: _view === "Pending" && !showPending
|
||||
? sync?.config?.files_on_host || sync?.config?.repo
|
||||
? "Info"
|
||||
: "Config"
|
||||
: _view;
|
||||
? sync?.config?.files_on_host || sync?.config?.repo
|
||||
? "Info"
|
||||
: "Config"
|
||||
: _view;
|
||||
|
||||
const title = (
|
||||
<TabsList className="justify-start w-fit">
|
||||
@@ -144,6 +145,10 @@ export const ResourceSyncComponents: RequiredResourceComponents = {
|
||||
);
|
||||
},
|
||||
|
||||
GroupActions: () => (
|
||||
<GroupActions type="ResourceSync" actions={["RunSync"]} />
|
||||
),
|
||||
|
||||
Table: ({ resources }) => (
|
||||
<ResourceSyncTable syncs={resources as Types.ResourceSyncListItem[]} />
|
||||
),
|
||||
|
||||
@@ -3,16 +3,22 @@ import { ResourceLink } from "../common";
|
||||
import { TableTags } from "@components/tags";
|
||||
import { Types } from "komodo_client";
|
||||
import { ResourceSyncComponents } from ".";
|
||||
import { useSelectedResources } from "@lib/hooks";
|
||||
|
||||
export const ResourceSyncTable = ({
|
||||
syncs,
|
||||
}: {
|
||||
syncs: Types.ResourceSyncListItem[];
|
||||
}) => {
|
||||
const [_, setSelectedResources] = useSelectedResources("ResourceSync");
|
||||
return (
|
||||
<DataTable
|
||||
tableKey="syncs"
|
||||
data={syncs}
|
||||
selectOptions={{
|
||||
selectKey: ({ name }) => name,
|
||||
onSelect: setSelectedResources,
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
|
||||
@@ -21,6 +21,7 @@ import { ServerTemplateTable } from "./table";
|
||||
import { LaunchServer } from "./actions";
|
||||
import { ResourcePageHeader } from "@components/util";
|
||||
import { RenameResource } from "@components/config/util";
|
||||
import { GroupActions } from "@components/group-actions";
|
||||
|
||||
export const useServerTemplate = (id?: string) =>
|
||||
useRead("ListServerTemplates", {}).data?.find((d) => d.id === id);
|
||||
@@ -96,6 +97,8 @@ export const ServerTemplateComponents: RequiredResourceComponents = {
|
||||
);
|
||||
},
|
||||
|
||||
GroupActions: () => <GroupActions type="ServerTemplate" actions={[]} />,
|
||||
|
||||
Table: ({ resources }) => (
|
||||
<ServerTemplateTable
|
||||
serverTemplates={resources as Types.ServerTemplateListItem[]}
|
||||
|
||||
@@ -2,16 +2,22 @@ import { DataTable, SortableHeader } from "@ui/data-table";
|
||||
import { ResourceLink } from "../common";
|
||||
import { TableTags } from "@components/tags";
|
||||
import { Types } from "komodo_client";
|
||||
import { useSelectedResources } from "@lib/hooks";
|
||||
|
||||
export const ServerTemplateTable = ({
|
||||
serverTemplates,
|
||||
}: {
|
||||
serverTemplates: Types.ServerTemplateListItem[];
|
||||
}) => {
|
||||
const [_, setSelectedResources] = useSelectedResources("ServerTemplate");
|
||||
return (
|
||||
<DataTable
|
||||
tableKey="server-templates"
|
||||
data={serverTemplates}
|
||||
selectOptions={{
|
||||
selectKey: ({ name }) => name,
|
||||
onSelect: setSelectedResources,
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
|
||||
@@ -72,6 +72,17 @@ export const ServerConfig = ({
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Timeout",
|
||||
labelHidden: true,
|
||||
components: {
|
||||
timeout_seconds: {
|
||||
// boldLabel: true,
|
||||
description:
|
||||
"The timeout used with the server health check, in seconds.",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Disks",
|
||||
labelHidden: true,
|
||||
|
||||
@@ -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[]} />
|
||||
),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TableTags } from "@components/tags";
|
||||
import { useRead } from "@lib/hooks";
|
||||
import { useRead, useSelectedResources } from "@lib/hooks";
|
||||
import { DataTable, SortableHeader } from "@ui/data-table";
|
||||
import { ServerComponents } from ".";
|
||||
import { ResourceLink } from "../common";
|
||||
@@ -11,6 +11,7 @@ export const ServerTable = ({
|
||||
}: {
|
||||
servers: Types.ServerListItem[];
|
||||
}) => {
|
||||
const [_, setSelectedResources] = useSelectedResources("Server");
|
||||
const deployments = useRead("ListDeployments", {}).data;
|
||||
const stacks = useRead("ListStacks", {}).data;
|
||||
const repos = useRead("ListRepos", {}).data;
|
||||
@@ -28,6 +29,10 @@ export const ServerTable = ({
|
||||
<DataTable
|
||||
tableKey="servers"
|
||||
data={servers}
|
||||
selectOptions={{
|
||||
selectKey: ({ name }) => name,
|
||||
onSelect: setSelectedResources,
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
@@ -71,9 +76,7 @@ export const ServerTable = ({
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="State" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<ServerComponents.State id={row.original.id} />
|
||||
),
|
||||
cell: ({ row }) => <ServerComponents.State id={row.original.id} />,
|
||||
},
|
||||
{
|
||||
header: "Tags",
|
||||
|
||||
@@ -134,8 +134,7 @@ export const PullStack = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionWithDialog
|
||||
name={`${stack?.name}${service ? ` - ${service}` : ""}`}
|
||||
<ConfirmButton
|
||||
title={`Pull Image${service ? "" : "s"}`}
|
||||
icon={<Download className="h-4 w-4" />}
|
||||
onClick={() => pull({ stack: id, service })}
|
||||
|
||||
@@ -38,6 +38,7 @@ import { DashboardPieChart } from "@pages/home/dashboard";
|
||||
import { ResourcePageHeader, StatusBadge } from "@components/util";
|
||||
import { StackConfig } from "./config";
|
||||
import { RenameResource } from "@components/config/util";
|
||||
import { GroupActions } from "@components/group-actions";
|
||||
|
||||
export const useStack = (id?: string) =>
|
||||
useRead("ListStacks", {}, { refetchInterval: 10_000 }).data?.find(
|
||||
@@ -152,6 +153,19 @@ export const StackComponents: RequiredResourceComponents = {
|
||||
);
|
||||
},
|
||||
|
||||
GroupActions: () => (
|
||||
<GroupActions
|
||||
type="Stack"
|
||||
actions={[
|
||||
"PullStack",
|
||||
"DeployStack",
|
||||
"RestartStack",
|
||||
"StopStack",
|
||||
"DestroyStack",
|
||||
]}
|
||||
/>
|
||||
),
|
||||
|
||||
New: ({ server_id: _server_id }) => {
|
||||
const servers = useRead("ListServers", {}).data;
|
||||
const server_id = _server_id
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRead } from "@lib/hooks";
|
||||
import { useRead, useSelectedResources } from "@lib/hooks";
|
||||
import { DataTable, SortableHeader } from "@ui/data-table";
|
||||
import { ResourceLink } from "../common";
|
||||
import { TableTags } from "@components/tags";
|
||||
@@ -12,10 +12,17 @@ export const StackTable = ({ stacks }: { stacks: Types.StackListItem[] }) => {
|
||||
(id: string) => servers?.find((server) => server.id === id)?.name,
|
||||
[servers]
|
||||
);
|
||||
|
||||
const [_, setSelectedResources] = useSelectedResources("Stack");
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
tableKey="Stacks"
|
||||
data={stacks}
|
||||
selectOptions={{
|
||||
selectKey: ({ name }) => name,
|
||||
onSelect: setSelectedResources,
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import { UsableResource } from "@types";
|
||||
import { useToast } from "@ui/use-toast";
|
||||
import { atom, useAtom } from "jotai";
|
||||
import { atomFamily } from "jotai/utils";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { RESOURCE_TARGETS } from "./utils";
|
||||
@@ -466,19 +467,28 @@ export const useWebhookIdOrName = () => {
|
||||
|
||||
export type Dimensions = { width: number; height: number };
|
||||
export const useWindowDimensions = () => {
|
||||
const [dimensions, setDimensions] = useState<Dimensions>({ width: 0, height: 0 });
|
||||
const [dimensions, setDimensions] = useState<Dimensions>({
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
useEffect(() => {
|
||||
const callback = () => {
|
||||
setDimensions({
|
||||
width: window.screen.availWidth,
|
||||
height: window.screen.availHeight,
|
||||
});
|
||||
}
|
||||
};
|
||||
callback();
|
||||
window.addEventListener("resize", callback);
|
||||
return () => {
|
||||
window.removeEventListener("resize", callback);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
return dimensions;
|
||||
}
|
||||
};
|
||||
|
||||
const selected_resources = atomFamily((_: UsableResource) =>
|
||||
atom<string[]>([])
|
||||
);
|
||||
export const useSelectedResources = (type: UsableResource) =>
|
||||
useAtom(selected_resources(type));
|
||||
|
||||
@@ -138,6 +138,12 @@ export const usableResourcePath = (resource: UsableResource) => {
|
||||
return `${resource.toLowerCase()}s`;
|
||||
};
|
||||
|
||||
export const usableResourceExecuteKey = (resource: UsableResource) => {
|
||||
if (resource === "ServerTemplate") return "template";
|
||||
if (resource === "ResourceSync") return "sync";
|
||||
return `${resource.toLowerCase()}`;
|
||||
};
|
||||
|
||||
export const sanitizeOnlySpan = (log: string) => {
|
||||
return sanitizeHtml(log, {
|
||||
allowedTags: ["span"],
|
||||
|
||||
@@ -24,8 +24,8 @@ export const Resources = () => {
|
||||
type === "ServerTemplate"
|
||||
? "Server Template"
|
||||
: type === "ResourceSync"
|
||||
? "Resource Sync"
|
||||
: type;
|
||||
? "Resource Sync"
|
||||
: type;
|
||||
useSetTitle(name + "s");
|
||||
const [search, set] = useState("");
|
||||
const resources = useRead(`List${type}s`, {}).data;
|
||||
@@ -57,11 +57,10 @@ export const Resources = () => {
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
{is_admin || !disable_non_admin_create ? (
|
||||
<Components.New />
|
||||
) : (
|
||||
<div></div>
|
||||
)}
|
||||
<div className="flex gap-4">
|
||||
{(is_admin || !disable_non_admin_create) && <Components.New />}
|
||||
<Components.GroupActions />
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<TagsFilter />
|
||||
<div className="relative">
|
||||
|
||||
3
frontend/src/types.d.ts
vendored
3
frontend/src/types.d.ts
vendored
@@ -25,6 +25,9 @@ export interface RequiredResourceComponents {
|
||||
/** A table component to view resource list */
|
||||
Table: React.FC<{ resources: Types.ResourceListItem<unknown>[] }>;
|
||||
|
||||
/** Dropdown menu to trigger group actions for selected resources */
|
||||
GroupActions: React.FC;
|
||||
|
||||
/** Icon for the component */
|
||||
Icon: OptionalIdComponent;
|
||||
BigIcon: OptionalIdComponent;
|
||||
|
||||
@@ -2,10 +2,12 @@ import { cn } from "@lib/utils";
|
||||
import {
|
||||
Column,
|
||||
ColumnDef,
|
||||
SortingState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
Row,
|
||||
RowSelectionState,
|
||||
SortingState,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
@@ -19,6 +21,7 @@ import {
|
||||
} from "@ui/table";
|
||||
import { ArrowDown, ArrowUp, Minus } from "lucide-react";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import { Checkbox } from "./checkbox";
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
/** Unique key given to table so sorting can be remembered on local storage */
|
||||
@@ -29,6 +32,11 @@ interface DataTableProps<TData, TValue> {
|
||||
noResults?: ReactNode;
|
||||
defaultSort?: SortingState;
|
||||
sortDescFirst?: boolean;
|
||||
selectOptions?: {
|
||||
selectKey: (row: TData) => string;
|
||||
onSelect: (selected: string[]) => void;
|
||||
disableRow?: boolean | ((row: Row<TData>) => boolean);
|
||||
};
|
||||
}
|
||||
|
||||
export function DataTable<TData, TValue>({
|
||||
@@ -39,9 +47,14 @@ export function DataTable<TData, TValue>({
|
||||
noResults,
|
||||
sortDescFirst = false,
|
||||
defaultSort = [],
|
||||
selectOptions,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const [sorting, setSorting] = useState<SortingState>(defaultSort);
|
||||
|
||||
// intentionally not initialized to clear selected values on table mount
|
||||
// could add some prop for adding default selected state to preserve between mounts
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns: columns.filter((c) => c) as any,
|
||||
@@ -50,8 +63,12 @@ export function DataTable<TData, TValue>({
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
state: {
|
||||
sorting,
|
||||
rowSelection,
|
||||
},
|
||||
sortDescFirst,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getRowId: selectOptions?.selectKey,
|
||||
enableRowSelection: selectOptions?.disableRow,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -66,12 +83,31 @@ export function DataTable<TData, TValue>({
|
||||
}
|
||||
}, [tableKey, sorting]);
|
||||
|
||||
useEffect(() => {
|
||||
selectOptions?.onSelect(Object.keys(rowSelection));
|
||||
}, [rowSelection]);
|
||||
|
||||
return (
|
||||
<div className="rounded-md border bg-card text-card-foreground shadow py-1 px-1">
|
||||
<Table className="xl:table-fixed border-separate border-spacing-0">
|
||||
<TableHeader className="sticky top-0">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
{table.getHeaderGroups().map((headerGroup, i) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{/* placeholder header */}
|
||||
{i === 0 && selectOptions && (
|
||||
<TableHead className="w-8 relative whitespace-nowrap bg-background border-b border-r last:border-r-0">
|
||||
<Checkbox
|
||||
className="ml-2"
|
||||
disabled={selectOptions.disableRow === true}
|
||||
checked={
|
||||
table.getIsSomeRowsSelected()
|
||||
? "indeterminate"
|
||||
: table.getIsAllRowsSelected()
|
||||
}
|
||||
onCheckedChange={() => table.toggleAllRowsSelected()}
|
||||
/>
|
||||
</TableHead>
|
||||
)}
|
||||
{headerGroup.headers.map((header) => {
|
||||
const size = header.column.getSize();
|
||||
return (
|
||||
@@ -105,6 +141,18 @@ export function DataTable<TData, TValue>({
|
||||
onRowClick && "cursor-pointer"
|
||||
)}
|
||||
>
|
||||
{selectOptions && (
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
disabled={!row.getCanSelect()}
|
||||
className="ml-2"
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(c) =>
|
||||
c !== "indeterminate" && row.toggleSelected()
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
)}
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
const size = cell.column.getSize();
|
||||
return (
|
||||
|
||||
@@ -56,7 +56,7 @@ pub async fn write_file(
|
||||
|
||||
if !replacers.is_empty() {
|
||||
logs.push(Log::simple(
|
||||
"interpolate periphery secrets",
|
||||
"Interpolate - Environment",
|
||||
replacers
|
||||
.iter()
|
||||
.map(|(_, variable)| format!("<span class=\"text-muted-foreground\">replaced:</span> {variable}"))
|
||||
|
||||
@@ -3,8 +3,7 @@ use std::time::Duration;
|
||||
use opentelemetry::{global, trace::TracerProvider, KeyValue};
|
||||
use opentelemetry_otlp::WithExportConfig;
|
||||
use opentelemetry_sdk::{
|
||||
runtime,
|
||||
trace::{BatchConfig, Sampler, Tracer},
|
||||
trace::{Sampler, Tracer},
|
||||
Resource,
|
||||
};
|
||||
use opentelemetry_semantic_conventions::{
|
||||
@@ -23,22 +22,22 @@ fn resource(service_name: String) -> Resource {
|
||||
}
|
||||
|
||||
pub fn tracer(endpoint: &str, service_name: String) -> Tracer {
|
||||
let provider = opentelemetry_otlp::new_pipeline()
|
||||
.tracing()
|
||||
.with_trace_config(
|
||||
let provider = opentelemetry_sdk::trace::TracerProvider::builder()
|
||||
.with_config(
|
||||
opentelemetry_sdk::trace::Config::default()
|
||||
.with_sampler(Sampler::AlwaysOn)
|
||||
.with_resource(resource(service_name.clone())),
|
||||
)
|
||||
.with_batch_config(BatchConfig::default())
|
||||
.with_exporter(
|
||||
opentelemetry_otlp::new_exporter()
|
||||
.tonic()
|
||||
.with_batch_exporter(
|
||||
opentelemetry_otlp::SpanExporter::builder()
|
||||
.with_tonic()
|
||||
.with_endpoint(endpoint)
|
||||
.with_timeout(Duration::from_secs(3)),
|
||||
.with_timeout(Duration::from_secs(3))
|
||||
.build()
|
||||
.unwrap(),
|
||||
opentelemetry_sdk::runtime::Tokio,
|
||||
)
|
||||
.install_batch(runtime::Tokio)
|
||||
.unwrap();
|
||||
.build();
|
||||
global::set_tracer_provider(provider.clone());
|
||||
provider.tracer(service_name)
|
||||
}
|
||||
|
||||
15
runfile.toml
15
runfile.toml
@@ -46,13 +46,24 @@ cmd = """
|
||||
docker compose -p komodo-dev -f test.compose.yaml build"""
|
||||
|
||||
[test-core]
|
||||
description = "runs core --release pointing to test.core.config.toml"
|
||||
cmd = "KOMODO_CONFIG_PATH=test.core.config.toml cargo run -p komodo_core --release"
|
||||
description = "runs core --release pointing to .komodo/core.config.toml"
|
||||
cmd = "KOMODO_CONFIG_PATH=.komodo/core.config.toml cargo run -p komodo_core --release"
|
||||
|
||||
[test-periphery]
|
||||
description = "runs periphery --release pointing to test.periphery.config.toml"
|
||||
cmd = "PERIPHERY_CONFIG_PATH=test.periphery.config.toml cargo run -p komodo_periphery --release"
|
||||
|
||||
[create-multiarch-builder]
|
||||
cmd = "docker buildx create --name builder --use --bootstrap"
|
||||
|
||||
[build-multiarch-periphery]
|
||||
cmd = """
|
||||
docker build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
-f bin/periphery/cross-compile.Dockerfile \
|
||||
.
|
||||
"""
|
||||
|
||||
[docsite-start]
|
||||
path = "docsite"
|
||||
cmd = "yarn start"
|
||||
|
||||
@@ -37,6 +37,8 @@ Will install to paths:
|
||||
*Note*. Ensure the user running periphery has write permissions to the configured folders `repo_dir`, `stack_dir`, and `ssl_key_file` / `ssl_cert_file` parent folder.
|
||||
This allows periphery to clone repos, write compose files, and generate ssl certs.
|
||||
|
||||
*Note*. To ensure periphery stays running when your user logs out, use `sudo loginctl enable-linger $USER`.
|
||||
|
||||
For example in `periphery.config.toml`, running under `ubuntu` user:
|
||||
```toml
|
||||
repo_dir = "/home/ubuntu/.komodo/repos"
|
||||
|
||||
@@ -66,11 +66,13 @@ def copy_binary(user_install, bin_dir, version):
|
||||
if os.path.isfile(bin_path):
|
||||
os.remove(bin_path)
|
||||
|
||||
periphery_bin = "periphery"
|
||||
periphery_bin = "periphery-x86_64"
|
||||
arch = platform.machine().lower()
|
||||
if arch == "aarch64" or arch == "amd64":
|
||||
print("aarch64 detected")
|
||||
periphery_bin = "periphery-aarch64"
|
||||
else:
|
||||
print("using x86_64 binary")
|
||||
|
||||
# download the binary to bin path
|
||||
print(os.popen(f'curl -sSL https://github.com/mbecker20/komodo/releases/download/{version}/{periphery_bin} > {bin_path}').read())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
###########################
|
||||
############################
|
||||
# 🦎 KOMODO CORE CONFIG 🦎 #
|
||||
###########################
|
||||
############################
|
||||
title = "Komodo Test"
|
||||
host = "http://localhost:9121"
|
||||
port = 9121
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
################################
|
||||
#################################
|
||||
# 🦎 KOMODO PERIPHERY CONFIG 🦎 #
|
||||
################################
|
||||
#################################
|
||||
port = 8121
|
||||
repo_dir = ".komodo/repos"
|
||||
stack_dir = ".komodo/stacks"
|
||||
@@ -11,7 +11,7 @@ include_disk_mounts = []
|
||||
############
|
||||
# Security #
|
||||
############
|
||||
ssl_enabled = false
|
||||
ssl_enabled = true
|
||||
ssl_key_file = ".komodo/ssl/key.pem"
|
||||
ssl_cert_file = ".komodo/ssl/cert.pem"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user