forked from github-starred/komodo
Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3d8e603ec | ||
|
|
8b5c179473 | ||
|
|
8582bc92da | ||
|
|
8ee270d045 | ||
|
|
2cfae525e9 | ||
|
|
80e5d2a972 | ||
|
|
6f22c011a6 | ||
|
|
401cccee79 | ||
|
|
654b923f98 | ||
|
|
61261be70f | ||
|
|
46418125e3 | ||
|
|
e029e94f0d | ||
|
|
3be2b5163b | ||
|
|
6a145f58ff | ||
|
|
f1cede2ebd | ||
|
|
a5cfa1d412 | ||
|
|
a0674654c1 | ||
|
|
3faa1c58c1 | ||
|
|
7e296f34af | ||
|
|
9f8ced190c | ||
|
|
c194bb16d8 | ||
|
|
39fec9b55e | ||
|
|
e97ed9888d | ||
|
|
559102ffe3 | ||
|
|
6bf80ddcc7 | ||
|
|
89dbe1b4d9 | ||
|
|
334e16d646 | ||
|
|
a7bbe519f4 | ||
|
|
5827486c5a | ||
|
|
8ca8f7eddd | ||
|
|
0600276b43 | ||
|
|
a77a1495c7 | ||
|
|
021ed5d15f | ||
|
|
7d4376f426 | ||
|
|
7e9b406a34 | ||
|
|
dcf78b05b3 | ||
|
|
3236302d05 | ||
|
|
fc41258d6c | ||
|
|
ae8df90361 | ||
|
|
7d05b2677f | ||
|
|
2f55468a4c | ||
|
|
a20bd2c23f | ||
|
|
b3aa0ffa78 | ||
|
|
8e58a283cd | ||
|
|
9b2d9932ef | ||
|
|
7cb093ade1 | ||
|
|
e2f73d8474 | ||
|
|
12abd5a5bd | ||
|
|
f349cdf50d | ||
|
|
796bcac952 | ||
|
|
fed05684aa |
@@ -5,4 +5,10 @@ LICENSE
|
||||
*.code-workspace
|
||||
|
||||
*/node_modules
|
||||
*/dist
|
||||
*/dist
|
||||
|
||||
creds.toml
|
||||
.core-repos
|
||||
.repos
|
||||
.stacks
|
||||
.ssl
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,11 +1,14 @@
|
||||
target
|
||||
/frontend/build
|
||||
node_modules
|
||||
/lib/ts_client/build
|
||||
node_modules
|
||||
dist
|
||||
.env
|
||||
.env.development
|
||||
.DS_Store
|
||||
|
||||
creds.toml
|
||||
core.config.toml
|
||||
.syncs
|
||||
.stacks
|
||||
.core-repos
|
||||
.repos
|
||||
.stacks
|
||||
.ssl
|
||||
899
Cargo.lock
generated
899
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
64
Cargo.toml
64
Cargo.toml
@@ -3,7 +3,7 @@ resolver = "2"
|
||||
members = ["bin/*", "lib/*", "client/core/rs", "client/periphery/rs"]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.14.0"
|
||||
version = "1.15.5"
|
||||
edition = "2021"
|
||||
authors = ["mbecker20 <becker.maxh@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
@@ -15,9 +15,10 @@ homepage = "https://komo.do"
|
||||
|
||||
[workspace.dependencies]
|
||||
# LOCAL
|
||||
# komodo_client = "1.14.0"
|
||||
# komodo_client = "1.14.3"
|
||||
komodo_client = { path = "client/core/rs" }
|
||||
periphery_client = { path = "client/periphery/rs" }
|
||||
environment_file = { path = "lib/environment_file" }
|
||||
formatting = { path = "lib/formatting" }
|
||||
command = { path = "lib/command" }
|
||||
logger = { path = "lib/logger" }
|
||||
@@ -26,7 +27,7 @@ git = { path = "lib/git" }
|
||||
# MOGH
|
||||
run_command = { version = "0.0.6", features = ["async_tokio"] }
|
||||
serror = { version = "0.4.6", default-features = false }
|
||||
slack = { version = "0.1.0", package = "slack_client_rs" }
|
||||
slack = { version = "0.2.0", package = "slack_client_rs" }
|
||||
derive_default_builder = "0.1.8"
|
||||
derive_empty_traits = "0.1.0"
|
||||
merge_config_files = "0.1.5"
|
||||
@@ -40,46 +41,48 @@ mungos = "1.1.0"
|
||||
svi = "1.0.1"
|
||||
|
||||
# ASYNC
|
||||
tokio = { version = "1.40.0", features = ["full"] }
|
||||
reqwest = { version = "0.12.7", features = ["json"] }
|
||||
tokio-util = "0.7.11"
|
||||
futures = "0.3.30"
|
||||
futures-util = "0.3.30"
|
||||
reqwest = { version = "0.12.8", features = ["json"] }
|
||||
tokio = { version = "1.38.1", features = ["full"] }
|
||||
tokio-util = "0.7.12"
|
||||
futures = "0.3.31"
|
||||
futures-util = "0.3.31"
|
||||
|
||||
# SERVER
|
||||
axum = { version = "0.7.5", features = ["ws", "json"] }
|
||||
axum-extra = { version = "0.9.3", features = ["typed-header"] }
|
||||
tower-http = { version = "0.5.2", features = ["fs", "cors"] }
|
||||
tokio-tungstenite = "0.23.1"
|
||||
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"] }
|
||||
tokio-tungstenite = "0.24.0"
|
||||
|
||||
# SER/DE
|
||||
ordered_hash_map = { version = "0.4.0", features = ["serde"] }
|
||||
serde = { version = "1.0.209", features = ["derive"] }
|
||||
serde = { version = "1.0.210", features = ["derive"] }
|
||||
strum = { version = "0.26.3", features = ["derive"] }
|
||||
serde_json = "1.0.127"
|
||||
serde_json = "1.0.128"
|
||||
serde_yaml = "0.9.34"
|
||||
toml = "0.8.19"
|
||||
|
||||
# ERROR
|
||||
anyhow = "1.0.86"
|
||||
thiserror = "1.0.63"
|
||||
anyhow = "1.0.89"
|
||||
thiserror = "1.0.64"
|
||||
|
||||
# LOGGING
|
||||
opentelemetry_sdk = { version = "0.24.1", features = ["rt-tokio"] }
|
||||
opentelemetry_sdk = { version = "0.25.0", features = ["rt-tokio"] }
|
||||
tracing-subscriber = { version = "0.3.18", features = ["json"] }
|
||||
opentelemetry-semantic-conventions = "0.16.0"
|
||||
tracing-opentelemetry = "0.25.0"
|
||||
opentelemetry-otlp = "0.17.0"
|
||||
opentelemetry = "0.24.0"
|
||||
opentelemetry-semantic-conventions = "0.25.0"
|
||||
tracing-opentelemetry = "0.26.0"
|
||||
opentelemetry-otlp = "0.25.0"
|
||||
opentelemetry = "0.25.0"
|
||||
tracing = "0.1.40"
|
||||
|
||||
# CONFIG
|
||||
clap = { version = "4.5.16", features = ["derive"] }
|
||||
clap = { version = "4.5.20", features = ["derive"] }
|
||||
dotenvy = "0.15.7"
|
||||
envy = "0.4.2"
|
||||
|
||||
# CRYPTO
|
||||
# CRYPTO / AUTH
|
||||
uuid = { version = "1.10.0", features = ["v4", "fast-rng", "serde"] }
|
||||
openidconnect = "3.5.0"
|
||||
urlencoding = "2.1.3"
|
||||
nom_pem = "4.0.0"
|
||||
bcrypt = "0.15.1"
|
||||
@@ -92,18 +95,17 @@ hex = "0.4.3"
|
||||
|
||||
# SYSTEM
|
||||
bollard = "0.17.1"
|
||||
sysinfo = "0.31.4"
|
||||
sysinfo = "0.32.0"
|
||||
|
||||
# CLOUD
|
||||
aws-config = "1.5.5"
|
||||
aws-sdk-ec2 = "1.70.0"
|
||||
aws-sdk-ecr = "1.42.0"
|
||||
aws-config = "1.5.8"
|
||||
aws-sdk-ec2 = "1.77.0"
|
||||
|
||||
# MISC
|
||||
derive_builder = "0.20.1"
|
||||
derive_builder = "0.20.2"
|
||||
typeshare = "1.0.3"
|
||||
octorust = "0.7.0"
|
||||
dashmap = "6.1.0"
|
||||
colored = "2.1.0"
|
||||
regex = "1.10.6"
|
||||
bson = "2.11.0"
|
||||
|
||||
regex = "1.11.0"
|
||||
bson = "2.13.0"
|
||||
@@ -117,6 +117,12 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
|
||||
Execution::PruneVolumes(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::PruneDockerBuilders(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::PruneBuildx(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::PruneSystem(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
@@ -252,6 +258,12 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
|
||||
Execution::PruneVolumes(request) => {
|
||||
komodo_client().execute(request).await
|
||||
}
|
||||
Execution::PruneDockerBuilders(request) => {
|
||||
komodo_client().execute(request).await
|
||||
}
|
||||
Execution::PruneBuildx(request) => {
|
||||
komodo_client().execute(request).await
|
||||
}
|
||||
Execution::PruneSystem(request) => {
|
||||
komodo_client().execute(request).await
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ path = "src/main.rs"
|
||||
# local
|
||||
komodo_client = { workspace = true, features = ["mongo"] }
|
||||
periphery_client.workspace = true
|
||||
environment_file.workspace = true
|
||||
formatting.workspace = true
|
||||
logger.workspace = true
|
||||
git.workspace = true
|
||||
@@ -29,15 +30,15 @@ derive_variants.workspace = true
|
||||
mongo_indexed.workspace = true
|
||||
resolver_api.workspace = true
|
||||
toml_pretty.workspace = true
|
||||
run_command.workspace = true
|
||||
mungos.workspace = true
|
||||
slack.workspace = true
|
||||
svi.workspace = true
|
||||
# external
|
||||
axum-server.workspace = true
|
||||
ordered_hash_map.workspace = true
|
||||
openidconnect.workspace = true
|
||||
urlencoding.workspace = true
|
||||
aws-sdk-ec2.workspace = true
|
||||
aws-sdk-ecr.workspace = true
|
||||
aws-config.workspace = true
|
||||
tokio-util.workspace = true
|
||||
axum-extra.workspace = true
|
||||
@@ -46,6 +47,7 @@ serde_json.workspace = true
|
||||
serde_yaml.workspace = true
|
||||
typeshare.workspace = true
|
||||
octorust.workspace = true
|
||||
dashmap.workspace = true
|
||||
tracing.workspace = true
|
||||
reqwest.workspace = true
|
||||
futures.workspace = true
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
# Build Core
|
||||
FROM rust:1.80.1-bookworm AS core-builder
|
||||
WORKDIR /builder
|
||||
COPY . .
|
||||
RUN cargo build -p komodo_core --release
|
||||
|
||||
# Build Frontend
|
||||
FROM node:20.12-alpine AS frontend-builder
|
||||
WORKDIR /builder
|
||||
COPY ./frontend ./frontend
|
||||
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:bookworm-slim
|
||||
|
||||
# Install Deps
|
||||
RUN apt update && apt install -y git curl unzip ca-certificates && \
|
||||
curl -SL https://github.com/docker/compose/releases/download/v2.29.1/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose && \
|
||||
chmod +x /usr/local/bin/docker-compose && \
|
||||
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" && \
|
||||
unzip awscliv2.zip && \
|
||||
./aws/install
|
||||
|
||||
# Copy
|
||||
COPY ./config_example/core.config.example.toml /config/config.toml
|
||||
COPY --from=core-builder /builder/target/release/core /
|
||||
COPY --from=frontend-builder /builder/frontend/dist /frontend
|
||||
|
||||
# Hint at the port
|
||||
EXPOSE 9000
|
||||
|
||||
# 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
|
||||
|
||||
CMD ["./core"]
|
||||
45
bin/core/alpine.Dockerfile
Normal file
45
bin/core/alpine.Dockerfile
Normal file
@@ -0,0 +1,45 @@
|
||||
## 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 Core
|
||||
FROM rust:1.81.0-alpine AS core-builder
|
||||
WORKDIR /builder
|
||||
RUN apk update && apk --no-cache add musl-dev openssl-dev openssl-libs-static
|
||||
COPY . .
|
||||
RUN cargo build -p komodo_core --release
|
||||
|
||||
# Build Frontend
|
||||
FROM node:20.12-alpine AS frontend-builder
|
||||
WORKDIR /builder
|
||||
COPY ./frontend ./frontend
|
||||
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 alpine:3.20
|
||||
|
||||
# Install Deps
|
||||
RUN apk update && apk add --no-cache --virtual .build-deps \
|
||||
openssl ca-certificates git git-lfs
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# Using ENTRYPOINT allows cli args to be passed, eg using "command" in docker compose.
|
||||
ENTRYPOINT [ "/app/core" ]
|
||||
39
bin/core/debian.Dockerfile
Normal file
39
bin/core/debian.Dockerfile
Normal file
@@ -0,0 +1,39 @@
|
||||
# Build Core
|
||||
FROM rust:1.81.0-bullseye AS core-builder
|
||||
WORKDIR /builder
|
||||
COPY . .
|
||||
RUN cargo build -p komodo_core --release
|
||||
|
||||
# Build Frontend
|
||||
FROM node:20.12-alpine AS frontend-builder
|
||||
WORKDIR /builder
|
||||
COPY ./frontend ./frontend
|
||||
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
|
||||
|
||||
# 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 [ "/app/core" ]
|
||||
169
bin/core/src/alert/discord.rs
Normal file
169
bin/core/src/alert/discord.rs
Normal file
@@ -0,0 +1,169 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn send_alert(
|
||||
url: &str,
|
||||
alert: &Alert,
|
||||
) -> anyhow::Result<()> {
|
||||
let level = fmt_level(alert.level);
|
||||
let content = match &alert.data {
|
||||
AlertData::ServerUnreachable {
|
||||
id,
|
||||
name,
|
||||
region,
|
||||
err,
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
let link = resource_link(ResourceTargetVariant::Server, id);
|
||||
match alert.level {
|
||||
SeverityLevel::Ok => {
|
||||
format!(
|
||||
"{level} | *{name}*{region} is now *reachable*\n{link}"
|
||||
)
|
||||
}
|
||||
SeverityLevel::Critical => {
|
||||
let err = err
|
||||
.as_ref()
|
||||
.map(|e| format!("\n**error**: {e:#?}"))
|
||||
.unwrap_or_default();
|
||||
format!(
|
||||
"{level} | *{name}*{region} is *unreachable* ❌\n{link}{err}"
|
||||
)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
AlertData::ServerCpu {
|
||||
id,
|
||||
name,
|
||||
region,
|
||||
percentage,
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
let link = resource_link(ResourceTargetVariant::Server, id);
|
||||
format!(
|
||||
"{level} | *{name}*{region} cpu usage at *{percentage:.1}%*\n{link}"
|
||||
)
|
||||
}
|
||||
AlertData::ServerMem {
|
||||
id,
|
||||
name,
|
||||
region,
|
||||
used_gb,
|
||||
total_gb,
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
let link = resource_link(ResourceTargetVariant::Server, id);
|
||||
let percentage = 100.0 * used_gb / total_gb;
|
||||
format!(
|
||||
"{level} | *{name}*{region} memory usage at *{percentage:.1}%* 💾\n\nUsing *{used_gb:.1} GiB* / *{total_gb:.1} GiB*\n{link}"
|
||||
)
|
||||
}
|
||||
AlertData::ServerDisk {
|
||||
id,
|
||||
name,
|
||||
region,
|
||||
path,
|
||||
used_gb,
|
||||
total_gb,
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
let link = resource_link(ResourceTargetVariant::Server, id);
|
||||
let percentage = 100.0 * used_gb / total_gb;
|
||||
format!(
|
||||
"{level} | *{name}*{region} disk usage at *{percentage:.1}%* 💿\nmount point: `{path:?}`\nusing *{used_gb:.1} GiB* / *{total_gb:.1} GiB*\n{link}"
|
||||
)
|
||||
}
|
||||
AlertData::ContainerStateChange {
|
||||
id,
|
||||
name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
from,
|
||||
to,
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Deployment, id);
|
||||
let to = fmt_docker_container_state(to);
|
||||
format!("📦 Deployment *{name}* is now {to}\nserver: {server_name}\nprevious: {from}\n{link}")
|
||||
}
|
||||
AlertData::StackStateChange {
|
||||
id,
|
||||
name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
from,
|
||||
to,
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Stack, id);
|
||||
let to = fmt_stack_state(to);
|
||||
format!("🥞 Stack *{name}* is now {to}\nserver: {server_name}\nprevious: {from}\n{link}")
|
||||
}
|
||||
AlertData::AwsBuilderTerminationFailed {
|
||||
instance_id,
|
||||
message,
|
||||
} => {
|
||||
format!("{level} | Failed to terminated AWS builder instance\ninstance id: *{instance_id}*\n{message}")
|
||||
}
|
||||
AlertData::ResourceSyncPendingUpdates { id, name } => {
|
||||
let link =
|
||||
resource_link(ResourceTargetVariant::ResourceSync, id);
|
||||
format!(
|
||||
"{level} | Pending resource sync updates on *{name}*\n{link}"
|
||||
)
|
||||
}
|
||||
AlertData::BuildFailed { id, name, version } => {
|
||||
let link = resource_link(ResourceTargetVariant::Build, id);
|
||||
format!("{level} | Build *{name}* failed\nversion: v{version}\n{link}")
|
||||
}
|
||||
AlertData::RepoBuildFailed { id, name } => {
|
||||
let link = resource_link(ResourceTargetVariant::Repo, id);
|
||||
format!("{level} | Repo build for *{name}* failed\n{link}")
|
||||
}
|
||||
AlertData::None {} => Default::default(),
|
||||
};
|
||||
if !content.is_empty() {
|
||||
send_message(url, &content).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_message(
|
||||
url: &str,
|
||||
content: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let body = DiscordMessageBody { content };
|
||||
|
||||
let response = http_client()
|
||||
.post(url)
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send message")?;
|
||||
|
||||
let status = response.status();
|
||||
|
||||
if status.is_success() {
|
||||
Ok(())
|
||||
} else {
|
||||
let text = response.text().await.with_context(|| {
|
||||
format!("Failed to send message to Discord | {status} | failed to get response text")
|
||||
})?;
|
||||
Err(anyhow::anyhow!(
|
||||
"Failed to send message to Discord | {status} | {text}"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn http_client() -> &'static reqwest::Client {
|
||||
static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
|
||||
CLIENT.get_or_init(reqwest::Client::new)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct DiscordMessageBody<'a> {
|
||||
content: &'a str,
|
||||
}
|
||||
213
bin/core/src/alert/mod.rs
Normal file
213
bin/core/src/alert/mod.rs
Normal file
@@ -0,0 +1,213 @@
|
||||
use ::slack::types::Block;
|
||||
use anyhow::{anyhow, Context};
|
||||
use derive_variants::ExtractVariant;
|
||||
use futures::future::join_all;
|
||||
use komodo_client::entities::{
|
||||
alert::{Alert, AlertData, SeverityLevel},
|
||||
alerter::*,
|
||||
deployment::DeploymentState,
|
||||
stack::StackState,
|
||||
ResourceTargetVariant,
|
||||
};
|
||||
use mungos::{find::find_collect, mongodb::bson::doc};
|
||||
use tracing::Instrument;
|
||||
|
||||
use crate::{config::core_config, state::db_client};
|
||||
|
||||
mod discord;
|
||||
mod slack;
|
||||
|
||||
pub async fn send_alerts(alerts: &[Alert]) {
|
||||
if alerts.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let span =
|
||||
info_span!("send_alerts", alerts = format!("{alerts:?}"));
|
||||
async {
|
||||
let Ok(alerters) = find_collect(
|
||||
&db_client().alerters,
|
||||
doc! { "config.enabled": true },
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
error!(
|
||||
"ERROR sending alerts | failed to get alerters from db | {e:#}"
|
||||
)
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let handles =
|
||||
alerts.iter().map(|alert| send_alert(&alerters, alert));
|
||||
|
||||
join_all(handles).await;
|
||||
}
|
||||
.instrument(span)
|
||||
.await
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
async fn send_alert(alerters: &[Alerter], alert: &Alert) {
|
||||
if alerters.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let alert_type = alert.data.extract_variant();
|
||||
|
||||
let handles = alerters.iter().map(|alerter| async {
|
||||
// Don't send if not enabled
|
||||
if !alerter.config.enabled {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Don't send if alert type not configured on the alerter
|
||||
if !alerter.config.alert_types.is_empty()
|
||||
&& !alerter.config.alert_types.contains(&alert_type)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Don't send if resource is in the blacklist
|
||||
if alerter.config.except_resources.contains(&alert.target) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Don't send if whitelist configured and target is not included
|
||||
if !alerter.config.resources.is_empty()
|
||||
&& !alerter.config.resources.contains(&alert.target)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match &alerter.config.endpoint {
|
||||
AlerterEndpoint::Custom(CustomAlerterEndpoint { url }) => {
|
||||
send_custom_alert(url, alert).await.with_context(|| {
|
||||
format!(
|
||||
"failed to send alert to custom alerter {}",
|
||||
alerter.name
|
||||
)
|
||||
})
|
||||
}
|
||||
AlerterEndpoint::Slack(SlackAlerterEndpoint { url }) => {
|
||||
slack::send_alert(url, alert).await.with_context(|| {
|
||||
format!(
|
||||
"failed to send alert to slack alerter {}",
|
||||
alerter.name
|
||||
)
|
||||
})
|
||||
}
|
||||
AlerterEndpoint::Discord(DiscordAlerterEndpoint { url }) => {
|
||||
discord::send_alert(url, alert).await.with_context(|| {
|
||||
format!(
|
||||
"failed to send alert to Discord alerter {}",
|
||||
alerter.name
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
join_all(handles)
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|res| res.err())
|
||||
.for_each(|e| error!("{e:#}"));
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
async fn send_custom_alert(
|
||||
url: &str,
|
||||
alert: &Alert,
|
||||
) -> anyhow::Result<()> {
|
||||
let res = reqwest::Client::new()
|
||||
.post(url)
|
||||
.json(alert)
|
||||
.send()
|
||||
.await
|
||||
.context("failed at post request to alerter")?;
|
||||
let status = res.status();
|
||||
if !status.is_success() {
|
||||
let text = res
|
||||
.text()
|
||||
.await
|
||||
.context("failed to get response text on alerter response")?;
|
||||
return Err(anyhow!(
|
||||
"post to alerter failed | {status} | {text}"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fmt_region(region: &Option<String>) -> String {
|
||||
match region {
|
||||
Some(region) => format!(" ({region})"),
|
||||
None => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt_docker_container_state(state: &DeploymentState) -> String {
|
||||
match state {
|
||||
DeploymentState::Running => String::from("Running ▶️"),
|
||||
DeploymentState::Exited => String::from("Exited 🛑"),
|
||||
DeploymentState::Restarting => String::from("Restarting 🔄"),
|
||||
DeploymentState::NotDeployed => String::from("Not Deployed"),
|
||||
_ => state.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt_stack_state(state: &StackState) -> String {
|
||||
match state {
|
||||
StackState::Running => String::from("Running ▶️"),
|
||||
StackState::Stopped => String::from("Stopped 🛑"),
|
||||
StackState::Restarting => String::from("Restarting 🔄"),
|
||||
StackState::Down => String::from("Down ⬇️"),
|
||||
_ => state.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt_level(level: SeverityLevel) -> &'static str {
|
||||
match level {
|
||||
SeverityLevel::Critical => "CRITICAL 🚨",
|
||||
SeverityLevel::Warning => "WARNING ‼️",
|
||||
SeverityLevel::Ok => "OK ✅",
|
||||
}
|
||||
}
|
||||
|
||||
fn resource_link(
|
||||
resource_type: ResourceTargetVariant,
|
||||
id: &str,
|
||||
) -> String {
|
||||
let path = match resource_type {
|
||||
ResourceTargetVariant::System => unreachable!(),
|
||||
ResourceTargetVariant::Build => format!("/builds/{id}"),
|
||||
ResourceTargetVariant::Builder => {
|
||||
format!("/builders/{id}")
|
||||
}
|
||||
ResourceTargetVariant::Deployment => {
|
||||
format!("/deployments/{id}")
|
||||
}
|
||||
ResourceTargetVariant::Stack => {
|
||||
format!("/stacks/{id}")
|
||||
}
|
||||
ResourceTargetVariant::Server => {
|
||||
format!("/servers/{id}")
|
||||
}
|
||||
ResourceTargetVariant::Repo => format!("/repos/{id}"),
|
||||
ResourceTargetVariant::Alerter => {
|
||||
format!("/alerters/{id}")
|
||||
}
|
||||
ResourceTargetVariant::Procedure => {
|
||||
format!("/procedures/{id}")
|
||||
}
|
||||
ResourceTargetVariant::ServerTemplate => {
|
||||
format!("/server-templates/{id}")
|
||||
}
|
||||
ResourceTargetVariant::ResourceSync => {
|
||||
format!("/resource-syncs/{id}")
|
||||
}
|
||||
};
|
||||
|
||||
format!("{}{path}", core_config().host)
|
||||
}
|
||||
@@ -1,130 +1,7 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use derive_variants::ExtractVariant;
|
||||
use futures::future::join_all;
|
||||
use komodo_client::entities::{
|
||||
alert::{Alert, AlertData, SeverityLevel},
|
||||
alerter::*,
|
||||
deployment::DeploymentState,
|
||||
stack::StackState,
|
||||
ResourceTargetVariant,
|
||||
};
|
||||
use mungos::{find::find_collect, mongodb::bson::doc};
|
||||
use slack::types::Block;
|
||||
|
||||
use crate::{config::core_config, state::db_client};
|
||||
|
||||
#[instrument]
|
||||
pub async fn send_alerts(alerts: &[Alert]) {
|
||||
if alerts.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Ok(alerters) = find_collect(
|
||||
&db_client().await.alerters,
|
||||
doc! { "config.enabled": true },
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
error!(
|
||||
"ERROR sending alerts | failed to get alerters from db | {e:#}"
|
||||
)
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let handles =
|
||||
alerts.iter().map(|alert| send_alert(&alerters, alert));
|
||||
|
||||
join_all(handles).await;
|
||||
}
|
||||
use super::*;
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
async fn send_alert(alerters: &[Alerter], alert: &Alert) {
|
||||
if alerters.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let alert_type = alert.data.extract_variant();
|
||||
|
||||
let handles = alerters.iter().map(|alerter| async {
|
||||
// Don't send if not enabled
|
||||
if !alerter.config.enabled {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Don't send if alert type not configured on the alerter
|
||||
if !alerter.config.alert_types.is_empty()
|
||||
&& !alerter.config.alert_types.contains(&alert_type)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Don't send if resource is in the blacklist
|
||||
if alerter.config.except_resources.contains(&alert.target) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Don't send if whitelist configured and target is not included
|
||||
if !alerter.config.resources.is_empty()
|
||||
&& !alerter.config.resources.contains(&alert.target)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match &alerter.config.endpoint {
|
||||
AlerterEndpoint::Slack(SlackAlerterEndpoint { url }) => {
|
||||
send_slack_alert(url, alert).await.with_context(|| {
|
||||
format!(
|
||||
"failed to send alert to slack alerter {}",
|
||||
alerter.name
|
||||
)
|
||||
})
|
||||
}
|
||||
AlerterEndpoint::Custom(CustomAlerterEndpoint { url }) => {
|
||||
send_custom_alert(url, alert).await.with_context(|| {
|
||||
format!(
|
||||
"failed to send alert to custom alerter {}",
|
||||
alerter.name
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
join_all(handles)
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|res| res.err())
|
||||
.for_each(|e| error!("{e:#}"));
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
async fn send_custom_alert(
|
||||
url: &str,
|
||||
alert: &Alert,
|
||||
) -> anyhow::Result<()> {
|
||||
let res = reqwest::Client::new()
|
||||
.post(url)
|
||||
.json(alert)
|
||||
.send()
|
||||
.await
|
||||
.context("failed at post request to alerter")?;
|
||||
let status = res.status();
|
||||
if !status.is_success() {
|
||||
let text = res
|
||||
.text()
|
||||
.await
|
||||
.context("failed to get response text on alerter response")?;
|
||||
return Err(anyhow!(
|
||||
"post to alerter failed | {status} | {text}"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
async fn send_slack_alert(
|
||||
pub async fn send_alert(
|
||||
url: &str,
|
||||
alert: &Alert,
|
||||
) -> anyhow::Result<()> {
|
||||
@@ -399,80 +276,8 @@ async fn send_slack_alert(
|
||||
AlertData::None {} => Default::default(),
|
||||
};
|
||||
if !text.is_empty() {
|
||||
let slack = slack::Client::new(url);
|
||||
let slack = ::slack::Client::new(url);
|
||||
slack.send_message(text, blocks).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fmt_region(region: &Option<String>) -> String {
|
||||
match region {
|
||||
Some(region) => format!(" ({region})"),
|
||||
None => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt_docker_container_state(state: &DeploymentState) -> String {
|
||||
match state {
|
||||
DeploymentState::Running => String::from("Running ▶️"),
|
||||
DeploymentState::Exited => String::from("Exited 🛑"),
|
||||
DeploymentState::Restarting => String::from("Restarting 🔄"),
|
||||
DeploymentState::NotDeployed => String::from("Not Deployed"),
|
||||
_ => state.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt_stack_state(state: &StackState) -> String {
|
||||
match state {
|
||||
StackState::Running => String::from("Running ▶️"),
|
||||
StackState::Stopped => String::from("Stopped 🛑"),
|
||||
StackState::Restarting => String::from("Restarting 🔄"),
|
||||
StackState::Down => String::from("Down ⬇️"),
|
||||
_ => state.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt_level(level: SeverityLevel) -> &'static str {
|
||||
match level {
|
||||
SeverityLevel::Critical => "CRITICAL 🚨",
|
||||
SeverityLevel::Warning => "WARNING ‼️",
|
||||
SeverityLevel::Ok => "OK ✅",
|
||||
}
|
||||
}
|
||||
|
||||
fn resource_link(
|
||||
resource_type: ResourceTargetVariant,
|
||||
id: &str,
|
||||
) -> String {
|
||||
let path = match resource_type {
|
||||
ResourceTargetVariant::System => unreachable!(),
|
||||
ResourceTargetVariant::Build => format!("/builds/{id}"),
|
||||
ResourceTargetVariant::Builder => {
|
||||
format!("/builders/{id}")
|
||||
}
|
||||
ResourceTargetVariant::Deployment => {
|
||||
format!("/deployments/{id}")
|
||||
}
|
||||
ResourceTargetVariant::Stack => {
|
||||
format!("/stacks/{id}")
|
||||
}
|
||||
ResourceTargetVariant::Server => {
|
||||
format!("/servers/{id}")
|
||||
}
|
||||
ResourceTargetVariant::Repo => format!("/repos/{id}"),
|
||||
ResourceTargetVariant::Alerter => {
|
||||
format!("/alerters/{id}")
|
||||
}
|
||||
ResourceTargetVariant::Procedure => {
|
||||
format!("/procedures/{id}")
|
||||
}
|
||||
ResourceTargetVariant::ServerTemplate => {
|
||||
format!("/server-templates/{id}")
|
||||
}
|
||||
ResourceTargetVariant::ResourceSync => {
|
||||
format!("/resource-syncs/{id}")
|
||||
}
|
||||
};
|
||||
|
||||
format!("{}{path}", core_config().host)
|
||||
}
|
||||
@@ -16,6 +16,7 @@ use crate::{
|
||||
get_user_id_from_headers,
|
||||
github::{self, client::github_oauth_client},
|
||||
google::{self, client::google_oauth_client},
|
||||
oidc,
|
||||
},
|
||||
config::core_config,
|
||||
helpers::query::get_user,
|
||||
@@ -39,14 +40,25 @@ pub enum AuthRequest {
|
||||
pub fn router() -> Router {
|
||||
let mut router = Router::new().route("/", post(handler));
|
||||
|
||||
if core_config().local_auth {
|
||||
info!("🔑 Local Login Enabled");
|
||||
}
|
||||
|
||||
if github_oauth_client().is_some() {
|
||||
info!("🔑 Github Login Enabled");
|
||||
router = router.nest("/github", github::router())
|
||||
}
|
||||
|
||||
if google_oauth_client().is_some() {
|
||||
info!("🔑 Github Login Enabled");
|
||||
router = router.nest("/google", google::router())
|
||||
}
|
||||
|
||||
if core_config().oidc_enabled {
|
||||
info!("🔑 OIDC Login Enabled");
|
||||
router = router.nest("/oidc", oidc::router())
|
||||
}
|
||||
|
||||
router
|
||||
}
|
||||
|
||||
@@ -91,6 +103,11 @@ fn login_options_reponse() -> &'static GetLoginOptionsResponse {
|
||||
google: config.google_oauth.enabled
|
||||
&& !config.google_oauth.id.is_empty()
|
||||
&& !config.google_oauth.secret.is_empty(),
|
||||
oidc: config.oidc_enabled
|
||||
&& !config.oidc_provider.is_empty()
|
||||
&& !config.oidc_client_id.is_empty()
|
||||
&& !config.oidc_client_secret.is_empty(),
|
||||
registration_disabled: config.disable_user_registration,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,13 +8,11 @@ use komodo_client::{
|
||||
entities::{
|
||||
alert::{Alert, AlertData, SeverityLevel},
|
||||
all_logs_success,
|
||||
build::{Build, ImageRegistry, StandardRegistryConfig},
|
||||
build::{Build, BuildConfig, ImageRegistryConfig},
|
||||
builder::{Builder, BuilderConfig},
|
||||
config::core::{AwsEcrConfig, AwsEcrConfigWithCredentials},
|
||||
deployment::DeploymentState,
|
||||
komodo_timestamp,
|
||||
permission::PermissionLevel,
|
||||
to_komodo_name,
|
||||
update::{Log, Update},
|
||||
user::{auto_redeploy_user, User},
|
||||
},
|
||||
@@ -32,17 +30,15 @@ use resolver_api::Resolve;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::{
|
||||
cloud::aws::ecr,
|
||||
config::core_config,
|
||||
alert::send_alerts,
|
||||
helpers::{
|
||||
alert::send_alerts,
|
||||
builder::{cleanup_builder_instance, get_builder_periphery},
|
||||
channel::build_cancel_channel,
|
||||
git_token,
|
||||
interpolate::{
|
||||
add_interp_update_log,
|
||||
interpolate_variables_secrets_into_environment,
|
||||
interpolate_variables_secrets_into_extra_args,
|
||||
interpolate_variables_secrets_into_string,
|
||||
interpolate_variables_secrets_into_system_command,
|
||||
},
|
||||
query::{get_deployment_state, get_variables_and_secrets},
|
||||
@@ -68,7 +64,7 @@ impl Resolve<RunBuild, (User, Update)> for State {
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.await?;
|
||||
let vars_and_secrets = get_variables_and_secrets().await?;
|
||||
let mut vars_and_secrets = get_variables_and_secrets().await?;
|
||||
|
||||
if build.config.builder_id.is_empty() {
|
||||
return Err(anyhow!("Must attach builder to RunBuild"));
|
||||
@@ -89,6 +85,14 @@ impl Resolve<RunBuild, (User, Update)> for State {
|
||||
update.version = build.config.version;
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
// Add the $VERSION to variables. Use with [[$VERSION]]
|
||||
if !vars_and_secrets.variables.contains_key("$VERSION") {
|
||||
vars_and_secrets.variables.insert(
|
||||
String::from("$VERSION"),
|
||||
build.config.version.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let git_token = git_token(
|
||||
&build.config.git_provider,
|
||||
&build.config.git_account,
|
||||
@@ -99,8 +103,8 @@ impl Resolve<RunBuild, (User, Update)> for State {
|
||||
|| format!("Failed to get git token in call to db. This is a database error, not a token exisitence error. Stopping run. | {} | {}", build.config.git_provider, build.config.git_account),
|
||||
)?;
|
||||
|
||||
let (registry_token, aws_ecr) =
|
||||
validate_account_extract_registry_token_aws_ecr(&build).await?;
|
||||
let registry_token =
|
||||
validate_account_extract_registry_token(&build).await?;
|
||||
|
||||
let cancel = CancellationToken::new();
|
||||
let cancel_clone = cancel.clone();
|
||||
@@ -175,7 +179,6 @@ impl Resolve<RunBuild, (User, Update)> for State {
|
||||
};
|
||||
|
||||
// CLONE REPO
|
||||
|
||||
let secret_replacers = if !build.config.skip_secret_interp {
|
||||
// Interpolate variables / secrets into pre build command
|
||||
let mut global_replacers = HashSet::new();
|
||||
@@ -245,14 +248,14 @@ impl Resolve<RunBuild, (User, Update)> for State {
|
||||
let mut global_replacers = HashSet::new();
|
||||
let mut secret_replacers = HashSet::new();
|
||||
|
||||
interpolate_variables_secrets_into_environment(
|
||||
interpolate_variables_secrets_into_string(
|
||||
&vars_and_secrets,
|
||||
&mut build.config.build_args,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
interpolate_variables_secrets_into_environment(
|
||||
interpolate_variables_secrets_into_string(
|
||||
&vars_and_secrets,
|
||||
&mut build.config.secret_args,
|
||||
&mut global_replacers,
|
||||
@@ -282,7 +285,6 @@ impl Resolve<RunBuild, (User, Update)> for State {
|
||||
.request(api::build::Build {
|
||||
build: build.clone(),
|
||||
registry_token,
|
||||
aws_ecr,
|
||||
replacers: secret_replacers.into_iter().collect(),
|
||||
// Push a commit hash tagged image
|
||||
additional_tags: if update.commit_hash.is_empty() {
|
||||
@@ -317,7 +319,7 @@ impl Resolve<RunBuild, (User, Update)> for State {
|
||||
|
||||
update.finalize();
|
||||
|
||||
let db = db_client().await;
|
||||
let db = db_client();
|
||||
|
||||
if update.success {
|
||||
let _ = db
|
||||
@@ -403,7 +405,7 @@ async fn handle_early_return(
|
||||
// but will fail to update cache in that case.
|
||||
if let Ok(update_doc) = to_document(&update) {
|
||||
let _ = update_one_by_id(
|
||||
&db_client().await.updates,
|
||||
&db_client().updates,
|
||||
&update.id,
|
||||
mungos::update::Update::Set(update_doc),
|
||||
None,
|
||||
@@ -443,7 +445,7 @@ pub async fn validate_cancel_build(
|
||||
if let ExecuteRequest::CancelBuild(req) = request {
|
||||
let build = resource::get::<Build>(&req.build).await?;
|
||||
|
||||
let db = db_client().await;
|
||||
let db = db_client();
|
||||
|
||||
let (latest_build, latest_cancel) = tokio::try_join!(
|
||||
db.updates
|
||||
@@ -526,7 +528,7 @@ impl Resolve<CancelBuild, (User, Update)> for State {
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_secs(60)).await;
|
||||
if let Err(e) = update_one_by_id(
|
||||
&db_client().await.updates,
|
||||
&db_client().updates,
|
||||
&update_id,
|
||||
doc! { "$set": { "status": "Complete" } },
|
||||
None,
|
||||
@@ -544,7 +546,7 @@ impl Resolve<CancelBuild, (User, Update)> for State {
|
||||
#[instrument]
|
||||
async fn handle_post_build_redeploy(build_id: &str) {
|
||||
let Ok(redeploy_deployments) = find_collect(
|
||||
&db_client().await.deployments,
|
||||
&db_client().deployments,
|
||||
doc! {
|
||||
"config.image.params.build_id": build_id,
|
||||
"config.redeploy_on_build": true
|
||||
@@ -600,56 +602,24 @@ async fn handle_post_build_redeploy(build_id: &str) {
|
||||
}
|
||||
|
||||
/// This will make sure that a build with non-none image registry has an account attached,
|
||||
/// and will check the core config for a token / aws ecr config matching requirements.
|
||||
/// and will check the core config for a token matching requirements.
|
||||
/// Otherwise it is left to periphery.
|
||||
async fn validate_account_extract_registry_token_aws_ecr(
|
||||
build: &Build,
|
||||
) -> anyhow::Result<(Option<String>, Option<AwsEcrConfig>)> {
|
||||
let (domain, account) = match &build.config.image_registry {
|
||||
// Early return for None
|
||||
ImageRegistry::None(_) => return Ok((None, None)),
|
||||
// Early return for AwsEcr
|
||||
ImageRegistry::AwsEcr(label) => {
|
||||
// Note that aws ecr config still only lives in config file
|
||||
let config = core_config()
|
||||
.aws_ecr_registries
|
||||
.iter()
|
||||
.find(|reg| ®.label == label);
|
||||
let token = match config {
|
||||
Some(AwsEcrConfigWithCredentials {
|
||||
region,
|
||||
access_key_id,
|
||||
secret_access_key,
|
||||
..
|
||||
}) => {
|
||||
let token = ecr::get_ecr_token(
|
||||
region,
|
||||
access_key_id,
|
||||
secret_access_key,
|
||||
)
|
||||
.await
|
||||
.context("failed to get aws ecr token")?;
|
||||
ecr::maybe_create_repo(
|
||||
&to_komodo_name(&build.name),
|
||||
region.to_string(),
|
||||
access_key_id,
|
||||
secret_access_key,
|
||||
)
|
||||
.await
|
||||
.context("failed to create aws ecr repo")?;
|
||||
Some(token)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
return Ok((token, config.map(AwsEcrConfig::from)));
|
||||
}
|
||||
ImageRegistry::Standard(StandardRegistryConfig {
|
||||
domain,
|
||||
account,
|
||||
..
|
||||
}) => (domain.as_str(), account),
|
||||
};
|
||||
|
||||
async fn validate_account_extract_registry_token(
|
||||
Build {
|
||||
config:
|
||||
BuildConfig {
|
||||
image_registry:
|
||||
ImageRegistryConfig {
|
||||
domain, account, ..
|
||||
},
|
||||
..
|
||||
},
|
||||
..
|
||||
}: &Build,
|
||||
) -> anyhow::Result<Option<String>> {
|
||||
if domain.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
if account.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"Must attach account to use registry provider {domain}"
|
||||
@@ -660,5 +630,5 @@ async fn validate_account_extract_registry_token_aws_ecr(
|
||||
|| format!("Failed to get registry token in call to db. Stopping run. | {domain} | {account}"),
|
||||
)?;
|
||||
|
||||
Ok((registry_token, None))
|
||||
Ok(registry_token)
|
||||
}
|
||||
|
||||
@@ -5,8 +5,7 @@ use formatting::format_serror;
|
||||
use komodo_client::{
|
||||
api::execute::*,
|
||||
entities::{
|
||||
build::{Build, ImageRegistry},
|
||||
config::core::AwsEcrConfig,
|
||||
build::{Build, ImageRegistryConfig},
|
||||
deployment::{
|
||||
extract_registry_domain, Deployment, DeploymentImage,
|
||||
},
|
||||
@@ -22,14 +21,11 @@ use periphery_client::api;
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
cloud::aws::ecr,
|
||||
config::core_config,
|
||||
helpers::{
|
||||
interpolate::{
|
||||
add_interp_update_log,
|
||||
interpolate_variables_secrets_into_container_command,
|
||||
interpolate_variables_secrets_into_environment,
|
||||
interpolate_variables_secrets_into_extra_args,
|
||||
interpolate_variables_secrets_into_string,
|
||||
},
|
||||
periphery_client,
|
||||
query::get_variables_and_secrets,
|
||||
@@ -98,20 +94,11 @@ impl Resolve<Deploy, (User, Update)> for State {
|
||||
.context("Failed server health check, stopping run.")?;
|
||||
|
||||
// This block resolves the attached Build to an actual versioned image
|
||||
let (version, registry_token, aws_ecr) = match &deployment
|
||||
.config
|
||||
.image
|
||||
{
|
||||
let (version, registry_token) = match &deployment.config.image {
|
||||
DeploymentImage::Build { build_id, version } => {
|
||||
let build = resource::get::<Build>(build_id).await?;
|
||||
let image_name = get_image_name(&build, |label| {
|
||||
core_config()
|
||||
.aws_ecr_registries
|
||||
.iter()
|
||||
.find(|reg| ®.label == label)
|
||||
.map(AwsEcrConfig::from)
|
||||
})
|
||||
.context("failed to create image name")?;
|
||||
let image_name = get_image_name(&build)
|
||||
.context("failed to create image name")?;
|
||||
let version = if version.is_none() {
|
||||
build.config.version
|
||||
} else {
|
||||
@@ -133,45 +120,27 @@ impl Resolve<Deploy, (User, Update)> for State {
|
||||
deployment.config.image = DeploymentImage::Image {
|
||||
image: format!("{image_name}:{version_str}"),
|
||||
};
|
||||
match build.config.image_registry {
|
||||
ImageRegistry::None(_) => (version, None, None),
|
||||
ImageRegistry::AwsEcr(label) => {
|
||||
let config = core_config()
|
||||
.aws_ecr_registries
|
||||
.iter()
|
||||
.find(|reg| reg.label == label)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"did not find config for aws ecr registry {label}"
|
||||
)
|
||||
})?;
|
||||
let token = ecr::get_ecr_token(
|
||||
&config.region,
|
||||
&config.access_key_id,
|
||||
&config.secret_access_key,
|
||||
)
|
||||
.await
|
||||
.context("failed to create aws ecr login token")?;
|
||||
(version, Some(token), Some(AwsEcrConfig::from(config)))
|
||||
}
|
||||
ImageRegistry::Standard(params) => {
|
||||
if deployment.config.image_registry_account.is_empty() {
|
||||
deployment.config.image_registry_account =
|
||||
params.account
|
||||
}
|
||||
let token = if !deployment
|
||||
.config
|
||||
.image_registry_account
|
||||
.is_empty()
|
||||
{
|
||||
registry_token(¶ms.domain, &deployment.config.image_registry_account).await.with_context(
|
||||
|| format!("Failed to get git token in call to db. Stopping run. | {} | {}", params.domain, deployment.config.image_registry_account),
|
||||
)?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(version, token, None)
|
||||
if build.config.image_registry.domain.is_empty() {
|
||||
(version, None)
|
||||
} else {
|
||||
let ImageRegistryConfig {
|
||||
domain, account, ..
|
||||
} = build.config.image_registry;
|
||||
if deployment.config.image_registry_account.is_empty() {
|
||||
deployment.config.image_registry_account = account
|
||||
}
|
||||
let token = if !deployment
|
||||
.config
|
||||
.image_registry_account
|
||||
.is_empty()
|
||||
{
|
||||
registry_token(&domain, &deployment.config.image_registry_account).await.with_context(
|
||||
|| format!("Failed to get git token in call to db. Stopping run. | {domain} | {}", deployment.config.image_registry_account),
|
||||
)?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(version, token)
|
||||
}
|
||||
}
|
||||
DeploymentImage::Image { image } => {
|
||||
@@ -187,7 +156,7 @@ impl Resolve<Deploy, (User, Update)> for State {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(Version::default(), token, None)
|
||||
(Version::default(), token)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -199,13 +168,27 @@ impl Resolve<Deploy, (User, Update)> for State {
|
||||
let mut global_replacers = HashSet::new();
|
||||
let mut secret_replacers = HashSet::new();
|
||||
|
||||
interpolate_variables_secrets_into_environment(
|
||||
interpolate_variables_secrets_into_string(
|
||||
&vars_and_secrets,
|
||||
&mut deployment.config.environment,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
interpolate_variables_secrets_into_string(
|
||||
&vars_and_secrets,
|
||||
&mut deployment.config.ports,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
interpolate_variables_secrets_into_string(
|
||||
&vars_and_secrets,
|
||||
&mut deployment.config.volumes,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
interpolate_variables_secrets_into_extra_args(
|
||||
&vars_and_secrets,
|
||||
&mut deployment.config.extra_args,
|
||||
@@ -213,7 +196,7 @@ impl Resolve<Deploy, (User, Update)> for State {
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
interpolate_variables_secrets_into_container_command(
|
||||
interpolate_variables_secrets_into_string(
|
||||
&vars_and_secrets,
|
||||
&mut deployment.config.command,
|
||||
&mut global_replacers,
|
||||
@@ -240,7 +223,6 @@ impl Resolve<Deploy, (User, Update)> for State {
|
||||
stop_signal,
|
||||
stop_time,
|
||||
registry_token,
|
||||
aws_ecr,
|
||||
replacers: secret_replacers.into_iter().collect(),
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::time::Instant;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use axum::{middleware, routing::post, Extension, Router};
|
||||
use derive_variants::{EnumVariants, ExtractVariant};
|
||||
use formatting::format_serror;
|
||||
use komodo_client::{
|
||||
api::execute::*,
|
||||
@@ -33,7 +34,10 @@ mod stack;
|
||||
mod sync;
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Resolver)]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Resolver, EnumVariants,
|
||||
)]
|
||||
#[variant_derive(Debug)]
|
||||
#[resolver_target(State)]
|
||||
#[resolver_args((User, Update))]
|
||||
#[serde(tag = "type", content = "params")]
|
||||
@@ -57,6 +61,8 @@ pub enum ExecuteRequest {
|
||||
PruneImages(PruneImages),
|
||||
DeleteVolume(DeleteVolume),
|
||||
PruneVolumes(PruneVolumes),
|
||||
PruneDockerBuilders(PruneDockerBuilders),
|
||||
PruneBuildx(PruneBuildx),
|
||||
PruneSystem(PruneSystem),
|
||||
|
||||
// ==== DEPLOYMENT ====
|
||||
@@ -133,7 +139,7 @@ async fn handler(
|
||||
};
|
||||
let res = async {
|
||||
let mut update =
|
||||
find_one_by_id(&db_client().await.updates, &update_id)
|
||||
find_one_by_id(&db_client().updates, &update_id)
|
||||
.await
|
||||
.context("failed to query to db")?
|
||||
.context("no update exists with given id")?;
|
||||
@@ -152,7 +158,15 @@ async fn handler(
|
||||
Ok(Json(update))
|
||||
}
|
||||
|
||||
#[instrument(name = "ExecuteRequest", skip(user, update), fields(user_id = user.id, update_id = update.id))]
|
||||
#[instrument(
|
||||
name = "ExecuteRequest",
|
||||
skip(user, update),
|
||||
fields(
|
||||
user_id = user.id,
|
||||
update_id = update.id,
|
||||
request = format!("{:?}", request.extract_variant()))
|
||||
)
|
||||
]
|
||||
async fn task(
|
||||
req_id: Uuid,
|
||||
request: ExecuteRequest,
|
||||
|
||||
@@ -50,7 +50,7 @@ fn resolve_inner(
|
||||
// assumes first log is already created
|
||||
// and will panic otherwise.
|
||||
update.push_simple_log(
|
||||
"execute_procedure",
|
||||
"Execute procedure",
|
||||
format!(
|
||||
"{}: executing procedure '{}'",
|
||||
muted("INFO"),
|
||||
@@ -80,9 +80,9 @@ fn resolve_inner(
|
||||
match res {
|
||||
Ok(_) => {
|
||||
update.push_simple_log(
|
||||
"execution ok",
|
||||
"Execution ok",
|
||||
format!(
|
||||
"{}: the procedure has {} with no errors",
|
||||
"{}: The procedure has {} with no errors",
|
||||
muted("INFO"),
|
||||
colored("completed", Color::Green)
|
||||
),
|
||||
@@ -100,7 +100,7 @@ fn resolve_inner(
|
||||
// but will fail to update cache in that case.
|
||||
if let Ok(update_doc) = to_document(&update) {
|
||||
let _ = update_one_by_id(
|
||||
&db_client().await.updates,
|
||||
&db_client().updates,
|
||||
&update.id,
|
||||
mungos::update::Update::Set(update_doc),
|
||||
None,
|
||||
|
||||
@@ -7,7 +7,7 @@ use komodo_client::{
|
||||
entities::{
|
||||
alert::{Alert, AlertData, SeverityLevel},
|
||||
builder::{Builder, BuilderConfig},
|
||||
komodo_timestamp, optional_string,
|
||||
komodo_timestamp,
|
||||
permission::PermissionLevel,
|
||||
repo::Repo,
|
||||
server::Server,
|
||||
@@ -27,14 +27,14 @@ use resolver_api::Resolve;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::{
|
||||
alert::send_alerts,
|
||||
helpers::{
|
||||
alert::send_alerts,
|
||||
builder::{cleanup_builder_instance, get_builder_periphery},
|
||||
channel::repo_cancel_channel,
|
||||
git_token,
|
||||
interpolate::{
|
||||
add_interp_update_log,
|
||||
interpolate_variables_secrets_into_environment,
|
||||
interpolate_variables_secrets_into_string,
|
||||
interpolate_variables_secrets_into_system_command,
|
||||
},
|
||||
periphery_client,
|
||||
@@ -72,6 +72,10 @@ impl Resolve<CloneRepo, (User, Update)> for State {
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
if repo.config.server_id.is_empty() {
|
||||
return Err(anyhow!("repo has no server attached"));
|
||||
}
|
||||
|
||||
let git_token = git_token(
|
||||
&repo.config.git_provider,
|
||||
&repo.config.git_account,
|
||||
@@ -82,10 +86,6 @@ impl Resolve<CloneRepo, (User, Update)> for State {
|
||||
|| format!("Failed to get git token in call to db. This is a database error, not a token exisitence error. Stopping run. | {} | {}", repo.config.git_provider, repo.config.git_account),
|
||||
)?;
|
||||
|
||||
if repo.config.server_id.is_empty() {
|
||||
return Err(anyhow!("repo has no server attached"));
|
||||
}
|
||||
|
||||
let server =
|
||||
resource::get::<Server>(&repo.config.server_id).await?;
|
||||
|
||||
@@ -100,7 +100,7 @@ impl Resolve<CloneRepo, (User, Update)> for State {
|
||||
.request(api::git::CloneRepo {
|
||||
args: (&repo).into(),
|
||||
git_token,
|
||||
environment: repo.config.environment,
|
||||
environment: repo.config.env_vars()?,
|
||||
env_file_path: repo.config.env_file_path,
|
||||
skip_secret_interp: repo.config.skip_secret_interp,
|
||||
replacers: secret_replacers.into_iter().collect(),
|
||||
@@ -156,6 +156,16 @@ impl Resolve<PullRepo, (User, Update)> for State {
|
||||
return Err(anyhow!("repo has no server attached"));
|
||||
}
|
||||
|
||||
let git_token = git_token(
|
||||
&repo.config.git_provider,
|
||||
&repo.config.git_account,
|
||||
|https| repo.config.git_https = https,
|
||||
)
|
||||
.await
|
||||
.with_context(
|
||||
|| format!("Failed to get git token in call to db. This is a database error, not a token exisitence error. Stopping run. | {} | {}", repo.config.git_provider, repo.config.git_account),
|
||||
)?;
|
||||
|
||||
let server =
|
||||
resource::get::<Server>(&repo.config.server_id).await?;
|
||||
|
||||
@@ -168,11 +178,9 @@ impl Resolve<PullRepo, (User, Update)> for State {
|
||||
|
||||
let logs = match periphery
|
||||
.request(api::git::PullRepo {
|
||||
name: repo.name.clone(),
|
||||
branch: optional_string(&repo.config.branch),
|
||||
commit: optional_string(&repo.config.commit),
|
||||
on_pull: repo.config.on_pull.into_option(),
|
||||
environment: repo.config.environment,
|
||||
args: (&repo).into(),
|
||||
git_token,
|
||||
environment: repo.config.env_vars()?,
|
||||
env_file_path: repo.config.env_file_path,
|
||||
skip_secret_interp: repo.config.skip_secret_interp,
|
||||
replacers: secret_replacers.into_iter().collect(),
|
||||
@@ -213,7 +221,7 @@ async fn handle_server_update_return(
|
||||
// but will fail to update cache in that case.
|
||||
if let Ok(update_doc) = to_document(&update) {
|
||||
let _ = update_one_by_id(
|
||||
&db_client().await.updates,
|
||||
&db_client().updates,
|
||||
&update.id,
|
||||
mungos::update::Update::Set(update_doc),
|
||||
None,
|
||||
@@ -228,7 +236,6 @@ async fn handle_server_update_return(
|
||||
#[instrument]
|
||||
async fn update_last_pulled_time(repo_name: &str) {
|
||||
let res = db_client()
|
||||
.await
|
||||
.repos
|
||||
.update_one(
|
||||
doc! { "name": repo_name },
|
||||
@@ -362,7 +369,7 @@ impl Resolve<BuildRepo, (User, Update)> for State {
|
||||
.request(api::git::CloneRepo {
|
||||
args: (&repo).into(),
|
||||
git_token,
|
||||
environment: repo.config.environment,
|
||||
environment: repo.config.env_vars()?,
|
||||
env_file_path: repo.config.env_file_path,
|
||||
skip_secret_interp: repo.config.skip_secret_interp,
|
||||
replacers: secret_replacers.into_iter().collect()
|
||||
@@ -395,7 +402,7 @@ impl Resolve<BuildRepo, (User, Update)> for State {
|
||||
|
||||
update.finalize();
|
||||
|
||||
let db = db_client().await;
|
||||
let db = db_client();
|
||||
|
||||
if update.success {
|
||||
let _ = db
|
||||
@@ -472,7 +479,7 @@ async fn handle_builder_early_return(
|
||||
// but will fail to update cache in that case.
|
||||
if let Ok(update_doc) = to_document(&update) {
|
||||
let _ = update_one_by_id(
|
||||
&db_client().await.updates,
|
||||
&db_client().updates,
|
||||
&update.id,
|
||||
mungos::update::Update::Set(update_doc),
|
||||
None,
|
||||
@@ -510,7 +517,7 @@ pub async fn validate_cancel_repo_build(
|
||||
if let ExecuteRequest::CancelRepoBuild(req) = request {
|
||||
let repo = resource::get::<Repo>(&req.repo).await?;
|
||||
|
||||
let db = db_client().await;
|
||||
let db = db_client();
|
||||
|
||||
let (latest_build, latest_cancel) = tokio::try_join!(
|
||||
db.updates
|
||||
@@ -595,7 +602,7 @@ impl Resolve<CancelRepoBuild, (User, Update)> for State {
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_secs(60)).await;
|
||||
if let Err(e) = update_one_by_id(
|
||||
&db_client().await.updates,
|
||||
&db_client().updates,
|
||||
&update_id,
|
||||
doc! { "$set": { "status": "Complete" } },
|
||||
None,
|
||||
@@ -620,7 +627,7 @@ async fn interpolate(
|
||||
let mut global_replacers = HashSet::new();
|
||||
let mut secret_replacers = HashSet::new();
|
||||
|
||||
interpolate_variables_secrets_into_environment(
|
||||
interpolate_variables_secrets_into_string(
|
||||
&vars_and_secrets,
|
||||
&mut repo.config.environment,
|
||||
&mut global_replacers,
|
||||
|
||||
@@ -941,6 +941,108 @@ impl Resolve<PruneVolumes, (User, Update)> for State {
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<PruneDockerBuilders, (User, Update)> for State {
|
||||
#[instrument(name = "PruneDockerBuilders", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
PruneDockerBuilders { server }: PruneDockerBuilders,
|
||||
(user, mut update): (User, Update),
|
||||
) -> anyhow::Result<Update> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
&server,
|
||||
&user,
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// get the action state for the server (or insert default).
|
||||
let action_state = action_states()
|
||||
.server
|
||||
.get_or_insert_default(&server.id)
|
||||
.await;
|
||||
|
||||
// Will check to ensure server not already busy before updating, and return Err if so.
|
||||
// The returned guard will set the action state back to default when dropped.
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.pruning_builders = true)?;
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
let log =
|
||||
match periphery.request(api::build::PruneBuilders {}).await {
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"prune builders",
|
||||
format!(
|
||||
"failed to docker builder prune on server {} | {e:#?}",
|
||||
server.name
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
update.logs.push(log);
|
||||
update_cache_for_server(&server).await;
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<PruneBuildx, (User, Update)> for State {
|
||||
#[instrument(name = "PruneBuildx", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
PruneBuildx { server }: PruneBuildx,
|
||||
(user, mut update): (User, Update),
|
||||
) -> anyhow::Result<Update> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
&server,
|
||||
&user,
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// get the action state for the server (or insert default).
|
||||
let action_state = action_states()
|
||||
.server
|
||||
.get_or_insert_default(&server.id)
|
||||
.await;
|
||||
|
||||
// Will check to ensure server not already busy before updating, and return Err if so.
|
||||
// The returned guard will set the action state back to default when dropped.
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.pruning_buildx = true)?;
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
let log =
|
||||
match periphery.request(api::build::PruneBuildx {}).await {
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"prune buildx",
|
||||
format!(
|
||||
"failed to docker buildx prune on server {} | {e:#?}",
|
||||
server.name
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
update.logs.push(log);
|
||||
update_cache_for_server(&server).await;
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<PruneSystem, (User, Update)> for State {
|
||||
#[instrument(name = "PruneSystem", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
|
||||
async fn resolve(
|
||||
@@ -975,7 +1077,7 @@ impl Resolve<PruneSystem, (User, Update)> for State {
|
||||
Err(e) => Log::error(
|
||||
"prune system",
|
||||
format!(
|
||||
"failed to docket system prune on server {} | {e:#?}",
|
||||
"failed to docker system prune on server {} | {e:#?}",
|
||||
server.name
|
||||
),
|
||||
),
|
||||
|
||||
@@ -34,7 +34,6 @@ impl Resolve<LaunchServer, (User, Update)> for State {
|
||||
) -> anyhow::Result<Update> {
|
||||
// validate name isn't already taken by another server
|
||||
if db_client()
|
||||
.await
|
||||
.servers
|
||||
.find_one(doc! {
|
||||
"name": &name
|
||||
@@ -62,6 +61,8 @@ impl Resolve<LaunchServer, (User, Update)> for State {
|
||||
let config = match template.config {
|
||||
ServerTemplateConfig::Aws(config) => {
|
||||
let region = config.region.clone();
|
||||
let use_https = config.use_https;
|
||||
let port = config.port;
|
||||
let instance = match launch_ec2_instance(&name, config).await
|
||||
{
|
||||
Ok(instance) => instance,
|
||||
@@ -82,14 +83,18 @@ impl Resolve<LaunchServer, (User, Update)> for State {
|
||||
instance.ip
|
||||
),
|
||||
);
|
||||
let protocol = if use_https { "https" } else { "http" };
|
||||
PartialServerConfig {
|
||||
address: format!("http://{}:8120", instance.ip).into(),
|
||||
address: format!("{protocol}://{}:{port}", instance.ip)
|
||||
.into(),
|
||||
region: region.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
ServerTemplateConfig::Hetzner(config) => {
|
||||
let datacenter = config.datacenter;
|
||||
let use_https = config.use_https;
|
||||
let port = config.port;
|
||||
let server = match launch_hetzner_server(&name, config).await
|
||||
{
|
||||
Ok(server) => server,
|
||||
@@ -110,8 +115,10 @@ impl Resolve<LaunchServer, (User, Update)> for State {
|
||||
server.ip
|
||||
),
|
||||
);
|
||||
let protocol = if use_https { "https" } else { "http" };
|
||||
PartialServerConfig {
|
||||
address: format!("http://{}:8120", server.ip).into(),
|
||||
address: format!("{protocol}://{}:{port}", server.ip)
|
||||
.into(),
|
||||
region: datacenter.as_ref().to_string().into(),
|
||||
..Default::default()
|
||||
}
|
||||
|
||||
@@ -17,18 +17,19 @@ use crate::{
|
||||
helpers::{
|
||||
interpolate::{
|
||||
add_interp_update_log,
|
||||
interpolate_variables_secrets_into_environment,
|
||||
interpolate_variables_secrets_into_extra_args,
|
||||
interpolate_variables_secrets_into_string,
|
||||
interpolate_variables_secrets_into_system_command,
|
||||
},
|
||||
periphery_client,
|
||||
query::get_variables_and_secrets,
|
||||
stack::{
|
||||
execute::execute_compose, get_stack_and_server,
|
||||
services::extract_services_into_res,
|
||||
},
|
||||
update::update_update,
|
||||
},
|
||||
monitor::update_cache_for_server,
|
||||
stack::{
|
||||
execute::execute_compose, get_stack_and_server,
|
||||
services::extract_services_into_res,
|
||||
},
|
||||
state::{action_states, db_client, State},
|
||||
};
|
||||
|
||||
@@ -81,7 +82,7 @@ impl Resolve<DeployStack, (User, Update)> for State {
|
||||
let mut global_replacers = HashSet::new();
|
||||
let mut secret_replacers = HashSet::new();
|
||||
|
||||
interpolate_variables_secrets_into_environment(
|
||||
interpolate_variables_secrets_into_string(
|
||||
&vars_and_secrets,
|
||||
&mut stack.config.environment,
|
||||
&mut global_replacers,
|
||||
@@ -95,6 +96,20 @@ impl Resolve<DeployStack, (User, Update)> for State {
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
interpolate_variables_secrets_into_extra_args(
|
||||
&vars_and_secrets,
|
||||
&mut stack.config.build_extra_args,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
interpolate_variables_secrets_into_system_command(
|
||||
&vars_and_secrets,
|
||||
&mut stack.config.pre_deploy,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
add_interp_update_log(
|
||||
&mut update,
|
||||
&global_replacers,
|
||||
@@ -147,6 +162,8 @@ impl Resolve<DeployStack, (User, Update)> for State {
|
||||
stack.info.latest_services.clone()
|
||||
};
|
||||
|
||||
// This ensures to get the latest project name,
|
||||
// as it may have changed since the last deploy.
|
||||
let project_name = stack.project_name(true);
|
||||
|
||||
let (
|
||||
@@ -196,7 +213,6 @@ impl Resolve<DeployStack, (User, Update)> for State {
|
||||
.context("failed to serialize stack info to bson")?;
|
||||
|
||||
db_client()
|
||||
.await
|
||||
.stacks
|
||||
.update_one(
|
||||
doc! { "name": &stack.name },
|
||||
|
||||
@@ -2,7 +2,6 @@ use std::collections::HashMap;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use formatting::{colored, format_serror, Color};
|
||||
use mongo_indexed::doc;
|
||||
use komodo_client::{
|
||||
api::{execute::RunSync, write::RefreshResourceSyncPending},
|
||||
entities::{
|
||||
@@ -18,28 +17,27 @@ use komodo_client::{
|
||||
server::Server,
|
||||
server_template::ServerTemplate,
|
||||
stack::Stack,
|
||||
sync::ResourceSync,
|
||||
update::{Log, Update},
|
||||
user::{sync_user, User},
|
||||
},
|
||||
};
|
||||
use mongo_indexed::doc;
|
||||
use mungos::{by_id::update_one_by_id, mongodb::bson::to_document};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
query::get_id_to_tags,
|
||||
sync::{
|
||||
deploy::{
|
||||
build_deploy_cache, deploy_from_cache, SyncDeployParams,
|
||||
},
|
||||
resource::{
|
||||
get_updates_for_execution, AllResourcesById, ResourceSync,
|
||||
},
|
||||
},
|
||||
update::update_update,
|
||||
},
|
||||
helpers::{query::get_id_to_tags, update::update_update},
|
||||
resource::{self, refresh_resource_sync_state_cache},
|
||||
state::{db_client, State},
|
||||
sync::{
|
||||
deploy::{
|
||||
build_deploy_cache, deploy_from_cache, SyncDeployParams,
|
||||
},
|
||||
execute::{get_updates_for_execution, ExecuteResourceSync},
|
||||
remote::RemoteResources,
|
||||
AllResourcesById,
|
||||
},
|
||||
};
|
||||
|
||||
impl Resolve<RunSync, (User, Update)> for State {
|
||||
@@ -54,22 +52,28 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
>(&sync, &user, PermissionLevel::Execute)
|
||||
.await?;
|
||||
|
||||
if sync.config.repo.is_empty() {
|
||||
return Err(anyhow!("resource sync repo not configured"));
|
||||
}
|
||||
|
||||
// Send update here for FE to recheck action state
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let (res, logs, hash, message) =
|
||||
crate::helpers::sync::remote::get_remote_resources(&sync)
|
||||
.await
|
||||
.context("failed to get remote resources")?;
|
||||
let RemoteResources {
|
||||
resources,
|
||||
logs,
|
||||
hash,
|
||||
message,
|
||||
file_errors,
|
||||
..
|
||||
} = crate::sync::remote::get_remote_resources(&sync)
|
||||
.await
|
||||
.context("failed to get remote resources")?;
|
||||
|
||||
update.logs.extend(logs);
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let resources = res?;
|
||||
if !file_errors.is_empty() {
|
||||
return Err(anyhow!("Found file errors. Cannot execute sync."))
|
||||
}
|
||||
|
||||
let resources = resources?;
|
||||
|
||||
let id_to_tags = get_id_to_tags(None).await?;
|
||||
let all_resources = AllResourcesById::load().await?;
|
||||
@@ -94,12 +98,15 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
})
|
||||
.await?;
|
||||
|
||||
let delete = sync.config.managed || sync.config.delete;
|
||||
|
||||
let (servers_to_create, servers_to_update, servers_to_delete) =
|
||||
get_updates_for_execution::<Server>(
|
||||
resources.servers,
|
||||
sync.config.delete,
|
||||
delete,
|
||||
&all_resources,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
.await?;
|
||||
let (
|
||||
@@ -108,33 +115,37 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
deployments_to_delete,
|
||||
) = get_updates_for_execution::<Deployment>(
|
||||
resources.deployments,
|
||||
sync.config.delete,
|
||||
delete,
|
||||
&all_resources,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
.await?;
|
||||
let (stacks_to_create, stacks_to_update, stacks_to_delete) =
|
||||
get_updates_for_execution::<Stack>(
|
||||
resources.stacks,
|
||||
sync.config.delete,
|
||||
delete,
|
||||
&all_resources,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
.await?;
|
||||
let (builds_to_create, builds_to_update, builds_to_delete) =
|
||||
get_updates_for_execution::<Build>(
|
||||
resources.builds,
|
||||
sync.config.delete,
|
||||
delete,
|
||||
&all_resources,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
.await?;
|
||||
let (repos_to_create, repos_to_update, repos_to_delete) =
|
||||
get_updates_for_execution::<Repo>(
|
||||
resources.repos,
|
||||
sync.config.delete,
|
||||
delete,
|
||||
&all_resources,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
.await?;
|
||||
let (
|
||||
@@ -143,25 +154,28 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
procedures_to_delete,
|
||||
) = get_updates_for_execution::<Procedure>(
|
||||
resources.procedures,
|
||||
sync.config.delete,
|
||||
delete,
|
||||
&all_resources,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
.await?;
|
||||
let (builders_to_create, builders_to_update, builders_to_delete) =
|
||||
get_updates_for_execution::<Builder>(
|
||||
resources.builders,
|
||||
sync.config.delete,
|
||||
delete,
|
||||
&all_resources,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
.await?;
|
||||
let (alerters_to_create, alerters_to_update, alerters_to_delete) =
|
||||
get_updates_for_execution::<Alerter>(
|
||||
resources.alerters,
|
||||
sync.config.delete,
|
||||
delete,
|
||||
&all_resources,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
.await?;
|
||||
let (
|
||||
@@ -170,9 +184,10 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
server_templates_to_delete,
|
||||
) = get_updates_for_execution::<ServerTemplate>(
|
||||
resources.server_templates,
|
||||
sync.config.delete,
|
||||
delete,
|
||||
&all_resources,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
.await?;
|
||||
let (
|
||||
@@ -181,27 +196,30 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
resource_syncs_to_delete,
|
||||
) = get_updates_for_execution::<entities::sync::ResourceSync>(
|
||||
resources.resource_syncs,
|
||||
sync.config.delete,
|
||||
delete,
|
||||
&all_resources,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
.await?;
|
||||
let (
|
||||
variables_to_create,
|
||||
variables_to_update,
|
||||
variables_to_delete,
|
||||
) = crate::helpers::sync::variables::get_updates_for_execution(
|
||||
) = crate::sync::variables::get_updates_for_execution(
|
||||
resources.variables,
|
||||
sync.config.delete,
|
||||
// Delete doesn't work with variables when match tags are set
|
||||
sync.config.match_tags.is_empty() && delete,
|
||||
)
|
||||
.await?;
|
||||
let (
|
||||
user_groups_to_create,
|
||||
user_groups_to_update,
|
||||
user_groups_to_delete,
|
||||
) = crate::helpers::sync::user_groups::get_updates_for_execution(
|
||||
) = crate::sync::user_groups::get_updates_for_execution(
|
||||
resources.user_groups,
|
||||
sync.config.delete,
|
||||
// Delete doesn't work with user groups when match tags are set
|
||||
sync.config.match_tags.is_empty() && delete,
|
||||
&all_resources,
|
||||
)
|
||||
.await?;
|
||||
@@ -261,7 +279,7 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
// No deps
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
crate::helpers::sync::variables::run_updates(
|
||||
crate::sync::variables::run_updates(
|
||||
variables_to_create,
|
||||
variables_to_update,
|
||||
variables_to_delete,
|
||||
@@ -270,7 +288,7 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
);
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
crate::helpers::sync::user_groups::run_updates(
|
||||
crate::sync::user_groups::run_updates(
|
||||
user_groups_to_create,
|
||||
user_groups_to_update,
|
||||
user_groups_to_delete,
|
||||
@@ -279,7 +297,7 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
);
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
entities::sync::ResourceSync::run_updates(
|
||||
ResourceSync::execute_sync_updates(
|
||||
resource_syncs_to_create,
|
||||
resource_syncs_to_update,
|
||||
resource_syncs_to_delete,
|
||||
@@ -288,7 +306,7 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
);
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
ServerTemplate::run_updates(
|
||||
ServerTemplate::execute_sync_updates(
|
||||
server_templates_to_create,
|
||||
server_templates_to_update,
|
||||
server_templates_to_delete,
|
||||
@@ -297,7 +315,7 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
);
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
Server::run_updates(
|
||||
Server::execute_sync_updates(
|
||||
servers_to_create,
|
||||
servers_to_update,
|
||||
servers_to_delete,
|
||||
@@ -306,7 +324,7 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
);
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
Alerter::run_updates(
|
||||
Alerter::execute_sync_updates(
|
||||
alerters_to_create,
|
||||
alerters_to_update,
|
||||
alerters_to_delete,
|
||||
@@ -317,7 +335,7 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
// Dependent on server
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
Builder::run_updates(
|
||||
Builder::execute_sync_updates(
|
||||
builders_to_create,
|
||||
builders_to_update,
|
||||
builders_to_delete,
|
||||
@@ -326,7 +344,7 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
);
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
Repo::run_updates(
|
||||
Repo::execute_sync_updates(
|
||||
repos_to_create,
|
||||
repos_to_update,
|
||||
repos_to_delete,
|
||||
@@ -337,7 +355,7 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
// Dependant on builder
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
Build::run_updates(
|
||||
Build::execute_sync_updates(
|
||||
builds_to_create,
|
||||
builds_to_update,
|
||||
builds_to_delete,
|
||||
@@ -348,7 +366,7 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
// Dependant on server / build
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
Deployment::run_updates(
|
||||
Deployment::execute_sync_updates(
|
||||
deployments_to_create,
|
||||
deployments_to_update,
|
||||
deployments_to_delete,
|
||||
@@ -358,7 +376,7 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
// stack only depends on server, but maybe will depend on build later.
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
Stack::run_updates(
|
||||
Stack::execute_sync_updates(
|
||||
stacks_to_create,
|
||||
stacks_to_update,
|
||||
stacks_to_delete,
|
||||
@@ -369,7 +387,7 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
// Dependant on everything
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
Procedure::run_updates(
|
||||
Procedure::execute_sync_updates(
|
||||
procedures_to_create,
|
||||
procedures_to_update,
|
||||
procedures_to_delete,
|
||||
@@ -380,7 +398,7 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
// Execute the deploy cache
|
||||
deploy_from_cache(deploy_cache, &mut update.logs).await;
|
||||
|
||||
let db = db_client().await;
|
||||
let db = db_client();
|
||||
|
||||
if let Err(e) = update_one_by_id(
|
||||
&db.resource_syncs,
|
||||
|
||||
@@ -41,7 +41,7 @@ impl Resolve<ListAlerts, User> for State {
|
||||
}
|
||||
|
||||
let alerts = find_collect(
|
||||
&db_client().await.alerts,
|
||||
&db_client().alerts,
|
||||
query,
|
||||
FindOptions::builder()
|
||||
.sort(doc! { "ts": -1 })
|
||||
@@ -70,7 +70,7 @@ impl Resolve<GetAlert, User> for State {
|
||||
GetAlert { id }: GetAlert,
|
||||
_: User,
|
||||
) -> anyhow::Result<GetAlertResponse> {
|
||||
find_one_by_id(&db_client().await.alerts, &id)
|
||||
find_one_by_id(&db_client().alerts, &id)
|
||||
.await
|
||||
.context("failed to query db for alert")?
|
||||
.context("no alert found with given id")
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use anyhow::Context;
|
||||
use mongo_indexed::Document;
|
||||
use komodo_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
@@ -8,6 +7,7 @@ use komodo_client::{
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use mongo_indexed::Document;
|
||||
use mungos::mongodb::bson::doc;
|
||||
use resolver_api::Resolve;
|
||||
|
||||
@@ -67,7 +67,6 @@ impl Resolve<GetAlertersSummary, User> for State {
|
||||
None => Document::new(),
|
||||
};
|
||||
let total = db_client()
|
||||
.await
|
||||
.alerters
|
||||
.count_documents(query)
|
||||
.await
|
||||
|
||||
@@ -145,7 +145,6 @@ impl Resolve<GetBuildMonthlyStats, User> for State {
|
||||
let open_ts = close_ts - 30 * ONE_DAY_MS;
|
||||
|
||||
let mut build_updates = db_client()
|
||||
.await
|
||||
.updates
|
||||
.find(doc! {
|
||||
"start_ts": {
|
||||
@@ -229,7 +228,7 @@ impl Resolve<ListBuildVersions, User> for State {
|
||||
}
|
||||
|
||||
let versions = find_collect(
|
||||
&db_client().await.updates,
|
||||
&db_client().updates,
|
||||
filter,
|
||||
FindOptions::builder()
|
||||
.sort(doc! { "_id": -1 })
|
||||
@@ -328,7 +327,11 @@ impl Resolve<GetBuildWebhookEnabled, User> for State {
|
||||
..
|
||||
} = core_config();
|
||||
|
||||
let host = webhook_base_url.as_ref().unwrap_or(host);
|
||||
let host = if webhook_base_url.is_empty() {
|
||||
host
|
||||
} else {
|
||||
webhook_base_url
|
||||
};
|
||||
let url = format!("{host}/listener/github/build/{}", build.id);
|
||||
|
||||
for webhook in webhooks {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use anyhow::Context;
|
||||
use mongo_indexed::Document;
|
||||
use komodo_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
@@ -8,6 +7,7 @@ use komodo_client::{
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use mongo_indexed::Document;
|
||||
use mungos::mongodb::bson::doc;
|
||||
use resolver_api::Resolve;
|
||||
|
||||
@@ -67,7 +67,6 @@ impl Resolve<GetBuildersSummary, User> for State {
|
||||
None => Document::new(),
|
||||
};
|
||||
let total = db_client()
|
||||
.await
|
||||
.builders
|
||||
.count_documents(query)
|
||||
.await
|
||||
|
||||
@@ -88,7 +88,11 @@ const MAX_LOG_LENGTH: u64 = 5000;
|
||||
impl Resolve<GetDeploymentLog, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetDeploymentLog { deployment, tail }: GetDeploymentLog,
|
||||
GetDeploymentLog {
|
||||
deployment,
|
||||
tail,
|
||||
timestamps,
|
||||
}: GetDeploymentLog,
|
||||
user: User,
|
||||
) -> anyhow::Result<Log> {
|
||||
let Deployment {
|
||||
@@ -109,6 +113,7 @@ impl Resolve<GetDeploymentLog, User> for State {
|
||||
.request(api::container::GetContainerLog {
|
||||
name,
|
||||
tail: cmp::min(tail, MAX_LOG_LENGTH),
|
||||
timestamps,
|
||||
})
|
||||
.await
|
||||
.context("failed at call to periphery")
|
||||
@@ -123,6 +128,7 @@ impl Resolve<SearchDeploymentLog, User> for State {
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
}: SearchDeploymentLog,
|
||||
user: User,
|
||||
) -> anyhow::Result<Log> {
|
||||
@@ -146,6 +152,7 @@ impl Resolve<SearchDeploymentLog, User> for State {
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
})
|
||||
.await
|
||||
.context("failed at call to periphery")
|
||||
@@ -223,14 +230,17 @@ impl Resolve<GetDeploymentsSummary, User> for State {
|
||||
DeploymentState::Running => {
|
||||
res.running += 1;
|
||||
}
|
||||
DeploymentState::Unknown => {
|
||||
res.unknown += 1;
|
||||
DeploymentState::Exited | DeploymentState::Paused => {
|
||||
res.stopped += 1;
|
||||
}
|
||||
DeploymentState::NotDeployed => {
|
||||
res.not_deployed += 1;
|
||||
}
|
||||
DeploymentState::Unknown => {
|
||||
res.unknown += 1;
|
||||
}
|
||||
_ => {
|
||||
res.stopped += 1;
|
||||
res.unhealthy += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,8 +60,6 @@ enum ReadRequest {
|
||||
GetVersion(GetVersion),
|
||||
#[to_string_resolver]
|
||||
GetCoreInfo(GetCoreInfo),
|
||||
#[to_string_resolver]
|
||||
ListAwsEcrLabels(ListAwsEcrLabels),
|
||||
ListSecrets(ListSecrets),
|
||||
ListGitProvidersFromConfig(ListGitProvidersFromConfig),
|
||||
ListDockerRegistriesFromConfig(ListDockerRegistriesFromConfig),
|
||||
@@ -174,6 +172,7 @@ enum ReadRequest {
|
||||
ListFullStacks(ListFullStacks),
|
||||
ListStackServices(ListStackServices),
|
||||
ListCommonStackExtraArgs(ListCommonStackExtraArgs),
|
||||
ListCommonStackBuildExtraArgs(ListCommonStackBuildExtraArgs),
|
||||
|
||||
// ==== BUILDER ====
|
||||
GetBuildersSummary(GetBuildersSummary),
|
||||
@@ -282,12 +281,15 @@ fn core_info() -> &'static String {
|
||||
let info = GetCoreInfoResponse {
|
||||
title: config.title.clone(),
|
||||
monitoring_interval: config.monitoring_interval,
|
||||
webhook_base_url: config
|
||||
.webhook_base_url
|
||||
.clone()
|
||||
.unwrap_or_else(|| config.host.clone()),
|
||||
webhook_base_url: if config.webhook_base_url.is_empty() {
|
||||
config.host.clone()
|
||||
} else {
|
||||
config.webhook_base_url.clone()
|
||||
},
|
||||
transparent_mode: config.transparent_mode,
|
||||
ui_write_disabled: config.ui_write_disabled,
|
||||
disable_confirm_dialog: config.disable_confirm_dialog,
|
||||
disable_non_admin_create: config.disable_non_admin_create,
|
||||
github_webhook_owners: config
|
||||
.github_webhook_app
|
||||
.installations
|
||||
@@ -311,31 +313,6 @@ impl ResolveToString<GetCoreInfo, User> for State {
|
||||
}
|
||||
}
|
||||
|
||||
fn ecr_labels() -> &'static String {
|
||||
static ECR_LABELS: OnceLock<String> = OnceLock::new();
|
||||
ECR_LABELS.get_or_init(|| {
|
||||
serde_json::to_string(
|
||||
&core_config()
|
||||
.aws_ecr_registries
|
||||
.iter()
|
||||
.map(|reg| reg.label.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.context("failed to serialize ecr registries")
|
||||
.unwrap()
|
||||
})
|
||||
}
|
||||
|
||||
impl ResolveToString<ListAwsEcrLabels, User> for State {
|
||||
async fn resolve_to_string(
|
||||
&self,
|
||||
ListAwsEcrLabels {}: ListAwsEcrLabels,
|
||||
_: User,
|
||||
) -> anyhow::Result<String> {
|
||||
Ok(ecr_labels().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ListSecrets, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
|
||||
@@ -22,7 +22,7 @@ impl Resolve<ListPermissions, User> for State {
|
||||
user: User,
|
||||
) -> anyhow::Result<ListPermissionsResponse> {
|
||||
find_collect(
|
||||
&db_client().await.permissions,
|
||||
&db_client().permissions,
|
||||
doc! {
|
||||
"user_target.type": "User",
|
||||
"user_target.id": &user.id
|
||||
@@ -58,7 +58,7 @@ impl Resolve<ListUserTargetPermissions, User> for State {
|
||||
}
|
||||
let (variant, id) = user_target.extract_variant_id();
|
||||
find_collect(
|
||||
&db_client().await.permissions,
|
||||
&db_client().permissions,
|
||||
doc! {
|
||||
"user_target.type": variant.as_ref(),
|
||||
"user_target.id": id
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use mongo_indexed::{doc, Document};
|
||||
use komodo_client::{
|
||||
api::read::{
|
||||
GetDockerRegistryAccount, GetDockerRegistryAccountResponse,
|
||||
@@ -9,6 +8,7 @@ use komodo_client::{
|
||||
},
|
||||
entities::user::User,
|
||||
};
|
||||
use mongo_indexed::{doc, Document};
|
||||
use mungos::{
|
||||
by_id::find_one_by_id, find::find_collect,
|
||||
mongodb::options::FindOptions,
|
||||
@@ -28,7 +28,7 @@ impl Resolve<GetGitProviderAccount, User> for State {
|
||||
"Only admins can read git provider accounts"
|
||||
));
|
||||
}
|
||||
find_one_by_id(&db_client().await.git_accounts, &id)
|
||||
find_one_by_id(&db_client().git_accounts, &id)
|
||||
.await
|
||||
.context("failed to query db for git provider accounts")?
|
||||
.context("did not find git provider account with the given id")
|
||||
@@ -54,7 +54,7 @@ impl Resolve<ListGitProviderAccounts, User> for State {
|
||||
filter.insert("username", username);
|
||||
}
|
||||
find_collect(
|
||||
&db_client().await.git_accounts,
|
||||
&db_client().git_accounts,
|
||||
filter,
|
||||
FindOptions::builder()
|
||||
.sort(doc! { "domain": 1, "username": 1 })
|
||||
@@ -76,7 +76,7 @@ impl Resolve<GetDockerRegistryAccount, User> for State {
|
||||
"Only admins can read docker registry accounts"
|
||||
));
|
||||
}
|
||||
find_one_by_id(&db_client().await.registry_accounts, &id)
|
||||
find_one_by_id(&db_client().registry_accounts, &id)
|
||||
.await
|
||||
.context("failed to query db for docker registry accounts")?
|
||||
.context(
|
||||
@@ -104,7 +104,7 @@ impl Resolve<ListDockerRegistryAccounts, User> for State {
|
||||
filter.insert("username", username);
|
||||
}
|
||||
find_collect(
|
||||
&db_client().await.registry_accounts,
|
||||
&db_client().registry_accounts,
|
||||
filter,
|
||||
FindOptions::builder()
|
||||
.sort(doc! { "domain": 1, "username": 1 })
|
||||
|
||||
@@ -188,7 +188,11 @@ impl Resolve<GetRepoWebhooksEnabled, User> for State {
|
||||
..
|
||||
} = core_config();
|
||||
|
||||
let host = webhook_base_url.as_ref().unwrap_or(host);
|
||||
let host = if webhook_base_url.is_empty() {
|
||||
host
|
||||
} else {
|
||||
webhook_base_url
|
||||
};
|
||||
let clone_url =
|
||||
format!("{host}/listener/github/repo/{}/clone", repo.id);
|
||||
let pull_url =
|
||||
|
||||
@@ -43,8 +43,9 @@ use resolver_api::{Resolve, ResolveToString};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
helpers::{periphery_client, stack::compose_container_match_regex},
|
||||
helpers::periphery_client,
|
||||
resource,
|
||||
stack::compose_container_match_regex,
|
||||
state::{action_states, db_client, server_status_cache, State},
|
||||
};
|
||||
|
||||
@@ -320,7 +321,7 @@ impl Resolve<GetHistoricalServerStats, User> for State {
|
||||
}
|
||||
|
||||
let stats = find_collect(
|
||||
&db_client().await.stats,
|
||||
&db_client().stats,
|
||||
doc! {
|
||||
"sid": server.id,
|
||||
"ts": { "$in": ts_vec },
|
||||
@@ -403,6 +404,7 @@ impl Resolve<GetContainerLog, User> for State {
|
||||
server,
|
||||
container,
|
||||
tail,
|
||||
timestamps,
|
||||
}: GetContainerLog,
|
||||
user: User,
|
||||
) -> anyhow::Result<Log> {
|
||||
@@ -416,6 +418,7 @@ impl Resolve<GetContainerLog, User> for State {
|
||||
.request(periphery::container::GetContainerLog {
|
||||
name: container,
|
||||
tail: cmp::min(tail, MAX_LOG_LENGTH),
|
||||
timestamps,
|
||||
})
|
||||
.await
|
||||
.context("failed at call to periphery")
|
||||
@@ -431,6 +434,7 @@ impl Resolve<SearchContainerLog, User> for State {
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
}: SearchContainerLog,
|
||||
user: User,
|
||||
) -> anyhow::Result<Log> {
|
||||
@@ -446,6 +450,7 @@ impl Resolve<SearchContainerLog, User> for State {
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
})
|
||||
.await
|
||||
.context("failed at call to periphery")
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use anyhow::Context;
|
||||
use mongo_indexed::Document;
|
||||
use komodo_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
@@ -7,6 +6,7 @@ use komodo_client::{
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use mongo_indexed::Document;
|
||||
use mungos::mongodb::bson::doc;
|
||||
use resolver_api::Resolve;
|
||||
|
||||
@@ -67,7 +67,6 @@ impl Resolve<GetServerTemplatesSummary, User> for State {
|
||||
None => Document::new(),
|
||||
};
|
||||
let total = db_client()
|
||||
.await
|
||||
.server_templates
|
||||
.count_documents(query)
|
||||
.await
|
||||
|
||||
@@ -17,8 +17,9 @@ use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::{periphery_client, stack::get_stack_and_server},
|
||||
helpers::periphery_client,
|
||||
resource,
|
||||
stack::get_stack_and_server,
|
||||
state::{action_states, github_client, stack_status_cache, State},
|
||||
};
|
||||
|
||||
@@ -69,6 +70,7 @@ impl Resolve<GetStackServiceLog, User> for State {
|
||||
stack,
|
||||
service,
|
||||
tail,
|
||||
timestamps,
|
||||
}: GetStackServiceLog,
|
||||
user: User,
|
||||
) -> anyhow::Result<GetStackServiceLogResponse> {
|
||||
@@ -84,6 +86,7 @@ impl Resolve<GetStackServiceLog, User> for State {
|
||||
project: stack.project_name(false),
|
||||
service,
|
||||
tail,
|
||||
timestamps,
|
||||
})
|
||||
.await
|
||||
.context("failed to get stack service log from periphery")
|
||||
@@ -99,6 +102,7 @@ impl Resolve<SearchStackServiceLog, User> for State {
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
}: SearchStackServiceLog,
|
||||
user: User,
|
||||
) -> anyhow::Result<SearchStackServiceLogResponse> {
|
||||
@@ -116,6 +120,7 @@ impl Resolve<SearchStackServiceLog, User> for State {
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
})
|
||||
.await
|
||||
.context("failed to get stack service log from periphery")
|
||||
@@ -147,6 +152,31 @@ impl Resolve<ListCommonStackExtraArgs, User> for State {
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ListCommonStackBuildExtraArgs, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
ListCommonStackBuildExtraArgs { query }: ListCommonStackBuildExtraArgs,
|
||||
user: User,
|
||||
) -> anyhow::Result<ListCommonStackBuildExtraArgsResponse> {
|
||||
let stacks = resource::list_full_for_user::<Stack>(query, &user)
|
||||
.await
|
||||
.context("failed to get resources matching query")?;
|
||||
|
||||
// first collect with guaranteed uniqueness
|
||||
let mut res = HashSet::<String>::new();
|
||||
|
||||
for stack in stacks {
|
||||
for extra_arg in stack.config.build_extra_args {
|
||||
res.insert(extra_arg);
|
||||
}
|
||||
}
|
||||
|
||||
let mut res = res.into_iter().collect::<Vec<_>>();
|
||||
res.sort();
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ListStacks, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
@@ -211,15 +241,10 @@ impl Resolve<GetStacksSummary, User> for State {
|
||||
match cache.get(&stack.id).await.unwrap_or_default().curr.state
|
||||
{
|
||||
StackState::Running => res.running += 1,
|
||||
StackState::Paused => res.paused += 1,
|
||||
StackState::Stopped => res.stopped += 1,
|
||||
StackState::Restarting => res.restarting += 1,
|
||||
StackState::Created => res.created += 1,
|
||||
StackState::Removing => res.removing += 1,
|
||||
StackState::Dead => res.dead += 1,
|
||||
StackState::Unhealthy => res.unhealthy += 1,
|
||||
StackState::Stopped | StackState::Paused => res.stopped += 1,
|
||||
StackState::Down => res.down += 1,
|
||||
StackState::Unknown => res.unknown += 1,
|
||||
_ => res.unhealthy += 1,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,7 +311,11 @@ impl Resolve<GetStackWebhooksEnabled, User> for State {
|
||||
..
|
||||
} = core_config();
|
||||
|
||||
let host = webhook_base_url.as_ref().unwrap_or(host);
|
||||
let host = if webhook_base_url.is_empty() {
|
||||
host
|
||||
} else {
|
||||
webhook_base_url
|
||||
};
|
||||
let refresh_url =
|
||||
format!("{host}/listener/github/stack/{}/refresh", stack.id);
|
||||
let deploy_url =
|
||||
|
||||
@@ -5,8 +5,8 @@ use komodo_client::{
|
||||
config::core::CoreConfig,
|
||||
permission::PermissionLevel,
|
||||
sync::{
|
||||
PendingSyncUpdatesData, ResourceSync, ResourceSyncActionState,
|
||||
ResourceSyncListItem, ResourceSyncState,
|
||||
ResourceSync, ResourceSyncActionState, ResourceSyncListItem,
|
||||
ResourceSyncState,
|
||||
},
|
||||
user::User,
|
||||
},
|
||||
@@ -100,17 +100,18 @@ impl Resolve<GetResourceSyncsSummary, User> for State {
|
||||
for resource_sync in resource_syncs {
|
||||
res.total += 1;
|
||||
|
||||
match resource_sync.info.pending.data {
|
||||
PendingSyncUpdatesData::Ok(data) => {
|
||||
if !data.no_updates() {
|
||||
res.pending += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
PendingSyncUpdatesData::Err(_) => {
|
||||
res.failed += 1;
|
||||
continue;
|
||||
}
|
||||
if !(resource_sync.info.pending_deploy.to_deploy == 0
|
||||
&& resource_sync.info.resource_updates.is_empty()
|
||||
&& resource_sync.info.variable_updates.is_empty()
|
||||
&& resource_sync.info.user_group_updates.is_empty())
|
||||
{
|
||||
res.pending += 1;
|
||||
continue;
|
||||
} else if resource_sync.info.pending_error.is_some()
|
||||
|| !resource_sync.info.remote_errors.is_empty()
|
||||
{
|
||||
res.failed += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
match (
|
||||
@@ -201,7 +202,11 @@ impl Resolve<GetSyncWebhooksEnabled, User> for State {
|
||||
..
|
||||
} = core_config();
|
||||
|
||||
let host = webhook_base_url.as_ref().unwrap_or(host);
|
||||
let host = if webhook_base_url.is_empty() {
|
||||
host
|
||||
} else {
|
||||
webhook_base_url
|
||||
};
|
||||
let refresh_url =
|
||||
format!("{host}/listener/github/sync/{}/refresh", sync.id);
|
||||
let sync_url =
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use anyhow::Context;
|
||||
use mongo_indexed::doc;
|
||||
use komodo_client::{
|
||||
api::read::{GetTag, ListTags},
|
||||
entities::{tag::Tag, user::User},
|
||||
};
|
||||
use mongo_indexed::doc;
|
||||
use mungos::{find::find_collect, mongodb::options::FindOptions};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
@@ -29,7 +29,7 @@ impl Resolve<ListTags, User> for State {
|
||||
_: User,
|
||||
) -> anyhow::Result<Vec<Tag>> {
|
||||
find_collect(
|
||||
&db_client().await.tags,
|
||||
&db_client().tags,
|
||||
query,
|
||||
FindOptions::builder().sort(doc! { "name": 1 }).build(),
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -164,16 +164,15 @@ impl Resolve<ListUpdates, User> for State {
|
||||
query.into()
|
||||
};
|
||||
|
||||
let usernames =
|
||||
find_collect(&db_client().await.users, None, None)
|
||||
.await
|
||||
.context("failed to pull users from db")?
|
||||
.into_iter()
|
||||
.map(|u| (u.id, u.username))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let usernames = find_collect(&db_client().users, None, None)
|
||||
.await
|
||||
.context("failed to pull users from db")?
|
||||
.into_iter()
|
||||
.map(|u| (u.id, u.username))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let updates = find_collect(
|
||||
&db_client().await.updates,
|
||||
&db_client().updates,
|
||||
query,
|
||||
FindOptions::builder()
|
||||
.sort(doc! { "start_ts": -1 })
|
||||
@@ -224,7 +223,7 @@ impl Resolve<GetUpdate, User> for State {
|
||||
GetUpdate { id }: GetUpdate,
|
||||
user: User,
|
||||
) -> anyhow::Result<Update> {
|
||||
let update = find_one_by_id(&db_client().await.updates, &id)
|
||||
let update = find_one_by_id(&db_client().updates, &id)
|
||||
.await
|
||||
.context("failed to query to db")?
|
||||
.context("no update exists with given id")?;
|
||||
|
||||
@@ -26,7 +26,7 @@ impl Resolve<GetUsername, User> for State {
|
||||
GetUsername { user_id }: GetUsername,
|
||||
_: User,
|
||||
) -> anyhow::Result<GetUsernameResponse> {
|
||||
let user = find_one_by_id(&db_client().await.users, &user_id)
|
||||
let user = find_one_by_id(&db_client().users, &user_id)
|
||||
.await
|
||||
.context("failed at mongo query for user")?
|
||||
.context("no user found with id")?;
|
||||
@@ -67,7 +67,7 @@ impl Resolve<ListUsers, User> for State {
|
||||
return Err(anyhow!("this route is only accessable by admins"));
|
||||
}
|
||||
let mut users = find_collect(
|
||||
&db_client().await.users,
|
||||
&db_client().users,
|
||||
None,
|
||||
FindOptions::builder().sort(doc! { "username": 1 }).build(),
|
||||
)
|
||||
@@ -85,7 +85,7 @@ impl Resolve<ListApiKeys, User> for State {
|
||||
user: User,
|
||||
) -> anyhow::Result<ListApiKeysResponse> {
|
||||
let api_keys = find_collect(
|
||||
&db_client().await.api_keys,
|
||||
&db_client().api_keys,
|
||||
doc! { "user_id": &user.id },
|
||||
FindOptions::builder().sort(doc! { "name": 1 }).build(),
|
||||
)
|
||||
@@ -117,7 +117,7 @@ impl Resolve<ListApiKeysForServiceUser, User> for State {
|
||||
return Err(anyhow!("Given user is not service user"));
|
||||
};
|
||||
let api_keys = find_collect(
|
||||
&db_client().await.api_keys,
|
||||
&db_client().api_keys,
|
||||
doc! { "user_id": &user.id },
|
||||
None,
|
||||
)
|
||||
|
||||
@@ -35,7 +35,6 @@ impl Resolve<GetUserGroup, User> for State {
|
||||
filter.insert("users", &user.id);
|
||||
}
|
||||
db_client()
|
||||
.await
|
||||
.user_groups
|
||||
.find_one(filter)
|
||||
.await
|
||||
@@ -55,7 +54,7 @@ impl Resolve<ListUserGroups, User> for State {
|
||||
filter.insert("users", &user.id);
|
||||
}
|
||||
find_collect(
|
||||
&db_client().await.user_groups,
|
||||
&db_client().user_groups,
|
||||
filter,
|
||||
FindOptions::builder().sort(doc! { "name": 1 }).build(),
|
||||
)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use anyhow::Context;
|
||||
use mongo_indexed::doc;
|
||||
use komodo_client::{
|
||||
api::read::{
|
||||
GetVariable, GetVariableResponse, ListVariables,
|
||||
@@ -7,6 +6,7 @@ use komodo_client::{
|
||||
},
|
||||
entities::user::User,
|
||||
};
|
||||
use mongo_indexed::doc;
|
||||
use mungos::{find::find_collect, mongodb::options::FindOptions};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
@@ -37,7 +37,7 @@ impl Resolve<ListVariables, User> for State {
|
||||
user: User,
|
||||
) -> anyhow::Result<ListVariablesResponse> {
|
||||
let variables = find_collect(
|
||||
&db_client().await.variables,
|
||||
&db_client().variables,
|
||||
None,
|
||||
FindOptions::builder().sort(doc! { "name": 1 }).build(),
|
||||
)
|
||||
|
||||
@@ -103,7 +103,7 @@ impl Resolve<PushRecentlyViewed, User> for State {
|
||||
}
|
||||
};
|
||||
update_one_by_id(
|
||||
&db_client().await.users,
|
||||
&db_client().users,
|
||||
&user.id,
|
||||
mungos::update::Update::Set(update),
|
||||
None,
|
||||
@@ -129,7 +129,7 @@ impl Resolve<SetLastSeenUpdate, User> for State {
|
||||
user: User,
|
||||
) -> anyhow::Result<SetLastSeenUpdateResponse> {
|
||||
update_one_by_id(
|
||||
&db_client().await.users,
|
||||
&db_client().users,
|
||||
&user.id,
|
||||
mungos::update::Update::Set(doc! {
|
||||
"last_update_view": komodo_timestamp()
|
||||
@@ -172,7 +172,6 @@ impl Resolve<CreateApiKey, User> for State {
|
||||
expires,
|
||||
};
|
||||
db_client()
|
||||
.await
|
||||
.api_keys
|
||||
.insert_one(api_key)
|
||||
.await
|
||||
@@ -192,7 +191,7 @@ impl Resolve<DeleteApiKey, User> for State {
|
||||
DeleteApiKey { key }: DeleteApiKey,
|
||||
user: User,
|
||||
) -> anyhow::Result<DeleteApiKeyResponse> {
|
||||
let client = db_client().await;
|
||||
let client = db_client();
|
||||
let key = client
|
||||
.api_keys
|
||||
.find_one(doc! { "key": &key })
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use mongo_indexed::doc;
|
||||
use git::GitRes;
|
||||
use komodo_client::{
|
||||
api::write::*,
|
||||
entities::{
|
||||
@@ -10,6 +10,7 @@ use komodo_client::{
|
||||
CloneArgs, NoData,
|
||||
},
|
||||
};
|
||||
use mongo_indexed::doc;
|
||||
use mungos::mongodb::bson::to_document;
|
||||
use octorust::types::{
|
||||
ReposCreateWebhookRequest, ReposCreateWebhookRequestConfig,
|
||||
@@ -18,7 +19,7 @@ use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::{git_token, random_string},
|
||||
helpers::git_token,
|
||||
resource,
|
||||
state::{db_client, github_client, State},
|
||||
};
|
||||
@@ -96,35 +97,40 @@ impl Resolve<RefreshBuildCache, User> for State {
|
||||
)
|
||||
.await?;
|
||||
|
||||
if build.config.repo.is_empty()
|
||||
|| build.config.git_provider.is_empty()
|
||||
{
|
||||
// Nothing to do here
|
||||
return Ok(NoData {});
|
||||
}
|
||||
|
||||
let config = core_config();
|
||||
|
||||
let repo_dir = config.repo_directory.join(random_string(10));
|
||||
let mut clone_args: CloneArgs = (&build).into();
|
||||
let repo_path =
|
||||
clone_args.unique_path(&core_config().repo_directory)?;
|
||||
clone_args.destination = Some(repo_path.display().to_string());
|
||||
// Don't want to run these on core.
|
||||
clone_args.on_clone = None;
|
||||
clone_args.on_pull = None;
|
||||
clone_args.destination = Some(repo_dir.display().to_string());
|
||||
|
||||
let access_token = match (&clone_args.account, &clone_args.provider)
|
||||
{
|
||||
(None, _) => None,
|
||||
(Some(_), None) => {
|
||||
return Err(anyhow!(
|
||||
"Account is configured, but provider is empty"
|
||||
))
|
||||
}
|
||||
(Some(username), Some(provider)) => {
|
||||
git_token(provider, username, |https| {
|
||||
let access_token = if let Some(username) = &clone_args.account {
|
||||
git_token(&clone_args.provider, username, |https| {
|
||||
clone_args.https = https
|
||||
})
|
||||
.await
|
||||
.with_context(
|
||||
|| format!("Failed to get git token in call to db. Stopping run. | {provider} | {username}"),
|
||||
|| format!("Failed to get git token in call to db. Stopping run. | {} | {username}", clone_args.provider),
|
||||
)?
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (_, latest_hash, latest_message, _) = git::clone(
|
||||
let GitRes {
|
||||
hash: latest_hash,
|
||||
message: latest_message,
|
||||
..
|
||||
} = git::pull_or_clone(
|
||||
clone_args,
|
||||
&config.repo_directory,
|
||||
access_token,
|
||||
@@ -148,7 +154,6 @@ impl Resolve<RefreshBuildCache, User> for State {
|
||||
.context("failed to serialize build info to bson")?;
|
||||
|
||||
db_client()
|
||||
.await
|
||||
.builds
|
||||
.update_one(
|
||||
doc! { "name": &build.name },
|
||||
@@ -157,12 +162,6 @@ impl Resolve<RefreshBuildCache, User> for State {
|
||||
.await
|
||||
.context("failed to update build info on db")?;
|
||||
|
||||
if repo_dir.exists() {
|
||||
if let Err(e) = std::fs::remove_dir_all(&repo_dir) {
|
||||
warn!("failed to remove build cache update repo directory | {e:?}")
|
||||
}
|
||||
}
|
||||
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
@@ -227,7 +226,11 @@ impl Resolve<CreateBuildWebhook, User> for State {
|
||||
&build.config.webhook_secret
|
||||
};
|
||||
|
||||
let host = webhook_base_url.as_ref().unwrap_or(host);
|
||||
let host = if webhook_base_url.is_empty() {
|
||||
host
|
||||
} else {
|
||||
webhook_base_url
|
||||
};
|
||||
let url = format!("{host}/listener/github/build/{}", build.id);
|
||||
|
||||
for webhook in webhooks {
|
||||
@@ -333,7 +336,11 @@ impl Resolve<DeleteBuildWebhook, User> for State {
|
||||
..
|
||||
} = core_config();
|
||||
|
||||
let host = webhook_base_url.as_ref().unwrap_or(host);
|
||||
let host = if webhook_base_url.is_empty() {
|
||||
host
|
||||
} else {
|
||||
webhook_base_url
|
||||
};
|
||||
let url = format!("{host}/listener/github/build/{}", build.id);
|
||||
|
||||
for webhook in webhooks {
|
||||
|
||||
@@ -116,7 +116,7 @@ impl Resolve<RenameDeployment, User> for State {
|
||||
make_update(&deployment, Operation::RenameDeployment, &user);
|
||||
|
||||
update_one_by_id(
|
||||
&db_client().await.deployments,
|
||||
&db_client().deployments,
|
||||
&deployment.id,
|
||||
mungos::update::Update::Set(
|
||||
doc! { "name": &name, "updated_at": komodo_timestamp() },
|
||||
|
||||
@@ -28,6 +28,7 @@ mod service_user;
|
||||
mod stack;
|
||||
mod sync;
|
||||
mod tag;
|
||||
mod user;
|
||||
mod user_group;
|
||||
mod variable;
|
||||
|
||||
@@ -40,6 +41,11 @@ mod variable;
|
||||
#[resolver_args(User)]
|
||||
#[serde(tag = "type", content = "params")]
|
||||
pub enum WriteRequest {
|
||||
// ==== USER ====
|
||||
UpdateUserUsername(UpdateUserUsername),
|
||||
UpdateUserPassword(UpdateUserPassword),
|
||||
DeleteUser(DeleteUser),
|
||||
|
||||
// ==== SERVICE USER ====
|
||||
CreateServiceUser(CreateServiceUser),
|
||||
UpdateServiceUserDescription(UpdateServiceUserDescription),
|
||||
@@ -55,6 +61,7 @@ pub enum WriteRequest {
|
||||
SetUsersInUserGroup(SetUsersInUserGroup),
|
||||
|
||||
// ==== PERMISSIONS ====
|
||||
UpdateUserAdmin(UpdateUserAdmin),
|
||||
UpdateUserBasePermissions(UpdateUserBasePermissions),
|
||||
UpdatePermissionOnResourceType(UpdatePermissionOnResourceType),
|
||||
UpdatePermissionOnTarget(UpdatePermissionOnTarget),
|
||||
@@ -124,6 +131,7 @@ pub enum WriteRequest {
|
||||
DeleteResourceSync(DeleteResourceSync),
|
||||
UpdateResourceSync(UpdateResourceSync),
|
||||
RefreshResourceSyncPending(RefreshResourceSyncPending),
|
||||
CommitSync(CommitSync),
|
||||
CreateSyncWebhook(CreateSyncWebhook),
|
||||
DeleteSyncWebhook(DeleteSyncWebhook),
|
||||
|
||||
@@ -133,6 +141,7 @@ pub enum WriteRequest {
|
||||
DeleteStack(DeleteStack),
|
||||
UpdateStack(UpdateStack),
|
||||
RenameStack(RenameStack),
|
||||
WriteStackFileContents(WriteStackFileContents),
|
||||
RefreshStackCache(RefreshStackCache),
|
||||
CreateStackWebhook(CreateStackWebhook),
|
||||
DeleteStackWebhook(DeleteStackWebhook),
|
||||
@@ -185,7 +194,10 @@ async fn handler(
|
||||
#[instrument(
|
||||
name = "WriteRequest",
|
||||
skip(user, request),
|
||||
fields(user_id = user.id, request = format!("{:?}", request.extract_variant()))
|
||||
fields(
|
||||
user_id = user.id,
|
||||
request = format!("{:?}", request.extract_variant())
|
||||
)
|
||||
)]
|
||||
async fn task(
|
||||
req_id: Uuid,
|
||||
|
||||
@@ -5,7 +5,8 @@ use komodo_client::{
|
||||
api::write::{
|
||||
UpdatePermissionOnResourceType,
|
||||
UpdatePermissionOnResourceTypeResponse, UpdatePermissionOnTarget,
|
||||
UpdatePermissionOnTargetResponse, UpdateUserBasePermissions,
|
||||
UpdatePermissionOnTargetResponse, UpdateUserAdmin,
|
||||
UpdateUserAdminResponse, UpdateUserBasePermissions,
|
||||
UpdateUserBasePermissionsResponse,
|
||||
},
|
||||
entities::{
|
||||
@@ -28,6 +29,40 @@ use crate::{
|
||||
state::{db_client, State},
|
||||
};
|
||||
|
||||
impl Resolve<UpdateUserAdmin, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
UpdateUserAdmin { user_id, admin }: UpdateUserAdmin,
|
||||
super_admin: User,
|
||||
) -> anyhow::Result<UpdateUserAdminResponse> {
|
||||
if !super_admin.super_admin {
|
||||
return Err(anyhow!("Only super admins can call this method."));
|
||||
}
|
||||
let user = find_one_by_id(&db_client().users, &user_id)
|
||||
.await
|
||||
.context("failed to query mongo for user")?
|
||||
.context("did not find user with given id")?;
|
||||
|
||||
if !user.enabled {
|
||||
return Err(anyhow!("User is disabled. Enable user first."));
|
||||
}
|
||||
|
||||
if user.super_admin {
|
||||
return Err(anyhow!("Cannot update other super admins"));
|
||||
}
|
||||
|
||||
update_one_by_id(
|
||||
&db_client().users,
|
||||
&user_id,
|
||||
doc! { "$set": { "admin": admin } },
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(UpdateUserAdminResponse {})
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<UpdateUserBasePermissions, User> for State {
|
||||
#[instrument(name = "UpdateUserBasePermissions", skip(self, admin))]
|
||||
async fn resolve(
|
||||
@@ -44,13 +79,18 @@ impl Resolve<UpdateUserBasePermissions, User> for State {
|
||||
return Err(anyhow!("this method is admin only"));
|
||||
}
|
||||
|
||||
let user = find_one_by_id(&db_client().await.users, &user_id)
|
||||
let user = find_one_by_id(&db_client().users, &user_id)
|
||||
.await
|
||||
.context("failed to query mongo for user")?
|
||||
.context("did not find user with given id")?;
|
||||
if user.admin {
|
||||
if user.super_admin {
|
||||
return Err(anyhow!(
|
||||
"cannot use this method to update other admins permissions"
|
||||
"Cannot use this method to update super admins permissions"
|
||||
));
|
||||
}
|
||||
if user.admin && !admin.super_admin {
|
||||
return Err(anyhow!(
|
||||
"Only super admins can use this method to update other admins permissions"
|
||||
));
|
||||
}
|
||||
let mut update_doc = Document::new();
|
||||
@@ -65,7 +105,7 @@ impl Resolve<UpdateUserBasePermissions, User> for State {
|
||||
}
|
||||
|
||||
update_one_by_id(
|
||||
&db_client().await.users,
|
||||
&db_client().users,
|
||||
&user_id,
|
||||
mungos::update::Update::Set(update_doc),
|
||||
None,
|
||||
@@ -119,7 +159,6 @@ impl Resolve<UpdatePermissionOnResourceType, User> for State {
|
||||
match user_target_variant {
|
||||
UserTargetVariant::User => {
|
||||
db_client()
|
||||
.await
|
||||
.users
|
||||
.update_one(filter, update)
|
||||
.await
|
||||
@@ -129,7 +168,6 @@ impl Resolve<UpdatePermissionOnResourceType, User> for State {
|
||||
}
|
||||
UserTargetVariant::UserGroup => {
|
||||
db_client()
|
||||
.await
|
||||
.user_groups
|
||||
.update_one(filter, update)
|
||||
.await
|
||||
@@ -181,7 +219,6 @@ impl Resolve<UpdatePermissionOnTarget, User> for State {
|
||||
(user_target_variant.as_ref(), resource_variant.as_ref());
|
||||
|
||||
db_client()
|
||||
.await
|
||||
.permissions
|
||||
.update_one(
|
||||
doc! {
|
||||
@@ -218,7 +255,6 @@ async fn extract_user_target_with_validation(
|
||||
Err(_) => doc! { "username": ident },
|
||||
};
|
||||
let id = db_client()
|
||||
.await
|
||||
.users
|
||||
.find_one(filter)
|
||||
.await
|
||||
@@ -233,7 +269,6 @@ async fn extract_user_target_with_validation(
|
||||
Err(_) => doc! { "name": ident },
|
||||
};
|
||||
let id = db_client()
|
||||
.await
|
||||
.user_groups
|
||||
.find_one(filter)
|
||||
.await
|
||||
@@ -260,7 +295,6 @@ async fn extract_resource_target_with_validation(
|
||||
Err(_) => doc! { "name": ident },
|
||||
};
|
||||
let id = db_client()
|
||||
.await
|
||||
.builds
|
||||
.find_one(filter)
|
||||
.await
|
||||
@@ -275,7 +309,6 @@ async fn extract_resource_target_with_validation(
|
||||
Err(_) => doc! { "name": ident },
|
||||
};
|
||||
let id = db_client()
|
||||
.await
|
||||
.builders
|
||||
.find_one(filter)
|
||||
.await
|
||||
@@ -290,7 +323,6 @@ async fn extract_resource_target_with_validation(
|
||||
Err(_) => doc! { "name": ident },
|
||||
};
|
||||
let id = db_client()
|
||||
.await
|
||||
.deployments
|
||||
.find_one(filter)
|
||||
.await
|
||||
@@ -305,7 +337,6 @@ async fn extract_resource_target_with_validation(
|
||||
Err(_) => doc! { "name": ident },
|
||||
};
|
||||
let id = db_client()
|
||||
.await
|
||||
.servers
|
||||
.find_one(filter)
|
||||
.await
|
||||
@@ -320,7 +351,6 @@ async fn extract_resource_target_with_validation(
|
||||
Err(_) => doc! { "name": ident },
|
||||
};
|
||||
let id = db_client()
|
||||
.await
|
||||
.repos
|
||||
.find_one(filter)
|
||||
.await
|
||||
@@ -335,7 +365,6 @@ async fn extract_resource_target_with_validation(
|
||||
Err(_) => doc! { "name": ident },
|
||||
};
|
||||
let id = db_client()
|
||||
.await
|
||||
.alerters
|
||||
.find_one(filter)
|
||||
.await
|
||||
@@ -350,7 +379,6 @@ async fn extract_resource_target_with_validation(
|
||||
Err(_) => doc! { "name": ident },
|
||||
};
|
||||
let id = db_client()
|
||||
.await
|
||||
.procedures
|
||||
.find_one(filter)
|
||||
.await
|
||||
@@ -365,7 +393,6 @@ async fn extract_resource_target_with_validation(
|
||||
Err(_) => doc! { "name": ident },
|
||||
};
|
||||
let id = db_client()
|
||||
.await
|
||||
.server_templates
|
||||
.find_one(filter)
|
||||
.await
|
||||
@@ -380,7 +407,6 @@ async fn extract_resource_target_with_validation(
|
||||
Err(_) => doc! { "name": ident },
|
||||
};
|
||||
let id = db_client()
|
||||
.await
|
||||
.resource_syncs
|
||||
.find_one(filter)
|
||||
.await
|
||||
@@ -395,7 +421,6 @@ async fn extract_resource_target_with_validation(
|
||||
Err(_) => doc! { "name": ident },
|
||||
};
|
||||
let id = db_client()
|
||||
.await
|
||||
.stacks
|
||||
.find_one(filter)
|
||||
.await
|
||||
|
||||
@@ -47,7 +47,6 @@ impl Resolve<CreateGitProviderAccount, User> for State {
|
||||
);
|
||||
|
||||
account.id = db_client()
|
||||
.await
|
||||
.git_accounts
|
||||
.insert_one(&account)
|
||||
.await
|
||||
@@ -118,7 +117,7 @@ impl Resolve<UpdateGitProviderAccount, User> for State {
|
||||
let account = to_document(&account).context(
|
||||
"failed to serialize partial git provider account to bson",
|
||||
)?;
|
||||
let db = db_client().await;
|
||||
let db = db_client();
|
||||
update_one_by_id(
|
||||
&db.git_accounts,
|
||||
&id,
|
||||
@@ -175,7 +174,7 @@ impl Resolve<DeleteGitProviderAccount, User> for State {
|
||||
&user,
|
||||
);
|
||||
|
||||
let db = db_client().await;
|
||||
let db = db_client();
|
||||
let Some(account) =
|
||||
find_one_by_id(&db.git_accounts, &id)
|
||||
.await
|
||||
@@ -237,7 +236,6 @@ impl Resolve<CreateDockerRegistryAccount, User> for State {
|
||||
);
|
||||
|
||||
account.id = db_client()
|
||||
.await
|
||||
.registry_accounts
|
||||
.insert_one(&account)
|
||||
.await
|
||||
@@ -310,7 +308,7 @@ impl Resolve<UpdateDockerRegistryAccount, User> for State {
|
||||
"failed to serialize partial docker registry account account to bson",
|
||||
)?;
|
||||
|
||||
let db = db_client().await;
|
||||
let db = db_client();
|
||||
update_one_by_id(
|
||||
&db.registry_accounts,
|
||||
&id,
|
||||
@@ -368,7 +366,7 @@ impl Resolve<DeleteDockerRegistryAccount, User> for State {
|
||||
&user,
|
||||
);
|
||||
|
||||
let db = db_client().await;
|
||||
let db = db_client();
|
||||
let Some(account) = find_one_by_id(&db.registry_accounts, &id)
|
||||
.await
|
||||
.context("failed to query db for git accounts")?
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use mongo_indexed::doc;
|
||||
use git::GitRes;
|
||||
use komodo_client::{
|
||||
api::write::*,
|
||||
entities::{
|
||||
@@ -10,6 +10,7 @@ use komodo_client::{
|
||||
CloneArgs, NoData,
|
||||
},
|
||||
};
|
||||
use mongo_indexed::doc;
|
||||
use mungos::mongodb::bson::to_document;
|
||||
use octorust::types::{
|
||||
ReposCreateWebhookRequest, ReposCreateWebhookRequestConfig,
|
||||
@@ -18,7 +19,7 @@ use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::{git_token, random_string},
|
||||
helpers::git_token,
|
||||
resource,
|
||||
state::{db_client, github_client, State},
|
||||
};
|
||||
@@ -94,42 +95,36 @@ impl Resolve<RefreshRepoCache, User> for State {
|
||||
)
|
||||
.await?;
|
||||
|
||||
if repo.config.repo.is_empty() {
|
||||
if repo.config.git_provider.is_empty()
|
||||
|| repo.config.repo.is_empty()
|
||||
{
|
||||
// Nothing to do
|
||||
return Ok(NoData {});
|
||||
}
|
||||
|
||||
let config = core_config();
|
||||
|
||||
let repo_dir = config.repo_directory.join(random_string(10));
|
||||
let mut clone_args: CloneArgs = (&repo).into();
|
||||
// No reason to to the commands here.
|
||||
let repo_path =
|
||||
clone_args.unique_path(&core_config().repo_directory)?;
|
||||
clone_args.destination = Some(repo_path.display().to_string());
|
||||
// Don't want to run these on core.
|
||||
clone_args.on_clone = None;
|
||||
clone_args.on_pull = None;
|
||||
clone_args.destination = Some(repo_dir.display().to_string());
|
||||
|
||||
let access_token = match (&clone_args.account, &clone_args.provider)
|
||||
{
|
||||
(None, _) => None,
|
||||
(Some(_), None) => {
|
||||
return Err(anyhow!(
|
||||
"Account is configured, but provider is empty"
|
||||
))
|
||||
}
|
||||
(Some(username), Some(provider)) => {
|
||||
git_token(provider, username, |https| {
|
||||
let access_token = if let Some(username) = &clone_args.account {
|
||||
git_token(&clone_args.provider, username, |https| {
|
||||
clone_args.https = https
|
||||
})
|
||||
.await
|
||||
.with_context(
|
||||
|| format!("Failed to get git token in call to db. Stopping run. | {provider} | {username}"),
|
||||
|| format!("Failed to get git token in call to db. Stopping run. | {} | {username}", clone_args.provider),
|
||||
)?
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (_, latest_hash, latest_message, _) = git::clone(
|
||||
let GitRes { hash, message, .. } = git::pull_or_clone(
|
||||
clone_args,
|
||||
&config.repo_directory,
|
||||
&core_config().repo_directory,
|
||||
access_token,
|
||||
&[],
|
||||
"",
|
||||
@@ -137,22 +132,23 @@ impl Resolve<RefreshRepoCache, User> for State {
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.context("failed to clone repo (the resource) repo")?;
|
||||
.with_context(|| {
|
||||
format!("Failed to update repo at {repo_path:?}")
|
||||
})?;
|
||||
|
||||
let info = RepoInfo {
|
||||
last_pulled_at: repo.info.last_pulled_at,
|
||||
last_built_at: repo.info.last_built_at,
|
||||
built_hash: repo.info.built_hash,
|
||||
built_message: repo.info.built_message,
|
||||
latest_hash,
|
||||
latest_message,
|
||||
latest_hash: hash,
|
||||
latest_message: message,
|
||||
};
|
||||
|
||||
let info = to_document(&info)
|
||||
.context("failed to serialize repo info to bson")?;
|
||||
|
||||
db_client()
|
||||
.await
|
||||
.repos
|
||||
.update_one(
|
||||
doc! { "name": &repo.name },
|
||||
@@ -161,14 +157,6 @@ impl Resolve<RefreshRepoCache, User> for State {
|
||||
.await
|
||||
.context("failed to update repo info on db")?;
|
||||
|
||||
if repo_dir.exists() {
|
||||
if let Err(e) = std::fs::remove_dir_all(&repo_dir) {
|
||||
warn!(
|
||||
"failed to remove repo (resource) cache update repo directory | {e:?}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
@@ -233,7 +221,11 @@ impl Resolve<CreateRepoWebhook, User> for State {
|
||||
&repo.config.webhook_secret
|
||||
};
|
||||
|
||||
let host = webhook_base_url.as_ref().unwrap_or(host);
|
||||
let host = if webhook_base_url.is_empty() {
|
||||
host
|
||||
} else {
|
||||
webhook_base_url
|
||||
};
|
||||
let url = match action {
|
||||
RepoWebhookAction::Clone => {
|
||||
format!("{host}/listener/github/repo/{}/clone", repo.id)
|
||||
@@ -350,7 +342,11 @@ impl Resolve<DeleteRepoWebhook, User> for State {
|
||||
..
|
||||
} = core_config();
|
||||
|
||||
let host = webhook_base_url.as_ref().unwrap_or(host);
|
||||
let host = if webhook_base_url.is_empty() {
|
||||
host
|
||||
} else {
|
||||
webhook_base_url
|
||||
};
|
||||
let url = match action {
|
||||
RepoWebhookAction::Clone => {
|
||||
format!("{host}/listener/github/repo/{}/clone", repo.id)
|
||||
|
||||
@@ -73,7 +73,7 @@ impl Resolve<RenameServer, User> for State {
|
||||
let mut update =
|
||||
make_update(&server, Operation::RenameServer, &user);
|
||||
|
||||
update_one_by_id(&db_client().await.servers, &id, mungos::update::Update::Set(doc! { "name": &name, "updated_at": komodo_timestamp() }), None)
|
||||
update_one_by_id(&db_client().servers, &id, mungos::update::Update::Set(doc! { "name": &name, "updated_at": komodo_timestamp() }), None)
|
||||
.await
|
||||
.context("failed to update server on db. this name may already be taken.")?;
|
||||
update.push_simple_log(
|
||||
|
||||
@@ -48,6 +48,7 @@ impl Resolve<CreateServiceUser, User> for State {
|
||||
config,
|
||||
enabled: true,
|
||||
admin: false,
|
||||
super_admin: false,
|
||||
create_server_permissions: false,
|
||||
create_build_permissions: false,
|
||||
last_update_view: 0,
|
||||
@@ -56,7 +57,6 @@ impl Resolve<CreateServiceUser, User> for State {
|
||||
updated_at: komodo_timestamp(),
|
||||
};
|
||||
user.id = db_client()
|
||||
.await
|
||||
.users
|
||||
.insert_one(&user)
|
||||
.await
|
||||
@@ -85,7 +85,7 @@ impl Resolve<UpdateServiceUserDescription, User> for State {
|
||||
if !user.admin {
|
||||
return Err(anyhow!("user not admin"));
|
||||
}
|
||||
let db = db_client().await;
|
||||
let db = db_client();
|
||||
let service_user = db
|
||||
.users
|
||||
.find_one(doc! { "username": &username })
|
||||
@@ -124,11 +124,10 @@ impl Resolve<CreateApiKeyForServiceUser, User> for State {
|
||||
if !user.admin {
|
||||
return Err(anyhow!("user not admin"));
|
||||
}
|
||||
let service_user =
|
||||
find_one_by_id(&db_client().await.users, &user_id)
|
||||
.await
|
||||
.context("failed to query db for user")?
|
||||
.context("no user found with id")?;
|
||||
let service_user = find_one_by_id(&db_client().users, &user_id)
|
||||
.await
|
||||
.context("failed to query db for user")?
|
||||
.context("no user found with id")?;
|
||||
let UserConfig::Service { .. } = &service_user.config else {
|
||||
return Err(anyhow!("user is not service user"));
|
||||
};
|
||||
@@ -148,7 +147,7 @@ impl Resolve<DeleteApiKeyForServiceUser, User> for State {
|
||||
if !user.admin {
|
||||
return Err(anyhow!("user not admin"));
|
||||
}
|
||||
let db = db_client().await;
|
||||
let db = db_client();
|
||||
let api_key = db
|
||||
.api_keys
|
||||
.find_one(doc! { "key": &key })
|
||||
@@ -156,7 +155,7 @@ impl Resolve<DeleteApiKeyForServiceUser, User> for State {
|
||||
.context("failed to query db for api key")?
|
||||
.context("did not find matching api key")?;
|
||||
let service_user =
|
||||
find_one_by_id(&db_client().await.users, &api_key.user_id)
|
||||
find_one_by_id(&db_client().users, &api_key.user_id)
|
||||
.await
|
||||
.context("failed to query db for user")?
|
||||
.context("no user found with id")?;
|
||||
|
||||
@@ -7,10 +7,10 @@ use komodo_client::{
|
||||
komodo_timestamp,
|
||||
permission::PermissionLevel,
|
||||
server::ServerState,
|
||||
stack::{ComposeContents, PartialStackConfig, Stack, StackInfo},
|
||||
stack::{PartialStackConfig, Stack, StackInfo},
|
||||
update::Update,
|
||||
user::User,
|
||||
NoData, Operation,
|
||||
user::{stack_user, User},
|
||||
FileContents, NoData, Operation,
|
||||
},
|
||||
};
|
||||
use mungos::{
|
||||
@@ -22,6 +22,7 @@ use octorust::types::{
|
||||
};
|
||||
use periphery_client::api::compose::{
|
||||
GetComposeContentsOnHost, GetComposeContentsOnHostResponse,
|
||||
WriteComposeContentsToHost,
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
@@ -30,13 +31,14 @@ use crate::{
|
||||
helpers::{
|
||||
periphery_client,
|
||||
query::get_server_with_state,
|
||||
stack::{
|
||||
remote::get_remote_compose_contents,
|
||||
services::extract_services_into_res,
|
||||
},
|
||||
update::{add_update, make_update},
|
||||
},
|
||||
resource,
|
||||
stack::{
|
||||
get_stack_and_server,
|
||||
remote::{get_remote_compose_contents, RemoteComposeContents},
|
||||
services::extract_services_into_res,
|
||||
},
|
||||
state::{db_client, github_client, State},
|
||||
};
|
||||
|
||||
@@ -109,7 +111,7 @@ impl Resolve<RenameStack, User> for State {
|
||||
make_update(&stack, Operation::RenameStack, &user);
|
||||
|
||||
update_one_by_id(
|
||||
&db_client().await.stacks,
|
||||
&db_client().stacks,
|
||||
&stack.id,
|
||||
mungos::update::Update::Set(
|
||||
doc! { "name": &name, "updated_at": komodo_timestamp() },
|
||||
@@ -131,6 +133,79 @@ impl Resolve<RenameStack, User> for State {
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<WriteStackFileContents, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
WriteStackFileContents {
|
||||
stack,
|
||||
file_path,
|
||||
contents,
|
||||
}: WriteStackFileContents,
|
||||
user: User,
|
||||
) -> anyhow::Result<Update> {
|
||||
let (stack, server) = get_stack_and_server(
|
||||
&stack,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !stack.config.files_on_host {
|
||||
return Err(anyhow!(
|
||||
"Stack is not configured to use files on host, can't write file contents"
|
||||
));
|
||||
}
|
||||
|
||||
let mut update =
|
||||
make_update(&stack, Operation::WriteStackContents, &user);
|
||||
|
||||
update.push_simple_log("File contents to write", &contents);
|
||||
|
||||
match periphery_client(&server)?
|
||||
.request(WriteComposeContentsToHost {
|
||||
name: stack.name,
|
||||
run_directory: stack.config.run_directory,
|
||||
file_path,
|
||||
contents,
|
||||
})
|
||||
.await
|
||||
.context("Failed to write contents to host")
|
||||
{
|
||||
Ok(log) => {
|
||||
update.logs.push(log);
|
||||
}
|
||||
Err(e) => {
|
||||
update.push_error_log(
|
||||
"Write file contents",
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = State
|
||||
.resolve(
|
||||
RefreshStackCache { stack: stack.id },
|
||||
stack_user().to_owned(),
|
||||
)
|
||||
.await
|
||||
.context(
|
||||
"Failed to refresh stack cache after writing file contents",
|
||||
)
|
||||
{
|
||||
update.push_error_log(
|
||||
"Refresh stack cache",
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
}
|
||||
|
||||
update.finalize();
|
||||
add_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<RefreshStackCache, User> for State {
|
||||
#[instrument(
|
||||
name = "RefreshStackCache",
|
||||
@@ -195,7 +270,7 @@ impl Resolve<RefreshStackCache, User> for State {
|
||||
Ok(res) => res,
|
||||
Err(e) => GetComposeContentsOnHostResponse {
|
||||
contents: Default::default(),
|
||||
errors: vec![ComposeContents {
|
||||
errors: vec![FileContents {
|
||||
path: stack.config.run_directory.clone(),
|
||||
contents: format_serror(&e.into()),
|
||||
}],
|
||||
@@ -226,16 +301,16 @@ impl Resolve<RefreshStackCache, User> for State {
|
||||
// ================
|
||||
// REPO BASED STACK
|
||||
// ================
|
||||
let (
|
||||
remote_contents,
|
||||
remote_errors,
|
||||
_,
|
||||
latest_hash,
|
||||
latest_message,
|
||||
) =
|
||||
let RemoteComposeContents {
|
||||
successful: remote_contents,
|
||||
errored: remote_errors,
|
||||
hash: latest_hash,
|
||||
message: latest_message,
|
||||
..
|
||||
} =
|
||||
get_remote_compose_contents(&stack, Some(&mut missing_files))
|
||||
.await
|
||||
.context("failed to clone remote compose file")?;
|
||||
.await?;
|
||||
|
||||
let project_name = stack.project_name(true);
|
||||
|
||||
let mut services = Vec::new();
|
||||
@@ -298,7 +373,6 @@ impl Resolve<RefreshStackCache, User> for State {
|
||||
.context("failed to serialize stack info to bson")?;
|
||||
|
||||
db_client()
|
||||
.await
|
||||
.stacks
|
||||
.update_one(
|
||||
doc! { "name": &stack.name },
|
||||
@@ -371,7 +445,11 @@ impl Resolve<CreateStackWebhook, User> for State {
|
||||
&stack.config.webhook_secret
|
||||
};
|
||||
|
||||
let host = webhook_base_url.as_ref().unwrap_or(host);
|
||||
let host = if webhook_base_url.is_empty() {
|
||||
host
|
||||
} else {
|
||||
webhook_base_url
|
||||
};
|
||||
let url = match action {
|
||||
StackWebhookAction::Refresh => {
|
||||
format!("{host}/listener/github/stack/{}/refresh", stack.id)
|
||||
@@ -485,7 +563,11 @@ impl Resolve<DeleteStackWebhook, User> for State {
|
||||
..
|
||||
} = core_config();
|
||||
|
||||
let host = webhook_base_url.as_ref().unwrap_or(host);
|
||||
let host = if webhook_base_url.is_empty() {
|
||||
host
|
||||
} else {
|
||||
webhook_base_url
|
||||
};
|
||||
let url = match action {
|
||||
StackWebhookAction::Refresh => {
|
||||
format!("{host}/listener/github/stack/{}/refresh", stack.id)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::collections::HashMap;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use formatting::format_serror;
|
||||
use komodo_client::{
|
||||
api::write::*,
|
||||
api::{read::ExportAllResourcesToToml, write::*},
|
||||
entities::{
|
||||
self,
|
||||
alert::{Alert, AlertData, SeverityLevel},
|
||||
@@ -20,13 +20,11 @@ use komodo_client::{
|
||||
server_template::ServerTemplate,
|
||||
stack::Stack,
|
||||
sync::{
|
||||
PartialResourceSyncConfig, PendingSyncUpdates,
|
||||
PendingSyncUpdatesData, PendingSyncUpdatesDataErr,
|
||||
PendingSyncUpdatesDataOk, ResourceSync,
|
||||
PartialResourceSyncConfig, ResourceSync, ResourceSyncInfo,
|
||||
},
|
||||
ResourceTarget,
|
||||
user::User,
|
||||
NoData,
|
||||
update::Log,
|
||||
user::{sync_user, User},
|
||||
NoData, Operation, ResourceTarget,
|
||||
},
|
||||
};
|
||||
use mungos::{
|
||||
@@ -39,17 +37,18 @@ use octorust::types::{
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
alert::send_alerts,
|
||||
config::core_config,
|
||||
helpers::{
|
||||
alert::send_alerts,
|
||||
query::get_id_to_tags,
|
||||
sync::{
|
||||
deploy::SyncDeployParams,
|
||||
resource::{get_updates_for_view, AllResourcesById},
|
||||
},
|
||||
update::{add_update, make_update, update_update},
|
||||
},
|
||||
resource,
|
||||
resource::{self, refresh_resource_sync_state_cache},
|
||||
state::{db_client, github_client, State},
|
||||
sync::{
|
||||
deploy::SyncDeployParams, remote::RemoteResources,
|
||||
view::push_updates_for_view, AllResourcesById,
|
||||
},
|
||||
};
|
||||
|
||||
impl Resolve<CreateResourceSync, User> for State {
|
||||
@@ -117,21 +116,44 @@ impl Resolve<RefreshResourceSyncPending, User> for State {
|
||||
) -> anyhow::Result<ResourceSync> {
|
||||
// Even though this is a write request, this doesn't change any config. Anyone that can execute the
|
||||
// sync should be able to do this.
|
||||
let sync = resource::get_check_permissions::<
|
||||
let mut sync = resource::get_check_permissions::<
|
||||
entities::sync::ResourceSync,
|
||||
>(&sync, &user, PermissionLevel::Execute)
|
||||
.await?;
|
||||
|
||||
if sync.config.repo.is_empty() {
|
||||
return Err(anyhow!("resource sync repo not configured"));
|
||||
if !sync.config.managed
|
||||
&& !sync.config.files_on_host
|
||||
&& sync.config.file_contents.is_empty()
|
||||
&& sync.config.repo.is_empty()
|
||||
{
|
||||
// Sync not configured, nothing to refresh
|
||||
return Ok(sync);
|
||||
}
|
||||
|
||||
let res = async {
|
||||
let (res, _, hash, message) =
|
||||
crate::helpers::sync::remote::get_remote_resources(&sync)
|
||||
.await
|
||||
.context("failed to get remote resources")?;
|
||||
let resources = res?;
|
||||
let RemoteResources {
|
||||
resources,
|
||||
files,
|
||||
file_errors,
|
||||
hash,
|
||||
message,
|
||||
..
|
||||
} = crate::sync::remote::get_remote_resources(&sync)
|
||||
.await
|
||||
.context("failed to get remote resources")?;
|
||||
|
||||
sync.info.remote_contents = files;
|
||||
sync.info.remote_errors = file_errors;
|
||||
sync.info.pending_hash = hash;
|
||||
sync.info.pending_message = message;
|
||||
|
||||
if !sync.info.remote_errors.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"Remote resources have errors. Cannot compute diffs."
|
||||
));
|
||||
}
|
||||
|
||||
let resources = resources?;
|
||||
|
||||
let id_to_tags = get_id_to_tags(None).await?;
|
||||
let all_resources = AllResourcesById::load().await?;
|
||||
@@ -150,155 +172,182 @@ impl Resolve<RefreshResourceSyncPending, User> for State {
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let deploy_updates =
|
||||
crate::helpers::sync::deploy::get_updates_for_view(
|
||||
SyncDeployParams {
|
||||
deployments: &resources.deployments,
|
||||
deployment_map: &deployments_by_name,
|
||||
stacks: &resources.stacks,
|
||||
stack_map: &stacks_by_name,
|
||||
all_resources: &all_resources,
|
||||
},
|
||||
)
|
||||
crate::sync::deploy::get_updates_for_view(SyncDeployParams {
|
||||
deployments: &resources.deployments,
|
||||
deployment_map: &deployments_by_name,
|
||||
stacks: &resources.stacks,
|
||||
stack_map: &stacks_by_name,
|
||||
all_resources: &all_resources,
|
||||
})
|
||||
.await;
|
||||
|
||||
let data = PendingSyncUpdatesDataOk {
|
||||
server_updates: get_updates_for_view::<Server>(
|
||||
let delete = sync.config.managed || sync.config.delete;
|
||||
|
||||
let mut diffs = Vec::new();
|
||||
|
||||
{
|
||||
push_updates_for_view::<Server>(
|
||||
resources.servers,
|
||||
sync.config.delete,
|
||||
delete,
|
||||
&all_resources,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
)
|
||||
.await
|
||||
.context("failed to get server updates")?,
|
||||
deployment_updates: get_updates_for_view::<Deployment>(
|
||||
resources.deployments,
|
||||
sync.config.delete,
|
||||
&all_resources,
|
||||
&id_to_tags,
|
||||
)
|
||||
.await
|
||||
.context("failed to get deployment updates")?,
|
||||
stack_updates: get_updates_for_view::<Stack>(
|
||||
.await?;
|
||||
push_updates_for_view::<Stack>(
|
||||
resources.stacks,
|
||||
sync.config.delete,
|
||||
delete,
|
||||
&all_resources,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
)
|
||||
.await
|
||||
.context("failed to get stack updates")?,
|
||||
build_updates: get_updates_for_view::<Build>(
|
||||
.await?;
|
||||
push_updates_for_view::<Deployment>(
|
||||
resources.deployments,
|
||||
delete,
|
||||
&all_resources,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
)
|
||||
.await?;
|
||||
push_updates_for_view::<Build>(
|
||||
resources.builds,
|
||||
sync.config.delete,
|
||||
delete,
|
||||
&all_resources,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
)
|
||||
.await
|
||||
.context("failed to get build updates")?,
|
||||
repo_updates: get_updates_for_view::<Repo>(
|
||||
.await?;
|
||||
push_updates_for_view::<Repo>(
|
||||
resources.repos,
|
||||
sync.config.delete,
|
||||
delete,
|
||||
&all_resources,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
)
|
||||
.await
|
||||
.context("failed to get repo updates")?,
|
||||
procedure_updates: get_updates_for_view::<Procedure>(
|
||||
.await?;
|
||||
push_updates_for_view::<Procedure>(
|
||||
resources.procedures,
|
||||
sync.config.delete,
|
||||
delete,
|
||||
&all_resources,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
)
|
||||
.await
|
||||
.context("failed to get procedure updates")?,
|
||||
alerter_updates: get_updates_for_view::<Alerter>(
|
||||
resources.alerters,
|
||||
sync.config.delete,
|
||||
&all_resources,
|
||||
&id_to_tags,
|
||||
)
|
||||
.await
|
||||
.context("failed to get alerter updates")?,
|
||||
builder_updates: get_updates_for_view::<Builder>(
|
||||
.await?;
|
||||
push_updates_for_view::<Builder>(
|
||||
resources.builders,
|
||||
sync.config.delete,
|
||||
delete,
|
||||
&all_resources,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
)
|
||||
.await
|
||||
.context("failed to get builder updates")?,
|
||||
server_template_updates:
|
||||
get_updates_for_view::<ServerTemplate>(
|
||||
resources.server_templates,
|
||||
sync.config.delete,
|
||||
&all_resources,
|
||||
&id_to_tags,
|
||||
)
|
||||
.await
|
||||
.context("failed to get server template updates")?,
|
||||
resource_sync_updates: get_updates_for_view::<
|
||||
entities::sync::ResourceSync,
|
||||
>(
|
||||
.await?;
|
||||
push_updates_for_view::<Alerter>(
|
||||
resources.alerters,
|
||||
delete,
|
||||
&all_resources,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
)
|
||||
.await?;
|
||||
push_updates_for_view::<ServerTemplate>(
|
||||
resources.server_templates,
|
||||
delete,
|
||||
&all_resources,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
)
|
||||
.await?;
|
||||
push_updates_for_view::<ResourceSync>(
|
||||
resources.resource_syncs,
|
||||
sync.config.delete,
|
||||
delete,
|
||||
&all_resources,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
)
|
||||
.await
|
||||
.context("failed to get resource sync updates")?,
|
||||
variable_updates:
|
||||
crate::helpers::sync::variables::get_updates_for_view(
|
||||
resources.variables,
|
||||
sync.config.delete,
|
||||
)
|
||||
.await
|
||||
.context("failed to get variable updates")?,
|
||||
user_group_updates:
|
||||
crate::helpers::sync::user_groups::get_updates_for_view(
|
||||
resources.user_groups,
|
||||
sync.config.delete,
|
||||
&all_resources,
|
||||
)
|
||||
.await
|
||||
.context("failed to get user group updates")?,
|
||||
.await?;
|
||||
}
|
||||
|
||||
let variable_updates =
|
||||
crate::sync::variables::get_updates_for_view(
|
||||
&resources.variables,
|
||||
// Delete doesn't work with variables when match tags are set
|
||||
sync.config.match_tags.is_empty() && delete,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let user_group_updates =
|
||||
crate::sync::user_groups::get_updates_for_view(
|
||||
resources.user_groups,
|
||||
// Delete doesn't work with user groups when match tags are set
|
||||
sync.config.match_tags.is_empty() && delete,
|
||||
&all_resources,
|
||||
)
|
||||
.await?;
|
||||
|
||||
anyhow::Ok((
|
||||
diffs,
|
||||
deploy_updates,
|
||||
};
|
||||
anyhow::Ok((hash, message, data))
|
||||
variable_updates,
|
||||
user_group_updates,
|
||||
))
|
||||
}
|
||||
.await;
|
||||
|
||||
let (pending, has_updates) = match res {
|
||||
Ok((hash, message, data)) => {
|
||||
let has_updates = !data.no_updates();
|
||||
(
|
||||
PendingSyncUpdates {
|
||||
hash: Some(hash),
|
||||
message: Some(message),
|
||||
data: PendingSyncUpdatesData::Ok(data),
|
||||
},
|
||||
has_updates,
|
||||
)
|
||||
}
|
||||
let (
|
||||
resource_updates,
|
||||
deploy_updates,
|
||||
variable_updates,
|
||||
user_group_updates,
|
||||
pending_error,
|
||||
) = match res {
|
||||
Ok(res) => (res.0, res.1, res.2, res.3, None),
|
||||
Err(e) => (
|
||||
PendingSyncUpdates {
|
||||
hash: None,
|
||||
message: None,
|
||||
data: PendingSyncUpdatesData::Err(
|
||||
PendingSyncUpdatesDataErr {
|
||||
message: format_serror(&e.into()),
|
||||
},
|
||||
),
|
||||
},
|
||||
false,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
Some(format_serror(&e.into())),
|
||||
),
|
||||
};
|
||||
|
||||
let pending = to_document(&pending)
|
||||
let has_updates = !resource_updates.is_empty()
|
||||
|| !deploy_updates.to_deploy == 0
|
||||
|| !variable_updates.is_empty()
|
||||
|| !user_group_updates.is_empty();
|
||||
|
||||
let info = ResourceSyncInfo {
|
||||
last_sync_ts: sync.info.last_sync_ts,
|
||||
last_sync_hash: sync.info.last_sync_hash,
|
||||
last_sync_message: sync.info.last_sync_message,
|
||||
remote_contents: sync.info.remote_contents,
|
||||
remote_errors: sync.info.remote_errors,
|
||||
pending_hash: sync.info.pending_hash,
|
||||
pending_message: sync.info.pending_message,
|
||||
pending_deploy: deploy_updates,
|
||||
resource_updates,
|
||||
variable_updates,
|
||||
user_group_updates,
|
||||
pending_error,
|
||||
};
|
||||
|
||||
let info = to_document(&info)
|
||||
.context("failed to serialize pending to document")?;
|
||||
|
||||
update_one_by_id(
|
||||
&db_client().await.resource_syncs,
|
||||
&db_client().resource_syncs,
|
||||
&sync.id,
|
||||
doc! { "$set": { "info.pending": pending } },
|
||||
doc! { "$set": { "info": info } },
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
@@ -307,9 +356,8 @@ impl Resolve<RefreshResourceSyncPending, User> for State {
|
||||
let id = sync.id.clone();
|
||||
let name = sync.name.clone();
|
||||
tokio::task::spawn(async move {
|
||||
let db = db_client().await;
|
||||
let db = db_client();
|
||||
let Some(existing) = db_client()
|
||||
.await
|
||||
.alerts
|
||||
.find_one(doc! {
|
||||
"resolved": false,
|
||||
@@ -370,6 +418,135 @@ impl Resolve<RefreshResourceSyncPending, User> for State {
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<CommitSync, User> for State {
|
||||
#[instrument(name = "CommitSync", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
CommitSync { sync }: CommitSync,
|
||||
user: User,
|
||||
) -> anyhow::Result<ResourceSync> {
|
||||
let sync = resource::get_check_permissions::<
|
||||
entities::sync::ResourceSync,
|
||||
>(&sync, &user, PermissionLevel::Write)
|
||||
.await?;
|
||||
|
||||
let fresh_sync = !sync.config.files_on_host
|
||||
&& sync.config.file_contents.is_empty()
|
||||
&& sync.config.repo.is_empty();
|
||||
|
||||
if !sync.config.managed && !fresh_sync {
|
||||
return Err(anyhow!(
|
||||
"Cannot commit to sync. Enabled 'managed' mode."
|
||||
));
|
||||
}
|
||||
|
||||
let res = State
|
||||
.resolve(
|
||||
ExportAllResourcesToToml {
|
||||
tags: sync.config.match_tags,
|
||||
},
|
||||
sync_user().to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut update = make_update(
|
||||
ResourceTarget::ResourceSync(sync.id),
|
||||
Operation::CommitSync,
|
||||
&user,
|
||||
);
|
||||
update.id = add_update(update.clone()).await?;
|
||||
|
||||
if sync.config.files_on_host {
|
||||
let path = sync
|
||||
.config
|
||||
.resource_path
|
||||
.parse::<PathBuf>()
|
||||
.context("Resource path is not valid file path")?;
|
||||
let extension = path
|
||||
.extension()
|
||||
.context("Resource path missing '.toml' extension")?;
|
||||
if extension != "toml" {
|
||||
return Err(anyhow!("Wrong file extension. Expected '.toml', got '.{extension:?}'"));
|
||||
}
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = tokio::fs::create_dir_all(&parent).await;
|
||||
};
|
||||
if let Err(e) =
|
||||
tokio::fs::write(&sync.config.resource_path, &res.toml)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to write resource file to {}",
|
||||
sync.config.resource_path
|
||||
)
|
||||
})
|
||||
{
|
||||
update.push_error_log(
|
||||
"Write resource file",
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
update.finalize();
|
||||
add_update(update).await?;
|
||||
return resource::get::<ResourceSync>(&sync.name).await;
|
||||
}
|
||||
} else if let Err(e) = db_client()
|
||||
.resource_syncs
|
||||
.update_one(
|
||||
doc! { "name": &sync.name },
|
||||
doc! { "$set": { "config.file_contents": &res.toml } },
|
||||
)
|
||||
.await
|
||||
.context("failed to update file_contents on db")
|
||||
{
|
||||
update.push_error_log(
|
||||
"Write resource to database",
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
update.finalize();
|
||||
add_update(update).await?;
|
||||
return resource::get::<ResourceSync>(&sync.name).await;
|
||||
}
|
||||
|
||||
update
|
||||
.logs
|
||||
.push(Log::simple("Committed resources", res.toml));
|
||||
|
||||
let res = match State
|
||||
.resolve(RefreshResourceSyncPending { sync: sync.name }, user)
|
||||
.await
|
||||
{
|
||||
Ok(sync) => Ok(sync),
|
||||
Err(e) => {
|
||||
update.push_error_log(
|
||||
"Refresh sync pending",
|
||||
format_serror(&(&e).into()),
|
||||
);
|
||||
Err(e)
|
||||
}
|
||||
};
|
||||
|
||||
update.finalize();
|
||||
|
||||
// Need to manually update the update before cache refresh,
|
||||
// and before broadcast with add_update.
|
||||
// The Err case of to_document should be unreachable,
|
||||
// but will fail to update cache in that case.
|
||||
if let Ok(update_doc) = to_document(&update) {
|
||||
let _ = update_one_by_id(
|
||||
&db_client().updates,
|
||||
&update.id,
|
||||
mungos::update::Update::Set(update_doc),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
refresh_resource_sync_state_cache().await;
|
||||
}
|
||||
update_update(update).await?;
|
||||
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<CreateSyncWebhook, User> for State {
|
||||
#[instrument(name = "CreateSyncWebhook", skip(self, user))]
|
||||
async fn resolve(
|
||||
@@ -430,7 +607,11 @@ impl Resolve<CreateSyncWebhook, User> for State {
|
||||
&sync.config.webhook_secret
|
||||
};
|
||||
|
||||
let host = webhook_base_url.as_ref().unwrap_or(host);
|
||||
let host = if webhook_base_url.is_empty() {
|
||||
host
|
||||
} else {
|
||||
webhook_base_url
|
||||
};
|
||||
let url = match action {
|
||||
SyncWebhookAction::Refresh => {
|
||||
format!("{host}/listener/github/sync/{}/refresh", sync.id)
|
||||
@@ -544,7 +725,11 @@ impl Resolve<DeleteSyncWebhook, User> for State {
|
||||
..
|
||||
} = core_config();
|
||||
|
||||
let host = webhook_base_url.as_ref().unwrap_or(host);
|
||||
let host = if webhook_base_url.is_empty() {
|
||||
host
|
||||
} else {
|
||||
webhook_base_url
|
||||
};
|
||||
let url = match action {
|
||||
SyncWebhookAction::Refresh => {
|
||||
format!("{host}/listener/github/sync/{}/refresh", sync.id)
|
||||
|
||||
@@ -44,7 +44,6 @@ impl Resolve<CreateTag, User> for State {
|
||||
};
|
||||
|
||||
tag.id = db_client()
|
||||
.await
|
||||
.tags
|
||||
.insert_one(&tag)
|
||||
.await
|
||||
@@ -72,7 +71,7 @@ impl Resolve<RenameTag, User> for State {
|
||||
get_tag_check_owner(&id, &user).await?;
|
||||
|
||||
update_one_by_id(
|
||||
&db_client().await.tags,
|
||||
&db_client().tags,
|
||||
&id,
|
||||
doc! { "$set": { "name": name } },
|
||||
None,
|
||||
@@ -96,6 +95,7 @@ impl Resolve<DeleteTag, User> for State {
|
||||
tokio::try_join!(
|
||||
resource::remove_tag_from_all::<Server>(&id),
|
||||
resource::remove_tag_from_all::<Deployment>(&id),
|
||||
resource::remove_tag_from_all::<Stack>(&id),
|
||||
resource::remove_tag_from_all::<Build>(&id),
|
||||
resource::remove_tag_from_all::<Repo>(&id),
|
||||
resource::remove_tag_from_all::<Builder>(&id),
|
||||
@@ -104,7 +104,7 @@ impl Resolve<DeleteTag, User> for State {
|
||||
resource::remove_tag_from_all::<ServerTemplate>(&id),
|
||||
)?;
|
||||
|
||||
delete_one_by_id(&db_client().await.tags, &id, None).await?;
|
||||
delete_one_by_id(&db_client().tags, &id, None).await?;
|
||||
|
||||
Ok(tag)
|
||||
}
|
||||
|
||||
130
bin/core/src/api/write/user.rs
Normal file
130
bin/core/src/api/write/user.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use komodo_client::{
|
||||
api::write::{
|
||||
DeleteUser, DeleteUserResponse, UpdateUserPassword,
|
||||
UpdateUserPasswordResponse, UpdateUserUsername,
|
||||
UpdateUserUsernameResponse,
|
||||
},
|
||||
entities::{
|
||||
user::{User, UserConfig},
|
||||
NoData,
|
||||
},
|
||||
};
|
||||
use mungos::mongodb::bson::{doc, oid::ObjectId};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::hash_password,
|
||||
state::{db_client, State},
|
||||
};
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<UpdateUserUsername, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
UpdateUserUsername { username }: UpdateUserUsername,
|
||||
user: User,
|
||||
) -> anyhow::Result<UpdateUserUsernameResponse> {
|
||||
if username.is_empty() {
|
||||
return Err(anyhow!("Username cannot be empty."));
|
||||
}
|
||||
let db = db_client();
|
||||
if db
|
||||
.users
|
||||
.find_one(doc! { "username": &username })
|
||||
.await
|
||||
.context("Failed to query for existing users")?
|
||||
.is_some()
|
||||
{
|
||||
return Err(anyhow!("Username already taken."));
|
||||
}
|
||||
let id = ObjectId::from_str(&user.id)
|
||||
.context("User id not valid ObjectId.")?;
|
||||
db.users
|
||||
.update_one(
|
||||
doc! { "_id": id },
|
||||
doc! { "$set": { "username": username } },
|
||||
)
|
||||
.await
|
||||
.context("Failed to update user username on database.")?;
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<UpdateUserPassword, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
UpdateUserPassword { password }: UpdateUserPassword,
|
||||
user: User,
|
||||
) -> anyhow::Result<UpdateUserPasswordResponse> {
|
||||
let UserConfig::Local { .. } = user.config else {
|
||||
return Err(anyhow!("User is not local user"));
|
||||
};
|
||||
if password.is_empty() {
|
||||
return Err(anyhow!("Password cannot be empty."));
|
||||
}
|
||||
let id = ObjectId::from_str(&user.id)
|
||||
.context("User id not valid ObjectId.")?;
|
||||
let hashed_password = hash_password(password)?;
|
||||
db_client()
|
||||
.users
|
||||
.update_one(
|
||||
doc! { "_id": id },
|
||||
doc! { "$set": {
|
||||
"config.data.password": hashed_password
|
||||
} },
|
||||
)
|
||||
.await
|
||||
.context("Failed to update user password on database.")?;
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<DeleteUser, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
DeleteUser { user }: DeleteUser,
|
||||
admin: User,
|
||||
) -> anyhow::Result<DeleteUserResponse> {
|
||||
if !admin.admin {
|
||||
return Err(anyhow!("Calling user is not admin."));
|
||||
}
|
||||
if admin.username == user || admin.id == user {
|
||||
return Err(anyhow!("User cannot delete themselves."));
|
||||
}
|
||||
let query = if let Ok(id) = ObjectId::from_str(&user) {
|
||||
doc! { "_id": id }
|
||||
} else {
|
||||
doc! { "username": user }
|
||||
};
|
||||
let db = db_client();
|
||||
let Some(user) = db
|
||||
.users
|
||||
.find_one(query.clone())
|
||||
.await
|
||||
.context("Failed to query database for users.")?
|
||||
else {
|
||||
return Err(anyhow!("No user found with given id / username"));
|
||||
};
|
||||
if user.super_admin {
|
||||
return Err(anyhow!("Cannot delete a super admin user."));
|
||||
}
|
||||
if user.admin && !admin.super_admin {
|
||||
return Err(anyhow!(
|
||||
"Only a Super Admin can delete an admin user."
|
||||
));
|
||||
}
|
||||
db.users
|
||||
.delete_one(query)
|
||||
.await
|
||||
.context("Failed to delete user from database")?;
|
||||
Ok(user)
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ impl Resolve<CreateUserGroup, User> for State {
|
||||
updated_at: komodo_timestamp(),
|
||||
name,
|
||||
};
|
||||
let db = db_client().await;
|
||||
let db = db_client();
|
||||
let id = db
|
||||
.user_groups
|
||||
.insert_one(user_group)
|
||||
@@ -59,7 +59,7 @@ impl Resolve<RenameUserGroup, User> for State {
|
||||
if !admin.admin {
|
||||
return Err(anyhow!("This call is admin-only"));
|
||||
}
|
||||
let db = db_client().await;
|
||||
let db = db_client();
|
||||
update_one_by_id(
|
||||
&db.user_groups,
|
||||
&id,
|
||||
@@ -85,7 +85,7 @@ impl Resolve<DeleteUserGroup, User> for State {
|
||||
return Err(anyhow!("This call is admin-only"));
|
||||
}
|
||||
|
||||
let db = db_client().await;
|
||||
let db = db_client();
|
||||
|
||||
let ug = find_one_by_id(&db.user_groups, &id)
|
||||
.await
|
||||
@@ -118,7 +118,7 @@ impl Resolve<AddUserToUserGroup, User> for State {
|
||||
return Err(anyhow!("This call is admin-only"));
|
||||
}
|
||||
|
||||
let db = db_client().await;
|
||||
let db = db_client();
|
||||
|
||||
let filter = match ObjectId::from_str(&user) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
@@ -163,7 +163,7 @@ impl Resolve<RemoveUserFromUserGroup, User> for State {
|
||||
return Err(anyhow!("This call is admin-only"));
|
||||
}
|
||||
|
||||
let db = db_client().await;
|
||||
let db = db_client();
|
||||
|
||||
let filter = match ObjectId::from_str(&user) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
@@ -205,7 +205,7 @@ impl Resolve<SetUsersInUserGroup, User> for State {
|
||||
return Err(anyhow!("This call is admin-only"));
|
||||
}
|
||||
|
||||
let db = db_client().await;
|
||||
let db = db_client();
|
||||
|
||||
let all_users = find_collect(&db.users, None, None)
|
||||
.await
|
||||
|
||||
@@ -46,7 +46,6 @@ impl Resolve<CreateVariable, User> for State {
|
||||
};
|
||||
|
||||
db_client()
|
||||
.await
|
||||
.variables
|
||||
.insert_one(&variable)
|
||||
.await
|
||||
@@ -86,7 +85,6 @@ impl Resolve<UpdateVariableValue, User> for State {
|
||||
}
|
||||
|
||||
db_client()
|
||||
.await
|
||||
.variables
|
||||
.update_one(
|
||||
doc! { "name": &name },
|
||||
@@ -133,7 +131,6 @@ impl Resolve<UpdateVariableDescription, User> for State {
|
||||
return Err(anyhow!("only admins can update variables"));
|
||||
}
|
||||
db_client()
|
||||
.await
|
||||
.variables
|
||||
.update_one(
|
||||
doc! { "name": &name },
|
||||
@@ -156,7 +153,6 @@ impl Resolve<UpdateVariableIsSecret, User> for State {
|
||||
return Err(anyhow!("only admins can update variables"));
|
||||
}
|
||||
db_client()
|
||||
.await
|
||||
.variables
|
||||
.update_one(
|
||||
doc! { "name": &name },
|
||||
@@ -179,7 +175,6 @@ impl Resolve<DeleteVariable, User> for State {
|
||||
}
|
||||
let variable = get_variable(&name).await?;
|
||||
db_client()
|
||||
.await
|
||||
.variables
|
||||
.delete_one(doc! { "name": &name })
|
||||
.await
|
||||
|
||||
@@ -2,11 +2,11 @@ use anyhow::{anyhow, Context};
|
||||
use axum::{
|
||||
extract::Query, response::Redirect, routing::get, Router,
|
||||
};
|
||||
use mongo_indexed::Document;
|
||||
use komodo_client::entities::{
|
||||
komodo_timestamp,
|
||||
user::{User, UserConfig},
|
||||
};
|
||||
use mongo_indexed::Document;
|
||||
use mungos::mongodb::bson::doc;
|
||||
use reqwest::StatusCode;
|
||||
use serde::Deserialize;
|
||||
@@ -64,12 +64,12 @@ async fn callback(
|
||||
let github_user =
|
||||
client.get_github_user(&token.access_token).await?;
|
||||
let github_id = github_user.id.to_string();
|
||||
let db_client = db_client().await;
|
||||
let db_client = db_client();
|
||||
let user = db_client
|
||||
.users
|
||||
.find_one(doc! { "config.data.github_id": &github_id })
|
||||
.await
|
||||
.context("failed at find user query from mongo")?;
|
||||
.context("failed at find user query from database")?;
|
||||
let jwt = match user {
|
||||
Some(user) => jwt_client()
|
||||
.generate(user.id)
|
||||
@@ -78,11 +78,16 @@ async fn callback(
|
||||
let ts = komodo_timestamp();
|
||||
let no_users_exist =
|
||||
db_client.users.find_one(Document::new()).await?.is_none();
|
||||
let core_config = core_config();
|
||||
if !no_users_exist && core_config.disable_user_registration {
|
||||
return Err(anyhow!("User registration is disabled"));
|
||||
}
|
||||
let user = User {
|
||||
id: Default::default(),
|
||||
username: github_user.login,
|
||||
enabled: no_users_exist || core_config().enable_new_users,
|
||||
enabled: no_users_exist || core_config.enable_new_users,
|
||||
admin: no_users_exist,
|
||||
super_admin: no_users_exist,
|
||||
create_server_permissions: no_users_exist,
|
||||
create_build_permissions: no_users_exist,
|
||||
updated_at: ts,
|
||||
|
||||
@@ -3,8 +3,8 @@ use async_timing_util::unix_timestamp_ms;
|
||||
use axum::{
|
||||
extract::Query, response::Redirect, routing::get, Router,
|
||||
};
|
||||
use mongo_indexed::Document;
|
||||
use komodo_client::entities::user::{User, UserConfig};
|
||||
use mongo_indexed::Document;
|
||||
use mungos::mongodb::bson::doc;
|
||||
use reqwest::StatusCode;
|
||||
use serde::Deserialize;
|
||||
@@ -73,7 +73,7 @@ async fn callback(
|
||||
.await?;
|
||||
let google_user = client.get_google_user(&token.id_token)?;
|
||||
let google_id = google_user.id.to_string();
|
||||
let db_client = db_client().await;
|
||||
let db_client = db_client();
|
||||
let user = db_client
|
||||
.users
|
||||
.find_one(doc! { "config.data.google_id": &google_id })
|
||||
@@ -87,6 +87,10 @@ async fn callback(
|
||||
let ts = unix_timestamp_ms() as i64;
|
||||
let no_users_exist =
|
||||
db_client.users.find_one(Document::new()).await?.is_none();
|
||||
let core_config = core_config();
|
||||
if !no_users_exist && core_config.disable_user_registration {
|
||||
return Err(anyhow!("User registration is disabled"));
|
||||
}
|
||||
let user = User {
|
||||
id: Default::default(),
|
||||
username: google_user
|
||||
@@ -96,8 +100,9 @@ async fn callback(
|
||||
.first()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
enabled: no_users_exist || core_config().enable_new_users,
|
||||
enabled: no_users_exist || core_config.enable_new_users,
|
||||
admin: no_users_exist,
|
||||
super_admin: no_users_exist,
|
||||
create_server_permissions: no_users_exist,
|
||||
create_build_permissions: no_users_exist,
|
||||
updated_at: ts,
|
||||
|
||||
@@ -3,7 +3,6 @@ use std::str::FromStr;
|
||||
use anyhow::{anyhow, Context};
|
||||
use async_timing_util::unix_timestamp_ms;
|
||||
use axum::http::HeaderMap;
|
||||
use mongo_indexed::Document;
|
||||
use komodo_client::{
|
||||
api::auth::{
|
||||
CreateLocalUser, CreateLocalUserResponse, LoginLocalUser,
|
||||
@@ -11,17 +10,16 @@ use komodo_client::{
|
||||
},
|
||||
entities::user::{User, UserConfig},
|
||||
};
|
||||
use mongo_indexed::Document;
|
||||
use mungos::mongodb::bson::{doc, oid::ObjectId};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
state::State,
|
||||
state::{db_client, jwt_client},
|
||||
helpers::hash_password,
|
||||
state::{db_client, jwt_client, State},
|
||||
};
|
||||
|
||||
const BCRYPT_COST: u32 = 10;
|
||||
|
||||
impl Resolve<CreateLocalUser, HeaderMap> for State {
|
||||
#[instrument(name = "CreateLocalUser", skip(self))]
|
||||
async fn resolve(
|
||||
@@ -32,30 +30,29 @@ impl Resolve<CreateLocalUser, HeaderMap> for State {
|
||||
let core_config = core_config();
|
||||
|
||||
if !core_config.local_auth {
|
||||
return Err(anyhow!("local auth is not enabled"));
|
||||
return Err(anyhow!("Local auth is not enabled"));
|
||||
}
|
||||
|
||||
if username.is_empty() {
|
||||
return Err(anyhow!("username cannot be empty string"));
|
||||
return Err(anyhow!("Username cannot be empty string"));
|
||||
}
|
||||
|
||||
if ObjectId::from_str(&username).is_ok() {
|
||||
return Err(anyhow!("username cannot be valid ObjectId"));
|
||||
return Err(anyhow!("Username cannot be valid ObjectId"));
|
||||
}
|
||||
|
||||
if password.is_empty() {
|
||||
return Err(anyhow!("password cannot be empty string"));
|
||||
return Err(anyhow!("Password cannot be empty string"));
|
||||
}
|
||||
|
||||
let password = bcrypt::hash(password, BCRYPT_COST)
|
||||
.context("failed to hash password")?;
|
||||
let hashed_password = hash_password(password)?;
|
||||
|
||||
let no_users_exist = db_client()
|
||||
.await
|
||||
.users
|
||||
.find_one(Document::new())
|
||||
.await?
|
||||
.is_none();
|
||||
let no_users_exist =
|
||||
db_client().users.find_one(Document::new()).await?.is_none();
|
||||
|
||||
if !no_users_exist && core_config.disable_user_registration {
|
||||
return Err(anyhow!("User registration is disabled"));
|
||||
}
|
||||
|
||||
let ts = unix_timestamp_ms() as i64;
|
||||
|
||||
@@ -64,17 +61,19 @@ impl Resolve<CreateLocalUser, HeaderMap> for State {
|
||||
username,
|
||||
enabled: no_users_exist || core_config.enable_new_users,
|
||||
admin: no_users_exist,
|
||||
super_admin: no_users_exist,
|
||||
create_server_permissions: no_users_exist,
|
||||
create_build_permissions: no_users_exist,
|
||||
updated_at: ts,
|
||||
last_update_view: 0,
|
||||
recents: Default::default(),
|
||||
all: Default::default(),
|
||||
config: UserConfig::Local { password },
|
||||
config: UserConfig::Local {
|
||||
password: hashed_password,
|
||||
},
|
||||
};
|
||||
|
||||
let user_id = db_client()
|
||||
.await
|
||||
.users
|
||||
.insert_one(user)
|
||||
.await
|
||||
@@ -104,7 +103,6 @@ impl Resolve<LoginLocalUser, HeaderMap> for State {
|
||||
}
|
||||
|
||||
let user = db_client()
|
||||
.await
|
||||
.users
|
||||
.find_one(doc! { "username": &username })
|
||||
.await
|
||||
|
||||
@@ -21,14 +21,15 @@ use self::jwt::JwtClaims;
|
||||
pub mod github;
|
||||
pub mod google;
|
||||
pub mod jwt;
|
||||
pub mod oidc;
|
||||
|
||||
mod local;
|
||||
|
||||
const STATE_PREFIX_LENGTH: usize = 20;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RedirectQuery {
|
||||
pub redirect: Option<String>,
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RedirectQuery {
|
||||
redirect: Option<String>,
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
@@ -116,7 +117,6 @@ pub async fn auth_api_key_get_user_id(
|
||||
secret: &str,
|
||||
) -> anyhow::Result<String> {
|
||||
let key = db_client()
|
||||
.await
|
||||
.api_keys
|
||||
.find_one(doc! { "key": key })
|
||||
.await
|
||||
|
||||
67
bin/core/src/auth/oidc/client.rs
Normal file
67
bin/core/src/auth/oidc/client.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use anyhow::Context;
|
||||
use openidconnect::{
|
||||
core::{CoreClient, CoreProviderMetadata},
|
||||
reqwest::async_http_client,
|
||||
ClientId, ClientSecret, IssuerUrl, RedirectUrl,
|
||||
};
|
||||
|
||||
use crate::config::core_config;
|
||||
|
||||
static DEFAULT_OIDC_CLIENT: OnceLock<Option<CoreClient>> =
|
||||
OnceLock::new();
|
||||
|
||||
pub fn default_oidc_client() -> Option<&'static CoreClient> {
|
||||
DEFAULT_OIDC_CLIENT
|
||||
.get()
|
||||
.expect("OIDC client get before init")
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
pub async fn init_default_oidc_client() {
|
||||
let config = core_config();
|
||||
if !config.oidc_enabled
|
||||
|| config.oidc_provider.is_empty()
|
||||
|| config.oidc_client_id.is_empty()
|
||||
|| config.oidc_client_secret.is_empty()
|
||||
{
|
||||
DEFAULT_OIDC_CLIENT
|
||||
.set(None)
|
||||
.expect("Default OIDC client initialized twice");
|
||||
return;
|
||||
}
|
||||
async {
|
||||
// Use OpenID Connect Discovery to fetch the provider metadata.
|
||||
let provider_metadata = CoreProviderMetadata::discover_async(
|
||||
IssuerUrl::new(config.oidc_provider.clone())?,
|
||||
async_http_client,
|
||||
)
|
||||
.await
|
||||
.context(
|
||||
"Failed to get OIDC /.well-known/openid-configuration",
|
||||
)?;
|
||||
|
||||
// Create an OpenID Connect client by specifying the client ID, client secret, authorization URL
|
||||
// and token URL.
|
||||
let client = CoreClient::from_provider_metadata(
|
||||
provider_metadata,
|
||||
ClientId::new(config.oidc_client_id.to_string()),
|
||||
Some(ClientSecret::new(config.oidc_client_secret.to_string())),
|
||||
)
|
||||
// Set the URL the user will be redirected to after the authorization process.
|
||||
.set_redirect_uri(RedirectUrl::new(format!(
|
||||
"{}/auth/oidc/callback",
|
||||
core_config().host
|
||||
))?);
|
||||
|
||||
DEFAULT_OIDC_CLIENT
|
||||
.set(Some(client))
|
||||
.expect("Default OIDC client initialized twice");
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.await
|
||||
.context("Failed to init default OIDC client")
|
||||
.unwrap();
|
||||
}
|
||||
267
bin/core/src/auth/oidc/mod.rs
Normal file
267
bin/core/src/auth/oidc/mod.rs
Normal file
@@ -0,0 +1,267 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use axum::{
|
||||
extract::Query, response::Redirect, routing::get, Router,
|
||||
};
|
||||
use client::default_oidc_client;
|
||||
use dashmap::DashMap;
|
||||
use komodo_client::entities::{
|
||||
komodo_timestamp,
|
||||
user::{User, UserConfig},
|
||||
};
|
||||
use mungos::mongodb::bson::{doc, Document};
|
||||
use openidconnect::{
|
||||
core::CoreAuthenticationFlow, AccessTokenHash, AuthorizationCode,
|
||||
CsrfToken, Nonce, OAuth2TokenResponse, PkceCodeChallenge,
|
||||
PkceCodeVerifier, Scope, TokenResponse,
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use serde::Deserialize;
|
||||
use serror::AddStatusCode;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
state::{db_client, jwt_client},
|
||||
};
|
||||
|
||||
use super::RedirectQuery;
|
||||
|
||||
pub mod client;
|
||||
|
||||
/// CSRF tokens can only be used once from the callback,
|
||||
/// and must be used within this timeframe
|
||||
const CSRF_VALID_FOR_MS: i64 = 120_000; // 2 minutes for user to log in.
|
||||
|
||||
type RedirectUrl = Option<String>;
|
||||
type CsrfMap =
|
||||
DashMap<String, (PkceCodeVerifier, Nonce, RedirectUrl, i64)>;
|
||||
fn csrf_verifier_tokens() -> &'static CsrfMap {
|
||||
static CSRF: OnceLock<CsrfMap> = OnceLock::new();
|
||||
CSRF.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/login",
|
||||
get(|query| async {
|
||||
login(query).await.status_code(StatusCode::UNAUTHORIZED)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/callback",
|
||||
get(|query| async {
|
||||
callback(query).await.status_code(StatusCode::UNAUTHORIZED)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[instrument(name = "OidcRedirect", level = "debug")]
|
||||
async fn login(
|
||||
Query(RedirectQuery { redirect }): Query<RedirectQuery>,
|
||||
) -> anyhow::Result<Redirect> {
|
||||
let client =
|
||||
default_oidc_client().context("OIDC Client not configured")?;
|
||||
|
||||
// Generate a PKCE challenge.
|
||||
let (pkce_challenge, pkce_verifier) =
|
||||
PkceCodeChallenge::new_random_sha256();
|
||||
|
||||
// Generate the authorization URL.
|
||||
let (auth_url, csrf_token, nonce) = client
|
||||
.authorize_url(
|
||||
CoreAuthenticationFlow::AuthorizationCode,
|
||||
CsrfToken::new_random,
|
||||
Nonce::new_random,
|
||||
)
|
||||
.add_scope(Scope::new("openid".to_string()))
|
||||
.add_scope(Scope::new("email".to_string()))
|
||||
.set_pkce_challenge(pkce_challenge)
|
||||
.url();
|
||||
|
||||
// Data inserted here will be matched on callback side for csrf protection.
|
||||
csrf_verifier_tokens().insert(
|
||||
csrf_token.secret().clone(),
|
||||
(
|
||||
pkce_verifier,
|
||||
nonce,
|
||||
redirect,
|
||||
komodo_timestamp() + CSRF_VALID_FOR_MS,
|
||||
),
|
||||
);
|
||||
|
||||
let config = core_config();
|
||||
let redirect = if !config.oidc_redirect.is_empty() {
|
||||
Redirect::to(
|
||||
auth_url
|
||||
.as_str()
|
||||
.replace(&config.oidc_provider, &config.oidc_redirect)
|
||||
.as_str(),
|
||||
)
|
||||
} else {
|
||||
Redirect::to(auth_url.as_str())
|
||||
};
|
||||
|
||||
Ok(redirect)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CallbackQuery {
|
||||
state: Option<String>,
|
||||
code: Option<String>,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
#[instrument(name = "OidcCallback", level = "debug")]
|
||||
async fn callback(
|
||||
Query(query): Query<CallbackQuery>,
|
||||
) -> anyhow::Result<Redirect> {
|
||||
let client =
|
||||
default_oidc_client().context("OIDC Client not configured")?;
|
||||
|
||||
if let Some(e) = query.error {
|
||||
return Err(anyhow!("Provider returned error: {e}"));
|
||||
}
|
||||
|
||||
let code = query.code.context("Provider did not return code")?;
|
||||
let state = CsrfToken::new(
|
||||
query.state.context("Provider did not return state")?,
|
||||
);
|
||||
|
||||
let (_, (pkce_verifier, nonce, redirect, valid_until)) =
|
||||
csrf_verifier_tokens()
|
||||
.remove(state.secret())
|
||||
.context("CSRF Token invalid")?;
|
||||
|
||||
if komodo_timestamp() > valid_until {
|
||||
return Err(anyhow!(
|
||||
"CSRF token invalid (Timed out). The token must be "
|
||||
));
|
||||
}
|
||||
|
||||
let token_response = client
|
||||
.exchange_code(AuthorizationCode::new(code))
|
||||
// Set the PKCE code verifier.
|
||||
.set_pkce_verifier(pkce_verifier)
|
||||
.request_async(openidconnect::reqwest::async_http_client)
|
||||
.await
|
||||
.context("Failed to get Oauth token")?;
|
||||
|
||||
// Extract the ID token claims after verifying its authenticity and nonce.
|
||||
let id_token = token_response
|
||||
.id_token()
|
||||
.context("OIDC Server did not return an ID token")?;
|
||||
|
||||
// Some providers attach additional audiences, they must be added here
|
||||
// so token verification succeeds.
|
||||
let verifier = client.id_token_verifier();
|
||||
let additional_audiences = &core_config().oidc_additional_audiences;
|
||||
let verifier = if additional_audiences.is_empty() {
|
||||
verifier
|
||||
} else {
|
||||
verifier.set_other_audience_verifier_fn(|aud| {
|
||||
additional_audiences.contains(aud)
|
||||
})
|
||||
};
|
||||
|
||||
let claims = id_token
|
||||
.claims(&verifier, &nonce)
|
||||
.context("Failed to verify token claims")?;
|
||||
|
||||
// Verify the access token hash to ensure that the access token hasn't been substituted for
|
||||
// another user's.
|
||||
if let Some(expected_access_token_hash) = claims.access_token_hash()
|
||||
{
|
||||
let actual_access_token_hash = AccessTokenHash::from_token(
|
||||
token_response.access_token(),
|
||||
&id_token.signing_alg()?,
|
||||
)?;
|
||||
if actual_access_token_hash != *expected_access_token_hash {
|
||||
return Err(anyhow!("Invalid access token"));
|
||||
}
|
||||
}
|
||||
|
||||
let user_id = claims.subject().as_str();
|
||||
|
||||
let db_client = db_client();
|
||||
let user = db_client
|
||||
.users
|
||||
.find_one(doc! {
|
||||
"config.data.provider": &core_config().oidc_provider,
|
||||
"config.data.user_id": user_id
|
||||
})
|
||||
.await
|
||||
.context("failed at find user query from database")?;
|
||||
|
||||
let jwt = match user {
|
||||
Some(user) => jwt_client()
|
||||
.generate(user.id)
|
||||
.context("failed to generate jwt")?,
|
||||
None => {
|
||||
let ts = komodo_timestamp();
|
||||
let no_users_exist =
|
||||
db_client.users.find_one(Document::new()).await?.is_none();
|
||||
let core_config = core_config();
|
||||
if !no_users_exist && core_config.disable_user_registration {
|
||||
return Err(anyhow!("User registration is disabled"));
|
||||
}
|
||||
// Will use preferred_username, then email, then user_id if it isn't available.
|
||||
let username = claims
|
||||
.preferred_username()
|
||||
.map(|username| username.to_string())
|
||||
.unwrap_or_else(|| {
|
||||
let email = claims
|
||||
.email()
|
||||
.map(|email| email.as_str())
|
||||
.unwrap_or(user_id);
|
||||
if core_config.oidc_use_full_email {
|
||||
email
|
||||
} else {
|
||||
email
|
||||
.split_once('@')
|
||||
.map(|(username, _)| username)
|
||||
.unwrap_or(email)
|
||||
}
|
||||
.to_string()
|
||||
});
|
||||
let user = User {
|
||||
id: Default::default(),
|
||||
username,
|
||||
enabled: no_users_exist || core_config.enable_new_users,
|
||||
admin: no_users_exist,
|
||||
super_admin: no_users_exist,
|
||||
create_server_permissions: no_users_exist,
|
||||
create_build_permissions: no_users_exist,
|
||||
updated_at: ts,
|
||||
last_update_view: 0,
|
||||
recents: Default::default(),
|
||||
all: Default::default(),
|
||||
config: UserConfig::Oidc {
|
||||
provider: core_config.oidc_provider.clone(),
|
||||
user_id: user_id.to_string(),
|
||||
},
|
||||
};
|
||||
let user_id = db_client
|
||||
.users
|
||||
.insert_one(user)
|
||||
.await
|
||||
.context("failed to create user on database")?
|
||||
.inserted_id
|
||||
.as_object_id()
|
||||
.context("inserted_id is not ObjectId")?
|
||||
.to_string();
|
||||
jwt_client()
|
||||
.generate(user_id)
|
||||
.context("failed to generate jwt")?
|
||||
}
|
||||
};
|
||||
let exchange_token = jwt_client().create_exchange_token(jwt).await;
|
||||
let redirect_url = if let Some(redirect) = redirect {
|
||||
let splitter = if redirect.contains('?') { '&' } else { '?' };
|
||||
format!("{}{splitter}token={exchange_token}", redirect)
|
||||
} else {
|
||||
format!("{}?token={exchange_token}", core_config().host)
|
||||
};
|
||||
Ok(Redirect::to(&redirect_url))
|
||||
}
|
||||
@@ -19,7 +19,7 @@ use komodo_client::entities::{
|
||||
ResourceTarget,
|
||||
};
|
||||
|
||||
use crate::{config::core_config, helpers::alert::send_alerts};
|
||||
use crate::{alert::send_alerts, config::core_config};
|
||||
|
||||
const POLL_RATE_SECS: u64 = 2;
|
||||
const MAX_POLL_TRIES: usize = 30;
|
||||
@@ -65,6 +65,7 @@ pub async fn launch_ec2_instance(
|
||||
use_public_ip,
|
||||
user_data,
|
||||
port: _,
|
||||
use_https: _,
|
||||
} = config;
|
||||
let instance_type = handle_unknown_instance_type(
|
||||
InstanceType::from(instance_type.as_str()),
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use aws_config::{BehaviorVersion, Region};
|
||||
use aws_sdk_ecr::Client as EcrClient;
|
||||
use run_command::async_run_command;
|
||||
|
||||
#[tracing::instrument(skip(access_key_id, secret_access_key))]
|
||||
async fn make_ecr_client(
|
||||
region: String,
|
||||
access_key_id: &str,
|
||||
secret_access_key: &str,
|
||||
) -> EcrClient {
|
||||
std::env::set_var("AWS_ACCESS_KEY_ID", access_key_id);
|
||||
std::env::set_var("AWS_SECRET_ACCESS_KEY", secret_access_key);
|
||||
let region = Region::new(region);
|
||||
let config = aws_config::defaults(BehaviorVersion::v2024_03_28())
|
||||
.region(region)
|
||||
.load()
|
||||
.await;
|
||||
EcrClient::new(&config)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(access_key_id, secret_access_key))]
|
||||
pub async fn maybe_create_repo(
|
||||
repo: &str,
|
||||
region: String,
|
||||
access_key_id: &str,
|
||||
secret_access_key: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let client =
|
||||
make_ecr_client(region, access_key_id, secret_access_key).await;
|
||||
|
||||
let existing = client
|
||||
.describe_repositories()
|
||||
.send()
|
||||
.await
|
||||
.context("failed to describe existing repositories")?
|
||||
.repositories
|
||||
.unwrap_or_default();
|
||||
|
||||
if existing.iter().any(|r| {
|
||||
if let Some(name) = r.repository_name() {
|
||||
name == repo
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}) {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
client
|
||||
.create_repository()
|
||||
.repository_name(repo)
|
||||
.send()
|
||||
.await
|
||||
.context("failed to create repository")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets a token docker login.
|
||||
///
|
||||
/// Requires the aws cli be installed on the host
|
||||
#[tracing::instrument(skip(access_key_id, secret_access_key))]
|
||||
pub async fn get_ecr_token(
|
||||
region: &str,
|
||||
access_key_id: &str,
|
||||
secret_access_key: &str,
|
||||
) -> anyhow::Result<String> {
|
||||
let log = async_run_command(&format!(
|
||||
"AWS_ACCESS_KEY_ID={access_key_id} AWS_SECRET_ACCESS_KEY={secret_access_key} aws ecr get-login-password --region {region}"
|
||||
))
|
||||
.await;
|
||||
|
||||
if log.success() {
|
||||
Ok(log.stdout)
|
||||
} else {
|
||||
Err(
|
||||
anyhow!("stdout: {} | stderr: {}", log.stdout, log.stderr)
|
||||
.context("failed to get aws ecr login token"),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1 @@
|
||||
pub mod ec2;
|
||||
pub mod ecr;
|
||||
|
||||
@@ -66,6 +66,7 @@ pub async fn launch_hetzner_server(
|
||||
labels,
|
||||
volumes,
|
||||
port: _,
|
||||
use_https: _,
|
||||
} = config;
|
||||
let datacenter = hetzner_datacenter(datacenter);
|
||||
|
||||
|
||||
@@ -1,38 +1,18 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use anyhow::Context;
|
||||
use merge_config_files::parse_config_file;
|
||||
use environment_file::{
|
||||
maybe_read_item_from_file, maybe_read_list_from_file,
|
||||
};
|
||||
use komodo_client::entities::{
|
||||
config::core::{
|
||||
AwsCredentials, CoreConfig, Env, GithubWebhookAppConfig,
|
||||
GithubWebhookAppInstallationConfig, HetznerCredentials,
|
||||
MongoConfig, OauthCredentials,
|
||||
AwsCredentials, CoreConfig, DatabaseConfig, Env,
|
||||
GithubWebhookAppConfig, GithubWebhookAppInstallationConfig,
|
||||
HetznerCredentials, OauthCredentials,
|
||||
},
|
||||
logger::LogConfig,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
pub fn frontend_path() -> &'static String {
|
||||
#[derive(Deserialize)]
|
||||
struct FrontendEnv {
|
||||
#[serde(default = "default_frontend_path")]
|
||||
komodo_frontend_path: String,
|
||||
}
|
||||
|
||||
fn default_frontend_path() -> String {
|
||||
"/frontend".to_string()
|
||||
}
|
||||
|
||||
static FRONTEND_PATH: OnceLock<String> = OnceLock::new();
|
||||
FRONTEND_PATH.get_or_init(|| {
|
||||
let FrontendEnv {
|
||||
komodo_frontend_path,
|
||||
} = envy::from_env()
|
||||
.context("failed to parse FrontendEnv")
|
||||
.unwrap();
|
||||
komodo_frontend_path
|
||||
})
|
||||
}
|
||||
use merge_config_files::parse_config_file;
|
||||
|
||||
pub fn core_config() -> &'static CoreConfig {
|
||||
static CORE_CONFIG: OnceLock<CoreConfig> = OnceLock::new();
|
||||
@@ -50,7 +30,7 @@ pub fn core_config() -> &'static CoreConfig {
|
||||
.unwrap_or_else(|e| {
|
||||
panic!("failed at parsing config at {config_path} | {e:#}")
|
||||
});
|
||||
let installations = match (env.komodo_github_webhook_app_installations_ids, env.komodo_github_webhook_app_installations_namespaces) {
|
||||
let installations = match (maybe_read_list_from_file(env.komodo_github_webhook_app_installations_ids_file,env.komodo_github_webhook_app_installations_ids), env.komodo_github_webhook_app_installations_namespaces) {
|
||||
(Some(ids), Some(namespaces)) => {
|
||||
if ids.len() != namespaces.len() {
|
||||
panic!("KOMODO_GITHUB_WEBHOOK_APP_INSTALLATIONS_IDS length and KOMODO_GITHUB_WEBHOOK_APP_INSTALLATIONS_NAMESPACES length mismatch. Got {ids:?} and {namespaces:?}")
|
||||
@@ -71,35 +51,105 @@ pub fn core_config() -> &'static CoreConfig {
|
||||
config.github_webhook_app.installations
|
||||
}
|
||||
};
|
||||
// recreating CoreConfig here makes sure we apply all env overrides.
|
||||
|
||||
// recreating CoreConfig here makes sure apply all env overrides applied.
|
||||
CoreConfig {
|
||||
// Secret things overridden with file
|
||||
jwt_secret: maybe_read_item_from_file(env.komodo_jwt_secret_file, env.komodo_jwt_secret).unwrap_or(config.jwt_secret),
|
||||
passkey: maybe_read_item_from_file(env.komodo_passkey_file, env.komodo_passkey)
|
||||
.unwrap_or(config.passkey),
|
||||
webhook_secret: maybe_read_item_from_file(env.komodo_webhook_secret_file, env.komodo_webhook_secret)
|
||||
.unwrap_or(config.webhook_secret),
|
||||
database: DatabaseConfig {
|
||||
uri: maybe_read_item_from_file(env.komodo_database_uri_file,env.komodo_database_uri).unwrap_or(config.database.uri),
|
||||
address: env.komodo_database_address.unwrap_or(config.database.address),
|
||||
username: maybe_read_item_from_file(env.komodo_database_username_file,env
|
||||
.komodo_database_username)
|
||||
.unwrap_or(config.database.username),
|
||||
password: maybe_read_item_from_file(env.komodo_database_password_file,env
|
||||
.komodo_database_password)
|
||||
.unwrap_or(config.database.password),
|
||||
app_name: env
|
||||
.komodo_database_app_name
|
||||
.unwrap_or(config.database.app_name),
|
||||
db_name: env
|
||||
.komodo_database_db_name
|
||||
.unwrap_or(config.database.db_name),
|
||||
},
|
||||
oidc_enabled: env.komodo_oidc_enabled.unwrap_or(config.oidc_enabled),
|
||||
oidc_provider: env.komodo_oidc_provider.unwrap_or(config.oidc_provider),
|
||||
oidc_redirect: env.komodo_oidc_redirect.unwrap_or(config.oidc_redirect),
|
||||
oidc_client_id: maybe_read_item_from_file(env.komodo_oidc_client_id_file,env
|
||||
.komodo_oidc_client_id)
|
||||
.unwrap_or(config.oidc_client_id),
|
||||
oidc_client_secret: maybe_read_item_from_file(env.komodo_oidc_client_secret_file,env
|
||||
.komodo_oidc_client_secret)
|
||||
.unwrap_or(config.oidc_client_secret),
|
||||
oidc_use_full_email: env.komodo_oidc_use_full_email
|
||||
.unwrap_or(config.oidc_use_full_email),
|
||||
oidc_additional_audiences: maybe_read_list_from_file(env.komodo_oidc_additional_audiences_file,env
|
||||
.komodo_oidc_additional_audiences)
|
||||
.unwrap_or(config.oidc_additional_audiences),
|
||||
google_oauth: OauthCredentials {
|
||||
enabled: env
|
||||
.komodo_google_oauth_enabled
|
||||
.unwrap_or(config.google_oauth.enabled),
|
||||
id: maybe_read_item_from_file(env.komodo_google_oauth_id_file,env
|
||||
.komodo_google_oauth_id)
|
||||
.unwrap_or(config.google_oauth.id),
|
||||
secret: maybe_read_item_from_file(env.komodo_google_oauth_secret_file,env
|
||||
.komodo_google_oauth_secret)
|
||||
.unwrap_or(config.google_oauth.secret),
|
||||
},
|
||||
github_oauth: OauthCredentials {
|
||||
enabled: env
|
||||
.komodo_github_oauth_enabled
|
||||
.unwrap_or(config.github_oauth.enabled),
|
||||
id: maybe_read_item_from_file(env.komodo_github_oauth_id_file,env
|
||||
.komodo_github_oauth_id)
|
||||
.unwrap_or(config.github_oauth.id),
|
||||
secret: maybe_read_item_from_file(env.komodo_github_oauth_secret_file,env
|
||||
.komodo_github_oauth_secret)
|
||||
.unwrap_or(config.github_oauth.secret),
|
||||
},
|
||||
aws: AwsCredentials {
|
||||
access_key_id: maybe_read_item_from_file(env.komodo_aws_access_key_id_file, env
|
||||
.komodo_aws_access_key_id)
|
||||
.unwrap_or(config.aws.access_key_id),
|
||||
secret_access_key: maybe_read_item_from_file(env.komodo_aws_secret_access_key_file, env
|
||||
.komodo_aws_secret_access_key)
|
||||
.unwrap_or(config.aws.secret_access_key),
|
||||
},
|
||||
hetzner: HetznerCredentials {
|
||||
token: maybe_read_item_from_file(env.komodo_hetzner_token_file, env
|
||||
.komodo_hetzner_token)
|
||||
.unwrap_or(config.hetzner.token),
|
||||
},
|
||||
github_webhook_app: GithubWebhookAppConfig {
|
||||
app_id: maybe_read_item_from_file(env.komodo_github_webhook_app_app_id_file, env
|
||||
.komodo_github_webhook_app_app_id)
|
||||
.unwrap_or(config.github_webhook_app.app_id),
|
||||
pk_path: env
|
||||
.komodo_github_webhook_app_pk_path
|
||||
.unwrap_or(config.github_webhook_app.pk_path),
|
||||
installations,
|
||||
},
|
||||
|
||||
// Non secrets
|
||||
title: env.komodo_title.unwrap_or(config.title),
|
||||
host: env.komodo_host.unwrap_or(config.host),
|
||||
port: env.komodo_port.unwrap_or(config.port),
|
||||
passkey: env.komodo_passkey.unwrap_or(config.passkey),
|
||||
ensure_server: env.komodo_ensure_server.unwrap_or(config.ensure_server),
|
||||
jwt_secret: env.komodo_jwt_secret.unwrap_or(config.jwt_secret),
|
||||
first_server: env.komodo_first_server.unwrap_or(config.first_server),
|
||||
frontend_path: env.komodo_frontend_path.unwrap_or(config.frontend_path),
|
||||
jwt_ttl: env
|
||||
.komodo_jwt_ttl
|
||||
.unwrap_or(config.jwt_ttl),
|
||||
repo_directory: env
|
||||
.komodo_repo_directory
|
||||
.map(|dir|
|
||||
dir.parse()
|
||||
.context("failed to parse env komodo_REPO_DIRECTORY as valid path").unwrap())
|
||||
.unwrap_or(config.repo_directory),
|
||||
stack_poll_interval: env
|
||||
.komodo_stack_poll_interval
|
||||
.unwrap_or(config.stack_poll_interval),
|
||||
sync_poll_interval: env
|
||||
.komodo_sync_poll_interval
|
||||
.unwrap_or(config.sync_poll_interval),
|
||||
build_poll_interval: env
|
||||
.komodo_build_poll_interval
|
||||
.unwrap_or(config.build_poll_interval),
|
||||
repo_poll_interval: env
|
||||
.komodo_repo_poll_interval
|
||||
.unwrap_or(config.repo_poll_interval),
|
||||
resource_poll_interval: env
|
||||
.komodo_resource_poll_interval
|
||||
.unwrap_or(config.resource_poll_interval),
|
||||
monitoring_interval: env
|
||||
.komodo_monitoring_interval
|
||||
.unwrap_or(config.monitoring_interval),
|
||||
@@ -109,81 +159,25 @@ pub fn core_config() -> &'static CoreConfig {
|
||||
keep_alerts_for_days: env
|
||||
.komodo_keep_alerts_for_days
|
||||
.unwrap_or(config.keep_alerts_for_days),
|
||||
webhook_secret: env
|
||||
.komodo_webhook_secret
|
||||
.unwrap_or(config.webhook_secret),
|
||||
webhook_base_url: env
|
||||
.komodo_webhook_base_url
|
||||
.or(config.webhook_base_url),
|
||||
.unwrap_or(config.webhook_base_url),
|
||||
transparent_mode: env
|
||||
.komodo_transparent_mode
|
||||
.unwrap_or(config.transparent_mode),
|
||||
ui_write_disabled: env
|
||||
.komodo_ui_write_disabled
|
||||
.unwrap_or(config.ui_write_disabled),
|
||||
disable_confirm_dialog: env.komodo_disable_confirm_dialog
|
||||
.unwrap_or(config.disable_confirm_dialog),
|
||||
enable_new_users: env.komodo_enable_new_users
|
||||
.unwrap_or(config.enable_new_users),
|
||||
local_auth: env.komodo_local_auth.unwrap_or(config.local_auth),
|
||||
google_oauth: OauthCredentials {
|
||||
enabled: env
|
||||
.komodo_google_oauth_enabled
|
||||
.unwrap_or(config.google_oauth.enabled),
|
||||
id: env
|
||||
.komodo_google_oauth_id
|
||||
.unwrap_or(config.google_oauth.id),
|
||||
secret: env
|
||||
.komodo_google_oauth_secret
|
||||
.unwrap_or(config.google_oauth.secret),
|
||||
},
|
||||
github_oauth: OauthCredentials {
|
||||
enabled: env
|
||||
.komodo_github_oauth_enabled
|
||||
.unwrap_or(config.github_oauth.enabled),
|
||||
id: env
|
||||
.komodo_github_oauth_id
|
||||
.unwrap_or(config.github_oauth.id),
|
||||
secret: env
|
||||
.komodo_github_oauth_secret
|
||||
.unwrap_or(config.github_oauth.secret),
|
||||
},
|
||||
github_webhook_app: GithubWebhookAppConfig {
|
||||
app_id: env
|
||||
.komodo_github_webhook_app_app_id
|
||||
.unwrap_or(config.github_webhook_app.app_id),
|
||||
pk_path: env
|
||||
.komodo_github_webhook_app_pk_path
|
||||
.unwrap_or(config.github_webhook_app.pk_path),
|
||||
installations,
|
||||
},
|
||||
aws: AwsCredentials {
|
||||
access_key_id: env
|
||||
.komodo_aws_access_key_id
|
||||
.unwrap_or(config.aws.access_key_id),
|
||||
secret_access_key: env
|
||||
.komodo_aws_secret_access_key
|
||||
.unwrap_or(config.aws.secret_access_key),
|
||||
},
|
||||
hetzner: HetznerCredentials {
|
||||
token: env
|
||||
.komodo_hetzner_token
|
||||
.unwrap_or(config.hetzner.token),
|
||||
},
|
||||
mongo: MongoConfig {
|
||||
uri: env.komodo_mongo_uri.or(config.mongo.uri),
|
||||
address: env.komodo_mongo_address.or(config.mongo.address),
|
||||
username: env
|
||||
.komodo_mongo_username
|
||||
.or(config.mongo.username),
|
||||
password: env
|
||||
.komodo_mongo_password
|
||||
.or(config.mongo.password),
|
||||
app_name: env
|
||||
.komodo_mongo_app_name
|
||||
.unwrap_or(config.mongo.app_name),
|
||||
db_name: env
|
||||
.komodo_mongo_db_name
|
||||
.unwrap_or(config.mongo.db_name),
|
||||
},
|
||||
disable_user_registration: env.komodo_disable_user_registration
|
||||
.unwrap_or(config.disable_user_registration),
|
||||
disable_non_admin_create: env.komodo_disable_non_admin_create
|
||||
.unwrap_or(config.disable_non_admin_create),
|
||||
local_auth: env.komodo_local_auth
|
||||
.unwrap_or(config.local_auth),
|
||||
logging: LogConfig {
|
||||
level: env
|
||||
.komodo_logging_level
|
||||
@@ -193,17 +187,19 @@ pub fn core_config() -> &'static CoreConfig {
|
||||
.unwrap_or(config.logging.stdio),
|
||||
otlp_endpoint: env
|
||||
.komodo_logging_otlp_endpoint
|
||||
.or(config.logging.otlp_endpoint),
|
||||
.unwrap_or(config.logging.otlp_endpoint),
|
||||
opentelemetry_service_name: env
|
||||
.komodo_logging_opentelemetry_service_name
|
||||
.unwrap_or(config.logging.opentelemetry_service_name),
|
||||
},
|
||||
ssl_enabled: env.komodo_ssl_enabled.unwrap_or(config.ssl_enabled),
|
||||
ssl_key_file: env.komodo_ssl_key_file.unwrap_or(config.ssl_key_file),
|
||||
ssl_cert_file: env.komodo_ssl_cert_file.unwrap_or(config.ssl_cert_file),
|
||||
|
||||
// These can't be overridden on env
|
||||
secrets: config.secrets,
|
||||
git_providers: config.git_providers,
|
||||
docker_registries: config.docker_registries,
|
||||
aws_ecr_registries: config.aws_ecr_registries,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use mongo_indexed::{create_index, create_unique_index};
|
||||
use komodo_client::entities::{
|
||||
alert::Alert,
|
||||
alerter::Alerter,
|
||||
api_key::ApiKey,
|
||||
build::Build,
|
||||
builder::Builder,
|
||||
config::core::MongoConfig,
|
||||
config::core::DatabaseConfig,
|
||||
deployment::Deployment,
|
||||
permission::Permission,
|
||||
procedure::Procedure,
|
||||
@@ -22,11 +21,13 @@ use komodo_client::entities::{
|
||||
user_group::UserGroup,
|
||||
variable::Variable,
|
||||
};
|
||||
use mongo_indexed::{create_index, create_unique_index};
|
||||
use mungos::{
|
||||
init::MongoBuilder,
|
||||
mongodb::{Collection, Database},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DbClient {
|
||||
pub users: Collection<User>,
|
||||
pub user_groups: Collection<UserGroup>,
|
||||
@@ -56,28 +57,33 @@ pub struct DbClient {
|
||||
|
||||
impl DbClient {
|
||||
pub async fn new(
|
||||
MongoConfig {
|
||||
DatabaseConfig {
|
||||
uri,
|
||||
address,
|
||||
username,
|
||||
password,
|
||||
app_name,
|
||||
db_name,
|
||||
}: &MongoConfig,
|
||||
}: &DatabaseConfig,
|
||||
) -> anyhow::Result<DbClient> {
|
||||
let mut client = MongoBuilder::default().app_name(app_name);
|
||||
|
||||
match (uri, address, username, password) {
|
||||
(Some(uri), _, _, _) => {
|
||||
match (
|
||||
!uri.is_empty(),
|
||||
!address.is_empty(),
|
||||
!username.is_empty(),
|
||||
!password.is_empty(),
|
||||
) {
|
||||
(true, _, _, _) => {
|
||||
client = client.uri(uri);
|
||||
}
|
||||
(_, Some(address), Some(username), Some(password)) => {
|
||||
(_, true, true, true) => {
|
||||
client = client
|
||||
.address(address)
|
||||
.username(username)
|
||||
.password(password);
|
||||
}
|
||||
(_, Some(address), _, _) => {
|
||||
(_, true, _, _) => {
|
||||
client = client.address(address);
|
||||
}
|
||||
_ => {
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
use async_timing_util::{wait_until_timelength, Timelength};
|
||||
use komodo_client::{
|
||||
api::write::RefreshBuildCache, entities::user::build_user,
|
||||
};
|
||||
use mungos::find::find_collect;
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
state::{db_client, State},
|
||||
};
|
||||
|
||||
pub fn spawn_build_refresh_loop() {
|
||||
let interval: Timelength = core_config()
|
||||
.build_poll_interval
|
||||
.try_into()
|
||||
.expect("Invalid build poll interval");
|
||||
tokio::spawn(async move {
|
||||
refresh_builds().await;
|
||||
loop {
|
||||
wait_until_timelength(interval, 2000).await;
|
||||
refresh_builds().await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn refresh_builds() {
|
||||
let Ok(builds) =
|
||||
find_collect(&db_client().await.builds, None, None)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
warn!("failed to get builds from db in refresh task | {e:#}")
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
for build in builds {
|
||||
State
|
||||
.resolve(
|
||||
RefreshBuildCache { build: build.id },
|
||||
build_user().clone(),
|
||||
)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
warn!("failed to refresh build cache in refresh task | build: {} | {e:#}", build.name)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
@@ -93,7 +93,9 @@ async fn get_aws_builder(
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let periphery_address = format!("http://{ip}:{}", config.port);
|
||||
let protocol = if config.use_https { "https" } else { "http" };
|
||||
let periphery_address =
|
||||
format!("{protocol}://{ip}:{}", config.port);
|
||||
let periphery =
|
||||
PeripheryClient::new(&periphery_address, &core_config().passkey);
|
||||
|
||||
@@ -191,6 +193,7 @@ pub fn start_aws_builder_log(
|
||||
assign_public_ip,
|
||||
security_group_ids,
|
||||
use_public_ip,
|
||||
use_https,
|
||||
..
|
||||
} = config;
|
||||
|
||||
@@ -206,6 +209,7 @@ pub fn start_aws_builder_log(
|
||||
format!("{}: {readable_sec_group_ids}", muted("security groups")),
|
||||
format!("{}: {assign_public_ip}", muted("assign public ip")),
|
||||
format!("{}: {use_public_ip}", muted("use public ip")),
|
||||
format!("{}: {use_https}", muted("use https")),
|
||||
]
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
@@ -1,60 +1,10 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use anyhow::Context;
|
||||
use komodo_client::entities::{
|
||||
update::Update, EnvironmentVar, SystemCommand,
|
||||
};
|
||||
use komodo_client::entities::{update::Update, SystemCommand};
|
||||
|
||||
use super::query::VariablesAndSecrets;
|
||||
|
||||
pub fn interpolate_variables_secrets_into_environment(
|
||||
VariablesAndSecrets { variables, secrets }: &VariablesAndSecrets,
|
||||
environment: &mut Vec<EnvironmentVar>,
|
||||
global_replacers: &mut HashSet<(String, String)>,
|
||||
secret_replacers: &mut HashSet<(String, String)>,
|
||||
) -> anyhow::Result<()> {
|
||||
for env in environment {
|
||||
if env.value.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// first pass - global variables
|
||||
let (res, more_replacers) = svi::interpolate_variables(
|
||||
&env.value,
|
||||
variables,
|
||||
svi::Interpolator::DoubleBrackets,
|
||||
false,
|
||||
)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to interpolate global variables into env var '{}'",
|
||||
env.variable
|
||||
)
|
||||
})?;
|
||||
global_replacers.extend(more_replacers);
|
||||
|
||||
// second pass - core secrets
|
||||
let (res, more_replacers) = svi::interpolate_variables(
|
||||
&res,
|
||||
secrets,
|
||||
svi::Interpolator::DoubleBrackets,
|
||||
false,
|
||||
)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to interpolate core secrets into env var '{}'",
|
||||
env.variable
|
||||
)
|
||||
})?;
|
||||
secret_replacers.extend(more_replacers);
|
||||
|
||||
// set env value with the result
|
||||
env.value = res;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn interpolate_variables_secrets_into_extra_args(
|
||||
VariablesAndSecrets { variables, secrets }: &VariablesAndSecrets,
|
||||
extra_args: &mut Vec<String>,
|
||||
@@ -101,28 +51,24 @@ pub fn interpolate_variables_secrets_into_extra_args(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn interpolate_variables_secrets_into_container_command(
|
||||
pub fn interpolate_variables_secrets_into_string(
|
||||
VariablesAndSecrets { variables, secrets }: &VariablesAndSecrets,
|
||||
command: &mut String,
|
||||
target: &mut String,
|
||||
global_replacers: &mut HashSet<(String, String)>,
|
||||
secret_replacers: &mut HashSet<(String, String)>,
|
||||
) -> anyhow::Result<()> {
|
||||
if command.is_empty() {
|
||||
if target.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// first pass - global variables
|
||||
let (res, more_replacers) = svi::interpolate_variables(
|
||||
command,
|
||||
target,
|
||||
variables,
|
||||
svi::Interpolator::DoubleBrackets,
|
||||
false,
|
||||
)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to interpolate global variables into command '{command}'",
|
||||
)
|
||||
})?;
|
||||
.context("Failed to interpolate core variables")?;
|
||||
global_replacers.extend(more_replacers);
|
||||
|
||||
// second pass - core secrets
|
||||
@@ -132,15 +78,11 @@ pub fn interpolate_variables_secrets_into_container_command(
|
||||
svi::Interpolator::DoubleBrackets,
|
||||
false,
|
||||
)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to interpolate core secrets into command '{command}'",
|
||||
)
|
||||
})?;
|
||||
.context("Failed to interpolate core secrets")?;
|
||||
secret_replacers.extend(more_replacers);
|
||||
|
||||
// set command with the result
|
||||
*command = res;
|
||||
*target = res;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ use std::{str::FromStr, time::Duration};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use futures::future::join_all;
|
||||
use mongo_indexed::Document;
|
||||
use komodo_client::{
|
||||
api::write::CreateServer,
|
||||
api::write::{CreateBuilder, CreateServer},
|
||||
entities::{
|
||||
builder::{PartialBuilderConfig, PartialServerBuilderConfig},
|
||||
komodo_timestamp,
|
||||
permission::{Permission, PermissionLevel, UserTarget},
|
||||
server::{PartialServerConfig, Server},
|
||||
@@ -15,6 +15,7 @@ use komodo_client::{
|
||||
ResourceTarget,
|
||||
},
|
||||
};
|
||||
use mongo_indexed::Document;
|
||||
use mungos::{
|
||||
find::find_collect,
|
||||
mongodb::bson::{doc, oid::ObjectId, to_document, Bson},
|
||||
@@ -30,8 +31,6 @@ use crate::{
|
||||
};
|
||||
|
||||
pub mod action_state;
|
||||
pub mod alert;
|
||||
pub mod build;
|
||||
pub mod builder;
|
||||
pub mod cache;
|
||||
pub mod channel;
|
||||
@@ -39,9 +38,6 @@ pub mod interpolate;
|
||||
pub mod procedure;
|
||||
pub mod prune;
|
||||
pub mod query;
|
||||
pub mod repo;
|
||||
pub mod stack;
|
||||
pub mod sync;
|
||||
pub mod update;
|
||||
|
||||
// pub mod resource;
|
||||
@@ -70,6 +66,15 @@ pub fn random_string(length: usize) -> String {
|
||||
.collect()
|
||||
}
|
||||
|
||||
const BCRYPT_COST: u32 = 10;
|
||||
pub fn hash_password<P>(password: P) -> anyhow::Result<String>
|
||||
where
|
||||
P: AsRef<[u8]>,
|
||||
{
|
||||
bcrypt::hash(password, BCRYPT_COST)
|
||||
.context("failed to hash password")
|
||||
}
|
||||
|
||||
/// First checks db for token, then checks core config.
|
||||
/// Only errors if db call errors.
|
||||
/// Returns (token, use_https)
|
||||
@@ -79,7 +84,6 @@ pub async fn git_token(
|
||||
mut on_https_found: impl FnMut(bool),
|
||||
) -> anyhow::Result<Option<String>> {
|
||||
let db_provider = db_client()
|
||||
.await
|
||||
.git_accounts
|
||||
.find_one(doc! { "domain": provider_domain, "username": account_username })
|
||||
.await
|
||||
@@ -111,7 +115,6 @@ pub async fn registry_token(
|
||||
account_username: &str,
|
||||
) -> anyhow::Result<Option<String>> {
|
||||
let provider = db_client()
|
||||
.await
|
||||
.registry_accounts
|
||||
.find_one(doc! { "domain": provider_domain, "username": account_username })
|
||||
.await
|
||||
@@ -134,34 +137,6 @@ pub async fn registry_token(
|
||||
)
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn remove_from_recently_viewed<T>(resource: T)
|
||||
where
|
||||
T: Into<ResourceTarget> + std::fmt::Debug,
|
||||
{
|
||||
let resource: ResourceTarget = resource.into();
|
||||
let (ty, id) = resource.extract_variant_id();
|
||||
if let Err(e) = db_client()
|
||||
.await
|
||||
.users
|
||||
.update_many(
|
||||
doc! {},
|
||||
doc! {
|
||||
"$pull": {
|
||||
"recently_viewed": {
|
||||
"type": ty.to_string(),
|
||||
"id": id,
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("failed to remove resource from users recently viewed")
|
||||
{
|
||||
warn!("{e:#}");
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
pub fn periphery_client(
|
||||
@@ -193,7 +168,6 @@ pub async fn create_permission<T>(
|
||||
}
|
||||
let target: ResourceTarget = target.into();
|
||||
if let Err(e) = db_client()
|
||||
.await
|
||||
.permissions
|
||||
.insert_one(Permission {
|
||||
id: Default::default(),
|
||||
@@ -243,7 +217,6 @@ async fn startup_in_progress_update_cleanup() {
|
||||
// This static log won't fail to serialize, unwrap ok.
|
||||
let log = to_document(&log).unwrap();
|
||||
if let Err(e) = db_client()
|
||||
.await
|
||||
.updates
|
||||
.update_many(
|
||||
doc! { "status": "InProgress" },
|
||||
@@ -265,7 +238,7 @@ async fn startup_in_progress_update_cleanup() {
|
||||
|
||||
/// Run on startup, ensure open alerts pointing to invalid resources are closed.
|
||||
async fn startup_open_alert_cleanup() {
|
||||
let db = db_client().await;
|
||||
let db = db_client();
|
||||
let Ok(alerts) =
|
||||
find_collect(&db.alerts, doc! { "resolved": false }, None)
|
||||
.await
|
||||
@@ -317,39 +290,64 @@ async fn startup_open_alert_cleanup() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensures a default server exists with the defined address
|
||||
pub async fn ensure_server() {
|
||||
let ensure_server = &core_config().ensure_server;
|
||||
if ensure_server.is_empty() {
|
||||
/// Ensures a default server / builder exists with the defined address
|
||||
pub async fn ensure_first_server_and_builder() {
|
||||
let first_server = &core_config().first_server;
|
||||
if first_server.is_empty() {
|
||||
return;
|
||||
}
|
||||
let db = db_client().await;
|
||||
let db = db_client();
|
||||
let Ok(server) = db
|
||||
.servers
|
||||
.find_one(doc! { "config.address": ensure_server })
|
||||
.find_one(Document::new())
|
||||
.await
|
||||
.inspect_err(|e| error!("Failed to initialize 'ensure_server'. Failed to query db. {e:?}"))
|
||||
.inspect_err(|e| error!("Failed to initialize 'first_server'. Failed to query db. {e:?}"))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if server.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let server = if let Some(server) = server {
|
||||
server
|
||||
} else {
|
||||
match State
|
||||
.resolve(
|
||||
CreateServer {
|
||||
name: format!("server-{}", random_string(5)),
|
||||
config: PartialServerConfig {
|
||||
address: Some(first_server.to_string()),
|
||||
enabled: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
system_user().to_owned(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(server) => server,
|
||||
Err(e) => {
|
||||
error!("Failed to initialize 'first_server'. Failed to CreateServer. {e:?}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
let Ok(None) = db.builders
|
||||
.find_one(Document::new()).await
|
||||
.inspect_err(|e| error!("Failed to initialize 'first_builder'. Failed to query db. {e:?}")) else {
|
||||
return;
|
||||
};
|
||||
if let Err(e) = State
|
||||
.resolve(
|
||||
CreateServer {
|
||||
name: format!("server-{}", random_string(5)),
|
||||
config: PartialServerConfig {
|
||||
address: Some(ensure_server.to_string()),
|
||||
enabled: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
CreateBuilder {
|
||||
name: String::from("local"),
|
||||
config: PartialBuilderConfig::Server(
|
||||
PartialServerBuilderConfig {
|
||||
server_id: Some(server.id),
|
||||
},
|
||||
),
|
||||
},
|
||||
system_user().to_owned(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("Failed to initialize 'ensure_server'. Failed to CreateServer. {e:?}");
|
||||
error!("Failed to initialize 'first_builder'. Failed to CreateBuilder. {e:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ pub async fn execute_procedure(
|
||||
add_line_to_update(
|
||||
update,
|
||||
&format!(
|
||||
"{}: executing stage: '{}'",
|
||||
"{}: Executing stage: '{}'",
|
||||
muted("INFO"),
|
||||
bold(&stage.name)
|
||||
),
|
||||
@@ -55,7 +55,7 @@ pub async fn execute_procedure(
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed stage '{}' execution after {:?}",
|
||||
"Failed stage '{}' execution after {:?}",
|
||||
bold(&stage.name),
|
||||
timer.elapsed(),
|
||||
)
|
||||
@@ -65,7 +65,7 @@ pub async fn execute_procedure(
|
||||
&format!(
|
||||
"{}: {} stage '{}' execution in {:?}",
|
||||
muted("INFO"),
|
||||
colored("finished", Color::Green),
|
||||
colored("Finished", Color::Green),
|
||||
bold(&stage.name),
|
||||
timer.elapsed()
|
||||
),
|
||||
@@ -76,6 +76,7 @@ pub async fn execute_procedure(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dependency_on_unit_never_type_fallback)]
|
||||
#[instrument(skip(update))]
|
||||
async fn execute_stage(
|
||||
executions: Vec<Execution>,
|
||||
@@ -87,11 +88,11 @@ async fn execute_stage(
|
||||
let now = Instant::now();
|
||||
add_line_to_update(
|
||||
update,
|
||||
&format!("{}: executing: {execution:?}", muted("INFO")),
|
||||
&format!("{}: Executing: {execution:?}", muted("INFO")),
|
||||
)
|
||||
.await;
|
||||
let fail_log = format!(
|
||||
"{}: failed on {execution:?}",
|
||||
"{}: Failed on {execution:?}",
|
||||
colored("ERROR", Color::Red)
|
||||
);
|
||||
let res =
|
||||
@@ -103,7 +104,7 @@ async fn execute_stage(
|
||||
&format!(
|
||||
"{}: {} execution in {:?}: {execution:?}",
|
||||
muted("INFO"),
|
||||
colored("finished", Color::Green),
|
||||
colored("Finished", Color::Green),
|
||||
now.elapsed()
|
||||
),
|
||||
)
|
||||
@@ -140,7 +141,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at RunProcedure"),
|
||||
.context("Failed at RunProcedure"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -156,7 +157,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at RunBuild"),
|
||||
.context("Failed at RunBuild"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -172,7 +173,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at CancelBuild"),
|
||||
.context("Failed at CancelBuild"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -188,7 +189,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at Deploy"),
|
||||
.context("Failed at Deploy"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -204,7 +205,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at StartDeployment"),
|
||||
.context("Failed at StartDeployment"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -220,7 +221,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at RestartDeployment"),
|
||||
.context("Failed at RestartDeployment"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -236,7 +237,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at PauseDeployment"),
|
||||
.context("Failed at PauseDeployment"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -252,7 +253,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at UnpauseDeployment"),
|
||||
.context("Failed at UnpauseDeployment"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -268,7 +269,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at StopDeployment"),
|
||||
.context("Failed at StopDeployment"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -284,7 +285,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at RemoveDeployment"),
|
||||
.context("Failed at RemoveDeployment"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -300,7 +301,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at CloneRepo"),
|
||||
.context("Failed at CloneRepo"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -316,7 +317,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at PullRepo"),
|
||||
.context("Failed at PullRepo"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -332,7 +333,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at BuildRepo"),
|
||||
.context("Failed at BuildRepo"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -348,7 +349,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at CancelRepoBuild"),
|
||||
.context("Failed at CancelRepoBuild"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -364,7 +365,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at StartContainer"),
|
||||
.context("Failed at StartContainer"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -380,7 +381,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at RestartContainer"),
|
||||
.context("Failed at RestartContainer"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -396,7 +397,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at PauseContainer"),
|
||||
.context("Failed at PauseContainer"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -412,7 +413,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at UnpauseContainer"),
|
||||
.context("Failed at UnpauseContainer"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -428,7 +429,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at StopContainer"),
|
||||
.context("Failed at StopContainer"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -444,7 +445,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at RemoveContainer"),
|
||||
.context("Failed at RemoveContainer"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -460,7 +461,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at StartAllContainers"),
|
||||
.context("Failed at StartAllContainers"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -476,7 +477,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at RestartAllContainers"),
|
||||
.context("Failed at RestartAllContainers"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -492,7 +493,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at PauseAllContainers"),
|
||||
.context("Failed at PauseAllContainers"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -508,7 +509,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at UnpauseAllContainers"),
|
||||
.context("Failed at UnpauseAllContainers"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -524,7 +525,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at StopAllContainers"),
|
||||
.context("Failed at StopAllContainers"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -540,7 +541,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at PruneContainers"),
|
||||
.context("Failed at PruneContainers"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -556,7 +557,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at DeleteNetwork"),
|
||||
.context("Failed at DeleteNetwork"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -572,7 +573,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at PruneNetworks"),
|
||||
.context("Failed at PruneNetworks"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -588,7 +589,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at DeleteImage"),
|
||||
.context("Failed at DeleteImage"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -604,7 +605,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at PruneImages"),
|
||||
.context("Failed at PruneImages"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -620,7 +621,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at DeleteVolume"),
|
||||
.context("Failed at DeleteVolume"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -636,7 +637,39 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at PruneVolumes"),
|
||||
.context("Failed at PruneVolumes"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
Execution::PruneDockerBuilders(req) => {
|
||||
let req = ExecuteRequest::PruneDockerBuilders(req);
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::PruneDockerBuilders(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
let update_id = update.id.clone();
|
||||
handle_resolve_result(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("Failed at PruneDockerBuilders"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
Execution::PruneBuildx(req) => {
|
||||
let req = ExecuteRequest::PruneBuildx(req);
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::PruneBuildx(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
let update_id = update.id.clone();
|
||||
handle_resolve_result(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("Failed at PruneBuildx"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -652,7 +685,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at PruneSystem"),
|
||||
.context("Failed at PruneSystem"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -668,7 +701,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at RunSync"),
|
||||
.context("Failed at RunSync"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -684,7 +717,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at DeployStack"),
|
||||
.context("Failed at DeployStack"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -700,7 +733,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at StartStack"),
|
||||
.context("Failed at StartStack"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -716,7 +749,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at RestartStack"),
|
||||
.context("Failed at RestartStack"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -732,7 +765,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at PauseStack"),
|
||||
.context("Failed at PauseStack"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -748,7 +781,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at UnpauseStack"),
|
||||
.context("Failed at UnpauseStack"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -764,7 +797,7 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at StopStack"),
|
||||
.context("Failed at StopStack"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -780,16 +813,14 @@ async fn execute_execution(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("failed at DestroyStack"),
|
||||
.context("Failed at DestroyStack"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
Execution::Sleep(req) => {
|
||||
tokio::time::sleep(Duration::from_millis(
|
||||
req.duration_ms as u64,
|
||||
))
|
||||
.await;
|
||||
let duration = Duration::from_millis(req.duration_ms as u64);
|
||||
tokio::time::sleep(duration).await;
|
||||
Update {
|
||||
success: true,
|
||||
..Default::default()
|
||||
@@ -819,9 +850,9 @@ async fn handle_resolve_result(
|
||||
let log =
|
||||
Log::error("execution error", format_serror(&e.into()));
|
||||
let mut update =
|
||||
find_one_by_id(&db_client().await.updates, update_id)
|
||||
find_one_by_id(&db_client().updates, update_id)
|
||||
.await
|
||||
.context("failed to query to db")?
|
||||
.context("Failed to query to db")?
|
||||
.context("no update exists with given id")?;
|
||||
update.logs.push(log);
|
||||
update.finalize();
|
||||
@@ -841,6 +872,6 @@ async fn add_line_to_update(update: &Mutex<Update>, line: &str) {
|
||||
let update = lock.clone();
|
||||
drop(lock);
|
||||
if let Err(e) = update_update(update).await {
|
||||
error!("failed to update an update during procedure | {e:#}");
|
||||
error!("Failed to update an update during procedure | {e:#}");
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ pub fn spawn_prune_loop() {
|
||||
}
|
||||
|
||||
async fn prune_images() -> anyhow::Result<()> {
|
||||
let futures = find_collect(&db_client().await.servers, None, None)
|
||||
let futures = find_collect(&db_client().servers, None, None)
|
||||
.await
|
||||
.context("failed to get servers from db")?
|
||||
.into_iter()
|
||||
@@ -66,13 +66,14 @@ async fn prune_stats() -> anyhow::Result<()> {
|
||||
- core_config().keep_stats_for_days as u128 * ONE_DAY_MS)
|
||||
as i64;
|
||||
let res = db_client()
|
||||
.await
|
||||
.stats
|
||||
.delete_many(doc! {
|
||||
"ts": { "$lt": delete_before_ts }
|
||||
})
|
||||
.await?;
|
||||
info!("deleted {} stats from db", res.deleted_count);
|
||||
if res.deleted_count > 0 {
|
||||
info!("deleted {} stats from db", res.deleted_count);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -84,12 +85,13 @@ async fn prune_alerts() -> anyhow::Result<()> {
|
||||
- core_config().keep_alerts_for_days as u128 * ONE_DAY_MS)
|
||||
as i64;
|
||||
let res = db_client()
|
||||
.await
|
||||
.alerts
|
||||
.delete_many(doc! {
|
||||
"ts": { "$lt": delete_before_ts }
|
||||
})
|
||||
.await?;
|
||||
info!("deleted {} alerts from db", res.deleted_count);
|
||||
if res.deleted_count > 0 {
|
||||
info!("deleted {} alerts from db", res.deleted_count);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -32,11 +32,10 @@ use mungos::{
|
||||
use crate::{
|
||||
config::core_config,
|
||||
resource::{self, get_user_permission_on_resource},
|
||||
stack::compose_container_match_regex,
|
||||
state::{db_client, deployment_status_cache, stack_status_cache},
|
||||
};
|
||||
|
||||
use super::stack::compose_container_match_regex;
|
||||
|
||||
// user: Id or username
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn get_user(user: &str) -> anyhow::Result<User> {
|
||||
@@ -44,7 +43,6 @@ pub async fn get_user(user: &str) -> anyhow::Result<User> {
|
||||
return Ok(user);
|
||||
}
|
||||
db_client()
|
||||
.await
|
||||
.users
|
||||
.find_one(id_or_username_filter(user))
|
||||
.await
|
||||
@@ -184,7 +182,6 @@ pub async fn get_tag(id_or_name: &str) -> anyhow::Result<Tag> {
|
||||
Err(_) => doc! { "name": id_or_name },
|
||||
};
|
||||
db_client()
|
||||
.await
|
||||
.tags
|
||||
.find_one(query)
|
||||
.await
|
||||
@@ -207,7 +204,7 @@ pub async fn get_tag_check_owner(
|
||||
pub async fn get_id_to_tags(
|
||||
filter: impl Into<Option<Document>>,
|
||||
) -> anyhow::Result<HashMap<String, Tag>> {
|
||||
let res = find_collect(&db_client().await.tags, filter, None)
|
||||
let res = find_collect(&db_client().tags, filter, None)
|
||||
.await
|
||||
.context("failed to query db for tags")?
|
||||
.into_iter()
|
||||
@@ -221,7 +218,7 @@ pub async fn get_user_user_groups(
|
||||
user_id: &str,
|
||||
) -> anyhow::Result<Vec<UserGroup>> {
|
||||
find_collect(
|
||||
&db_client().await.user_groups,
|
||||
&db_client().user_groups,
|
||||
doc! {
|
||||
"users": user_id
|
||||
},
|
||||
@@ -315,7 +312,6 @@ pub fn id_or_username_filter(id_or_username: &str) -> Document {
|
||||
|
||||
pub async fn get_variable(name: &str) -> anyhow::Result<Variable> {
|
||||
db_client()
|
||||
.await
|
||||
.variables
|
||||
.find_one(doc! { "name": &name })
|
||||
.await
|
||||
@@ -331,7 +327,6 @@ pub async fn get_latest_update(
|
||||
operation: Operation,
|
||||
) -> anyhow::Result<Option<Update>> {
|
||||
db_client()
|
||||
.await
|
||||
.updates
|
||||
.find_one(doc! {
|
||||
"target.type": resource_type.as_ref(),
|
||||
@@ -354,10 +349,9 @@ pub struct VariablesAndSecrets {
|
||||
|
||||
pub async fn get_variables_and_secrets(
|
||||
) -> anyhow::Result<VariablesAndSecrets> {
|
||||
let variables =
|
||||
find_collect(&db_client().await.variables, None, None)
|
||||
.await
|
||||
.context("failed to get all variables from db")?;
|
||||
let variables = find_collect(&db_client().variables, None, None)
|
||||
.await
|
||||
.context("failed to get all variables from db")?;
|
||||
let mut secrets = core_config().secrets.clone();
|
||||
|
||||
// extend secrets with secret variables
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
use async_timing_util::{wait_until_timelength, Timelength};
|
||||
use komodo_client::{
|
||||
api::write::RefreshRepoCache, entities::user::repo_user,
|
||||
};
|
||||
use mungos::find::find_collect;
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
state::{db_client, State},
|
||||
};
|
||||
|
||||
pub fn spawn_repo_refresh_loop() {
|
||||
let interval: Timelength = core_config()
|
||||
.repo_poll_interval
|
||||
.try_into()
|
||||
.expect("Invalid repo poll interval");
|
||||
tokio::spawn(async move {
|
||||
refresh_repos().await;
|
||||
loop {
|
||||
wait_until_timelength(interval, 1000).await;
|
||||
refresh_repos().await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn refresh_repos() {
|
||||
let Ok(repos) = find_collect(&db_client().await.repos, None, None)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
warn!("failed to get repos from db in refresh task | {e:#}")
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
for repo in repos {
|
||||
State
|
||||
.resolve(
|
||||
RefreshRepoCache { repo: repo.id },
|
||||
repo_user().clone(),
|
||||
)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
warn!("failed to refresh repo cache in refresh task | repo: {} | {e:#}", repo.name)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use async_timing_util::{wait_until_timelength, Timelength};
|
||||
use komodo_client::{
|
||||
api::write::RefreshStackCache,
|
||||
entities::{
|
||||
permission::PermissionLevel,
|
||||
server::{Server, ServerState},
|
||||
stack::Stack,
|
||||
user::{stack_user, User},
|
||||
},
|
||||
};
|
||||
use mungos::find::find_collect;
|
||||
use regex::Regex;
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
resource,
|
||||
state::{db_client, State},
|
||||
};
|
||||
|
||||
use super::query::get_server_with_state;
|
||||
|
||||
pub mod execute;
|
||||
pub mod remote;
|
||||
pub mod services;
|
||||
|
||||
pub fn spawn_stack_refresh_loop() {
|
||||
let interval: Timelength = core_config()
|
||||
.stack_poll_interval
|
||||
.try_into()
|
||||
.expect("Invalid stack poll interval");
|
||||
tokio::spawn(async move {
|
||||
refresh_stacks().await;
|
||||
loop {
|
||||
wait_until_timelength(interval, 3000).await;
|
||||
refresh_stacks().await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn refresh_stacks() {
|
||||
let Ok(stacks) =
|
||||
find_collect(&db_client().await.stacks, None, None)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
warn!("failed to get stacks from db in refresh task | {e:#}")
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
for stack in stacks {
|
||||
State
|
||||
.resolve(
|
||||
RefreshStackCache { stack: stack.id },
|
||||
stack_user().clone(),
|
||||
)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
warn!("failed to refresh stack cache in refresh task | stack: {} | {e:#}", stack.name)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_stack_and_server(
|
||||
stack: &str,
|
||||
user: &User,
|
||||
permission_level: PermissionLevel,
|
||||
block_if_server_unreachable: bool,
|
||||
) -> anyhow::Result<(Stack, Server)> {
|
||||
let stack = resource::get_check_permissions::<Stack>(
|
||||
stack,
|
||||
user,
|
||||
permission_level,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if stack.config.server_id.is_empty() {
|
||||
return Err(anyhow!("Stack has no server configured"));
|
||||
}
|
||||
|
||||
let (server, status) =
|
||||
get_server_with_state(&stack.config.server_id).await?;
|
||||
if block_if_server_unreachable && status != ServerState::Ok {
|
||||
return Err(anyhow!(
|
||||
"cannot send action when server is unreachable or disabled"
|
||||
));
|
||||
}
|
||||
|
||||
Ok((stack, server))
|
||||
}
|
||||
|
||||
pub fn compose_container_match_regex(
|
||||
container_name: &str,
|
||||
) -> anyhow::Result<Regex> {
|
||||
let regex = format!("^{container_name}-?[0-9]*$");
|
||||
Regex::new(®ex).with_context(|| {
|
||||
format!("failed to construct valid regex from {regex}")
|
||||
})
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
use std::{fs, path::Path};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use formatting::{colored, muted, Color};
|
||||
use komodo_client::entities::{toml::ResourcesToml, update::Log};
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
pub fn read_resources(
|
||||
path: &Path,
|
||||
) -> anyhow::Result<(ResourcesToml, Log)> {
|
||||
let mut res = ResourcesToml::default();
|
||||
let mut log =
|
||||
format!("{}: reading resources from {path:?}", muted("INFO"));
|
||||
read_resources_recursive(path, &mut res, &mut log).with_context(
|
||||
|| format!("failed to read resources from {path:?}"),
|
||||
)?;
|
||||
Ok((res, Log::simple("read remote resources", log)))
|
||||
}
|
||||
|
||||
fn read_resources_recursive(
|
||||
path: &Path,
|
||||
resources: &mut ResourcesToml,
|
||||
log: &mut String,
|
||||
) -> anyhow::Result<()> {
|
||||
let res =
|
||||
fs::metadata(path).context("failed to get path metadata")?;
|
||||
if res.is_file() {
|
||||
if !path
|
||||
.extension()
|
||||
.map(|ext| ext == "toml")
|
||||
.unwrap_or_default()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
let more = parse_toml_file::<ResourcesToml>(path)
|
||||
.context("failed to parse resource file")?;
|
||||
|
||||
log.push('\n');
|
||||
log.push_str(&format!(
|
||||
"{}: {} from {}",
|
||||
muted("INFO"),
|
||||
colored("adding resources", Color::Green),
|
||||
colored(path.display(), Color::Blue)
|
||||
));
|
||||
|
||||
resources.servers.extend(more.servers);
|
||||
resources.deployments.extend(more.deployments);
|
||||
resources.stacks.extend(more.stacks);
|
||||
resources.builds.extend(more.builds);
|
||||
resources.repos.extend(more.repos);
|
||||
resources.procedures.extend(more.procedures);
|
||||
resources.alerters.extend(more.alerters);
|
||||
resources.builders.extend(more.builders);
|
||||
resources.server_templates.extend(more.server_templates);
|
||||
resources.resource_syncs.extend(more.resource_syncs);
|
||||
resources.user_groups.extend(more.user_groups);
|
||||
resources.variables.extend(more.variables);
|
||||
|
||||
Ok(())
|
||||
} else if res.is_dir() {
|
||||
let directory = fs::read_dir(path)
|
||||
.context("failed to read directory contents")?;
|
||||
for entry in directory.into_iter().flatten() {
|
||||
let path = entry.path();
|
||||
read_resources_recursive(&path, resources, log).with_context(
|
||||
|| format!("failed to read resources from {path:?}"),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("resources path is neither file nor directory"))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_toml_file<T: DeserializeOwned>(
|
||||
path: impl AsRef<std::path::Path>,
|
||||
) -> anyhow::Result<T> {
|
||||
let contents = std::fs::read_to_string(path)
|
||||
.context("failed to read file contents")?;
|
||||
toml::from_str(&contents)
|
||||
// the error without this comes through with multiple lines (\n) and looks bad
|
||||
.map_err(|e| anyhow!("{e:#}"))
|
||||
.context("failed to parse toml contents")
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
use async_timing_util::{wait_until_timelength, Timelength};
|
||||
use komodo_client::{
|
||||
api::write::RefreshResourceSyncPending, entities::user::sync_user,
|
||||
};
|
||||
use mungos::find::find_collect;
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
state::{db_client, State},
|
||||
};
|
||||
|
||||
// pub mod deployment;
|
||||
pub mod deploy;
|
||||
pub mod remote;
|
||||
pub mod resource;
|
||||
pub mod user_groups;
|
||||
pub mod variables;
|
||||
|
||||
mod file;
|
||||
mod resources;
|
||||
|
||||
pub fn spawn_sync_refresh_loop() {
|
||||
let interval: Timelength = core_config()
|
||||
.sync_poll_interval
|
||||
.try_into()
|
||||
.expect("Invalid sync poll interval");
|
||||
tokio::spawn(async move {
|
||||
refresh_syncs().await;
|
||||
loop {
|
||||
wait_until_timelength(interval, 0).await;
|
||||
refresh_syncs().await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn refresh_syncs() {
|
||||
let Ok(syncs) =
|
||||
find_collect(&db_client().await.resource_syncs, None, None)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
warn!(
|
||||
"failed to get resource syncs from db in refresh task | {e:#}"
|
||||
)
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
for sync in syncs {
|
||||
if sync.config.repo.is_empty() {
|
||||
continue;
|
||||
}
|
||||
State
|
||||
.resolve(
|
||||
RefreshResourceSyncPending { sync: sync.id },
|
||||
sync_user().clone(),
|
||||
)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
warn!("failed to refresh resource sync in refresh task | sync: {} | {e:#}", sync.name)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
use std::fs;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use komodo_client::entities::{
|
||||
sync::ResourceSync, toml::ResourcesToml, update::Log, CloneArgs,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::{git_token, random_string},
|
||||
state::resource_sync_lock_cache,
|
||||
};
|
||||
|
||||
pub async fn get_remote_resources(
|
||||
sync: &ResourceSync,
|
||||
) -> anyhow::Result<(
|
||||
anyhow::Result<ResourcesToml>,
|
||||
Vec<Log>,
|
||||
// commit short hash
|
||||
String,
|
||||
// commit message
|
||||
String,
|
||||
)> {
|
||||
let mut clone_args: CloneArgs = sync.into();
|
||||
|
||||
let config = core_config();
|
||||
|
||||
let access_token = match (&clone_args.account, &clone_args.provider)
|
||||
{
|
||||
(None, _) => None,
|
||||
(Some(_), None) => {
|
||||
return Err(anyhow!(
|
||||
"Account is configured, but provider is empty"
|
||||
))
|
||||
}
|
||||
(Some(username), Some(provider)) => {
|
||||
git_token(provider, username, |https| clone_args.https = https)
|
||||
.await
|
||||
.with_context(
|
||||
|| format!("Failed to get git token in call to db. Stopping run. | {provider} | {username}"),
|
||||
)?
|
||||
}
|
||||
};
|
||||
|
||||
fs::create_dir_all(&config.repo_directory)
|
||||
.context("failed to create sync directory")?;
|
||||
|
||||
// lock simultaneous access to same directory
|
||||
let lock = resource_sync_lock_cache()
|
||||
.get_or_insert_default(&sync.id)
|
||||
.await;
|
||||
let _lock = lock.lock().await;
|
||||
|
||||
let repo_path = config.repo_directory.join(random_string(10));
|
||||
// This overrides any other method of determining clone path.
|
||||
clone_args.destination = Some(repo_path.display().to_string());
|
||||
|
||||
// Don't want to run these on core.
|
||||
clone_args.on_clone = None;
|
||||
clone_args.on_pull = None;
|
||||
|
||||
let (mut logs, hash, message, _) = git::clone(
|
||||
clone_args,
|
||||
&config.repo_directory,
|
||||
access_token,
|
||||
&[],
|
||||
"",
|
||||
None,
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.context("failed to clone resource repo")?;
|
||||
|
||||
let hash = hash.context("failed to get commit hash")?;
|
||||
let message =
|
||||
message.context("failed to get commit hash message")?;
|
||||
|
||||
let resource_path = repo_path.join(&sync.config.resource_path);
|
||||
|
||||
let res = super::file::read_resources(&resource_path).map(
|
||||
|(resources, log)| {
|
||||
logs.push(log);
|
||||
resources
|
||||
},
|
||||
);
|
||||
|
||||
if repo_path.exists() {
|
||||
if let Err(e) = std::fs::remove_dir_all(&repo_path) {
|
||||
warn!("failed to remove sync repo directory | {e:?}")
|
||||
}
|
||||
}
|
||||
|
||||
Ok((res, logs, hash, message))
|
||||
}
|
||||
@@ -44,7 +44,6 @@ pub async fn add_update(
|
||||
mut update: Update,
|
||||
) -> anyhow::Result<String> {
|
||||
update.id = db_client()
|
||||
.await
|
||||
.updates
|
||||
.insert_one(&update)
|
||||
.await
|
||||
@@ -64,7 +63,6 @@ pub async fn add_update_without_send(
|
||||
update: &Update,
|
||||
) -> anyhow::Result<String> {
|
||||
let id = db_client()
|
||||
.await
|
||||
.updates
|
||||
.insert_one(update)
|
||||
.await
|
||||
@@ -78,7 +76,7 @@ pub async fn add_update_without_send(
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn update_update(update: Update) -> anyhow::Result<()> {
|
||||
update_one_by_id(&db_client().await.updates, &update.id, mungos::update::Update::Set(to_document(&update)?), None)
|
||||
update_one_by_id(&db_client().updates, &update.id, mungos::update::Update::Set(to_document(&update)?), None)
|
||||
.await
|
||||
.context("failed to update the update on db. the update build process was deleted")?;
|
||||
let update = update_list_item(update).await?;
|
||||
@@ -93,7 +91,7 @@ async fn update_list_item(
|
||||
let username = if User::is_service_user(&update.operator) {
|
||||
update.operator.clone()
|
||||
} else {
|
||||
find_one_by_id(&db_client().await.users, &update.operator)
|
||||
find_one_by_id(&db_client().users, &update.operator)
|
||||
.await
|
||||
.context("failed to query mongo for user")?
|
||||
.with_context(|| {
|
||||
@@ -236,6 +234,18 @@ pub async fn init_execution_update(
|
||||
resource::get::<Server>(&data.server).await?.id,
|
||||
),
|
||||
),
|
||||
ExecuteRequest::PruneDockerBuilders(data) => (
|
||||
Operation::PruneDockerBuilders,
|
||||
ResourceTarget::Server(
|
||||
resource::get::<Server>(&data.server).await?.id,
|
||||
),
|
||||
),
|
||||
ExecuteRequest::PruneBuildx(data) => (
|
||||
Operation::PruneBuildx,
|
||||
ResourceTarget::Server(
|
||||
resource::get::<Server>(&data.server).await?.id,
|
||||
),
|
||||
),
|
||||
ExecuteRequest::PruneSystem(data) => (
|
||||
Operation::PruneSystem,
|
||||
ResourceTarget::Server(
|
||||
|
||||
@@ -5,13 +5,15 @@ use std::{net::SocketAddr, str::FromStr};
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::Router;
|
||||
use axum_server::tls_openssl::OpenSSLConfig;
|
||||
use tower_http::{
|
||||
cors::{Any, CorsLayer},
|
||||
services::{ServeDir, ServeFile},
|
||||
};
|
||||
|
||||
use crate::config::{core_config, frontend_path};
|
||||
use crate::config::core_config;
|
||||
|
||||
mod alert;
|
||||
mod api;
|
||||
mod auth;
|
||||
mod cloud;
|
||||
@@ -21,37 +23,45 @@ mod helpers;
|
||||
mod listener;
|
||||
mod monitor;
|
||||
mod resource;
|
||||
mod stack;
|
||||
mod state;
|
||||
mod sync;
|
||||
mod ws;
|
||||
|
||||
async fn app() -> anyhow::Result<()> {
|
||||
dotenvy::dotenv().ok();
|
||||
let config = core_config();
|
||||
logger::init(&config.logging)?;
|
||||
info!("Komodo Core version: v{}", env!("CARGO_PKG_VERSION"));
|
||||
info!("config: {:?}", config.sanitized());
|
||||
|
||||
// includes init db_client check to crash on db init failure
|
||||
helpers::startup_cleanup().await;
|
||||
// Maybe initialize default server in All In One deployment.
|
||||
helpers::ensure_server().await;
|
||||
info!("Komodo Core version: v{}", env!("CARGO_PKG_VERSION"));
|
||||
info!("{:?}", config.sanitized());
|
||||
|
||||
tokio::join!(
|
||||
// Init db_client check to crash on db init failure
|
||||
state::init_db_client(),
|
||||
// Init default OIDC client (defined in config / env vars / compose secret file)
|
||||
auth::oidc::client::init_default_oidc_client()
|
||||
);
|
||||
tokio::join!(
|
||||
// Maybe initialize first server
|
||||
helpers::ensure_first_server_and_builder(),
|
||||
// Cleanup open updates / invalid alerts
|
||||
helpers::startup_cleanup(),
|
||||
);
|
||||
// init jwt client to crash on failure
|
||||
state::jwt_client();
|
||||
|
||||
// Spawn tasks
|
||||
monitor::spawn_monitor_loop();
|
||||
helpers::prune::spawn_prune_loop();
|
||||
helpers::stack::spawn_stack_refresh_loop();
|
||||
helpers::sync::spawn_sync_refresh_loop();
|
||||
helpers::build::spawn_build_refresh_loop();
|
||||
helpers::repo::spawn_repo_refresh_loop();
|
||||
resource::spawn_resource_refresh_loop();
|
||||
resource::spawn_build_state_refresh_loop();
|
||||
resource::spawn_repo_state_refresh_loop();
|
||||
resource::spawn_procedure_state_refresh_loop();
|
||||
resource::spawn_resource_sync_state_refresh_loop();
|
||||
helpers::prune::spawn_prune_loop();
|
||||
|
||||
// Setup static frontend services
|
||||
let frontend_path = frontend_path();
|
||||
let frontend_path = &config.frontend_path;
|
||||
let frontend_index =
|
||||
ServeFile::new(format!("{frontend_path}/index.html"));
|
||||
let serve_dir = ServeDir::new(frontend_path)
|
||||
@@ -67,19 +77,29 @@ async fn app() -> anyhow::Result<()> {
|
||||
.nest("/ws", ws::router())
|
||||
.nest_service("/", serve_dir)
|
||||
.fallback_service(frontend_index)
|
||||
.layer(cors()?);
|
||||
.layer(cors()?)
|
||||
.into_make_service();
|
||||
|
||||
let socket_addr =
|
||||
SocketAddr::from_str(&format!("0.0.0.0:{}", core_config().port))
|
||||
.context("failed to parse socket addr")?;
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(&socket_addr)
|
||||
.await
|
||||
.context("failed to bind to tcp listener")?;
|
||||
|
||||
info!("Komodo Core listening on {socket_addr}");
|
||||
|
||||
axum::serve(listener, app).await.context("server crashed")?;
|
||||
if config.ssl_enabled {
|
||||
info!("🔒 Core SSL Enabled");
|
||||
info!("Komodo Core starting on https://{socket_addr}");
|
||||
let ssl_config = OpenSSLConfig::from_pem_file(
|
||||
&config.ssl_cert_file,
|
||||
&config.ssl_key_file,
|
||||
)
|
||||
.context("Failed to parse ssl ")?;
|
||||
axum_server::bind_openssl(socket_addr, ssl_config)
|
||||
.serve(app)
|
||||
.await?
|
||||
} else {
|
||||
info!("🔓 Core SSL Disabled");
|
||||
info!("Komodo Core starting on http://{socket_addr}");
|
||||
axum_server::bind(socket_addr).serve(app).await?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ use komodo_client::entities::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
helpers::alert::send_alerts, monitor::deployment_status_cache,
|
||||
resource, state::db_client,
|
||||
alert::send_alerts, monitor::deployment_status_cache, resource,
|
||||
state::db_client,
|
||||
};
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
@@ -73,7 +73,7 @@ pub async fn alert_deployments(
|
||||
return;
|
||||
}
|
||||
send_alerts(&alerts).await;
|
||||
let res = db_client().await.alerts.insert_many(alerts).await;
|
||||
let res = db_client().alerts.insert_many(alerts).await;
|
||||
if let Err(e) = res {
|
||||
error!("failed to record deployment status alerts to db | {e:#}");
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ use std::{collections::HashMap, path::PathBuf, str::FromStr};
|
||||
|
||||
use anyhow::Context;
|
||||
use derive_variants::ExtractVariant;
|
||||
use mongo_indexed::Indexed;
|
||||
use komodo_client::entities::{
|
||||
alert::{Alert, AlertData, AlertDataVariant, SeverityLevel},
|
||||
komodo_timestamp, optional_string,
|
||||
server::{ServerListItem, ServerState},
|
||||
ResourceTarget,
|
||||
};
|
||||
use mongo_indexed::Indexed;
|
||||
use mungos::{
|
||||
bulk_update::{self, BulkUpdate},
|
||||
find::find_collect,
|
||||
@@ -16,7 +16,7 @@ use mungos::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
helpers::alert::send_alerts,
|
||||
alert::send_alerts,
|
||||
state::{db_client, server_status_cache},
|
||||
};
|
||||
|
||||
@@ -366,7 +366,7 @@ async fn open_alerts(alerts: &[(Alert, SendAlerts)]) {
|
||||
return;
|
||||
}
|
||||
|
||||
let db = db_client().await;
|
||||
let db = db_client();
|
||||
|
||||
let open = || async {
|
||||
let ids = db
|
||||
@@ -431,7 +431,7 @@ async fn update_alerts(alerts: &[(Alert, SendAlerts)]) {
|
||||
}).collect::<Vec<_>>();
|
||||
|
||||
bulk_update::bulk_update(
|
||||
&db_client().await.db,
|
||||
&db_client().db,
|
||||
Alert::default_collection_name(),
|
||||
&updates,
|
||||
false,
|
||||
@@ -472,7 +472,6 @@ async fn resolve_alerts(alerts: &[(Alert, SendAlerts)]) {
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
|
||||
db_client()
|
||||
.await
|
||||
.alerts
|
||||
.update_many(
|
||||
doc! { "_id": { "$in": &alert_ids } },
|
||||
@@ -518,7 +517,7 @@ async fn resolve_alerts(alerts: &[(Alert, SendAlerts)]) {
|
||||
async fn get_open_alerts(
|
||||
) -> anyhow::Result<(OpenAlertMap, OpenDiskAlertMap)> {
|
||||
let alerts = find_collect(
|
||||
&db_client().await.alerts,
|
||||
&db_client().alerts,
|
||||
doc! { "resolved": false },
|
||||
None,
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ use komodo_client::entities::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
helpers::alert::send_alerts,
|
||||
alert::send_alerts,
|
||||
resource,
|
||||
state::{db_client, stack_status_cache},
|
||||
};
|
||||
@@ -72,7 +72,7 @@ pub async fn alert_stacks(
|
||||
return;
|
||||
}
|
||||
send_alerts(&alerts).await;
|
||||
let res = db_client().await.alerts.insert_many(alerts).await;
|
||||
let res = db_client().alerts.insert_many(alerts).await;
|
||||
if let Err(e) = res {
|
||||
error!("failed to record stack status alerts to db | {e:#}");
|
||||
}
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
use komodo_client::entities::{
|
||||
alert::SeverityLevel, deployment::{Deployment, DeploymentState}, docker::{
|
||||
alert::SeverityLevel,
|
||||
deployment::{Deployment, DeploymentState},
|
||||
docker::{
|
||||
container::ContainerListItem, image::ImageListItem,
|
||||
network::NetworkListItem, volume::VolumeListItem,
|
||||
}, repo::Repo, server::{Server, ServerConfig, ServerHealth, ServerState}, stack::{ComposeProject, Stack, StackState}, stats::{SingleDiskUsage, SystemStats}
|
||||
},
|
||||
repo::Repo,
|
||||
server::{Server, ServerConfig, ServerHealth, ServerState},
|
||||
stack::{ComposeProject, Stack, StackState},
|
||||
stats::{SingleDiskUsage, SystemStats},
|
||||
};
|
||||
use serror::Serror;
|
||||
|
||||
|
||||
@@ -98,21 +98,16 @@ pub fn spawn_monitor_loop() {
|
||||
}
|
||||
|
||||
async fn refresh_server_cache(ts: i64) {
|
||||
let servers = match find_collect(
|
||||
&db_client().await.servers,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(servers) => servers,
|
||||
Err(e) => {
|
||||
error!(
|
||||
"failed to get server list (manage status cache) | {e:#}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let servers =
|
||||
match find_collect(&db_client().servers, None, None).await {
|
||||
Ok(servers) => servers,
|
||||
Err(e) => {
|
||||
error!(
|
||||
"failed to get server list (manage status cache) | {e:#}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let futures = servers.into_iter().map(|server| async move {
|
||||
update_cache_for_server(&server).await;
|
||||
});
|
||||
@@ -124,17 +119,17 @@ async fn refresh_server_cache(ts: i64) {
|
||||
pub async fn update_cache_for_server(server: &Server) {
|
||||
let (deployments, repos, stacks) = tokio::join!(
|
||||
find_collect(
|
||||
&db_client().await.deployments,
|
||||
&db_client().deployments,
|
||||
doc! { "config.server_id": &server.id },
|
||||
None,
|
||||
),
|
||||
find_collect(
|
||||
&db_client().await.repos,
|
||||
&db_client().repos,
|
||||
doc! { "config.server_id": &server.id },
|
||||
None,
|
||||
),
|
||||
find_collect(
|
||||
&db_client().await.stacks,
|
||||
&db_client().stacks,
|
||||
doc! { "config.server_id": &server.id },
|
||||
None,
|
||||
)
|
||||
|
||||
@@ -30,7 +30,7 @@ pub async fn record_server_stats(ts: i64) {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if !records.is_empty() {
|
||||
let res = db_client().await.stats.insert_many(records).await;
|
||||
let res = db_client().stats.insert_many(records).await;
|
||||
if let Err(e) = res {
|
||||
error!("failed to record server stats | {e:#}");
|
||||
}
|
||||
|
||||
@@ -6,12 +6,10 @@ use komodo_client::entities::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
query::get_stack_state_from_containers,
|
||||
stack::{
|
||||
compose_container_match_regex,
|
||||
services::extract_services_from_stack,
|
||||
},
|
||||
helpers::query::get_stack_state_from_containers,
|
||||
stack::{
|
||||
compose_container_match_regex,
|
||||
services::extract_services_from_stack,
|
||||
},
|
||||
state::{deployment_status_cache, stack_status_cache},
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@ impl super::KomodoResource for Alerter {
|
||||
|
||||
async fn coll(
|
||||
) -> &'static Collection<Resource<Self::Config, Self::Info>> {
|
||||
&db_client().await.alerters
|
||||
&db_client().alerters
|
||||
}
|
||||
|
||||
async fn to_list_item(
|
||||
|
||||
@@ -8,6 +8,7 @@ use komodo_client::entities::{
|
||||
PartialBuildConfig,
|
||||
},
|
||||
builder::Builder,
|
||||
environment_vars_from_str,
|
||||
permission::PermissionLevel,
|
||||
resource::Resource,
|
||||
update::Update,
|
||||
@@ -20,6 +21,7 @@ use mungos::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::{empty_or_only_spaces, query::get_latest_update},
|
||||
state::{action_states, build_state_cache, db_client},
|
||||
};
|
||||
@@ -38,7 +40,7 @@ impl super::KomodoResource for Build {
|
||||
|
||||
async fn coll(
|
||||
) -> &'static Collection<Resource<Self::Config, Self::Info>> {
|
||||
&db_client().await.builds
|
||||
&db_client().builds
|
||||
}
|
||||
|
||||
async fn to_list_item(
|
||||
@@ -55,6 +57,7 @@ impl super::KomodoResource for Build {
|
||||
version: build.config.version,
|
||||
builder_id: build.config.builder_id,
|
||||
git_provider: build.config.git_provider,
|
||||
image_registry_domain: build.config.image_registry.domain,
|
||||
repo: build.config.repo,
|
||||
branch: build.config.branch,
|
||||
built_hash: build.info.built_hash,
|
||||
@@ -80,7 +83,9 @@ impl super::KomodoResource for Build {
|
||||
}
|
||||
|
||||
fn user_can_create(user: &User) -> bool {
|
||||
user.admin || user.create_build_permissions
|
||||
user.admin
|
||||
|| (!core_config().disable_non_admin_create
|
||||
&& user.create_build_permissions)
|
||||
}
|
||||
|
||||
async fn validate_create_config(
|
||||
@@ -152,7 +157,7 @@ pub fn spawn_build_state_refresh_loop() {
|
||||
|
||||
pub async fn refresh_build_state_cache() {
|
||||
let _ = async {
|
||||
let builds = find_collect(&db_client().await.builds, None, None)
|
||||
let builds = find_collect(&db_client().builds, None, None)
|
||||
.await
|
||||
.context("failed to get builds from db")?;
|
||||
let cache = build_state_cache();
|
||||
@@ -181,11 +186,13 @@ async fn validate_config(
|
||||
config.builder_id = Some(builder.id)
|
||||
}
|
||||
}
|
||||
if let Some(build_args) = &mut config.build_args {
|
||||
build_args.retain(|v| {
|
||||
!empty_or_only_spaces(&v.variable)
|
||||
&& !empty_or_only_spaces(&v.value)
|
||||
})
|
||||
if let Some(build_args) = &config.build_args {
|
||||
environment_vars_from_str(build_args)
|
||||
.context("Invalid build_args")?;
|
||||
}
|
||||
if let Some(secret_args) = &config.secret_args {
|
||||
environment_vars_from_str(secret_args)
|
||||
.context("Invalid secret_args")?;
|
||||
}
|
||||
if let Some(extra_args) = &mut config.extra_args {
|
||||
extra_args.retain(|v| !empty_or_only_spaces(v))
|
||||
@@ -258,7 +265,7 @@ async fn latest_2_build_updates(
|
||||
id: &str,
|
||||
) -> anyhow::Result<[Option<Update>; 2]> {
|
||||
let mut builds = find_collect(
|
||||
&db_client().await.updates,
|
||||
&db_client().updates,
|
||||
doc! {
|
||||
"target.type": "Build",
|
||||
"target.id": id,
|
||||
|
||||
@@ -33,7 +33,7 @@ impl super::KomodoResource for Builder {
|
||||
|
||||
async fn coll(
|
||||
) -> &'static Collection<Resource<Self::Config, Self::Info>> {
|
||||
&db_client().await.builders
|
||||
&db_client().builders
|
||||
}
|
||||
|
||||
async fn to_list_item(
|
||||
@@ -130,7 +130,6 @@ impl super::KomodoResource for Builder {
|
||||
) -> anyhow::Result<()> {
|
||||
// remove the builder from any attached builds
|
||||
db_client()
|
||||
.await
|
||||
.builds
|
||||
.update_many(
|
||||
doc! { "config.builder.params.builder_id": &resource.id },
|
||||
|
||||
@@ -3,11 +3,12 @@ use formatting::format_serror;
|
||||
use komodo_client::entities::{
|
||||
build::Build,
|
||||
deployment::{
|
||||
Deployment, DeploymentConfig, DeploymentConfigDiff,
|
||||
DeploymentImage, DeploymentListItem, DeploymentListItemInfo,
|
||||
DeploymentQuerySpecifics, DeploymentState,
|
||||
PartialDeploymentConfig,
|
||||
conversions_from_str, Deployment, DeploymentConfig,
|
||||
DeploymentConfigDiff, DeploymentImage, DeploymentListItem,
|
||||
DeploymentListItemInfo, DeploymentQuerySpecifics,
|
||||
DeploymentState, PartialDeploymentConfig,
|
||||
},
|
||||
environment_vars_from_str,
|
||||
permission::PermissionLevel,
|
||||
resource::Resource,
|
||||
server::Server,
|
||||
@@ -19,6 +20,7 @@ use mungos::mongodb::Collection;
|
||||
use periphery_client::api::container::RemoveContainer;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::{
|
||||
empty_or_only_spaces, periphery_client,
|
||||
query::get_deployment_state,
|
||||
@@ -44,7 +46,7 @@ impl super::KomodoResource for Deployment {
|
||||
|
||||
async fn coll(
|
||||
) -> &'static Collection<Resource<Self::Config, Self::Info>> {
|
||||
&db_client().await.deployments
|
||||
&db_client().deployments
|
||||
}
|
||||
|
||||
async fn to_list_item(
|
||||
@@ -86,12 +88,12 @@ impl super::KomodoResource for Deployment {
|
||||
}),
|
||||
image: status
|
||||
.as_ref()
|
||||
.map(|s| {
|
||||
s.curr
|
||||
.container
|
||||
.as_ref()
|
||||
.and_then(|c| c.image.clone())
|
||||
.unwrap_or_else(|| String::from("Unknown"))
|
||||
.and_then(|s| {
|
||||
s.curr.container.as_ref().map(|c| {
|
||||
c.image
|
||||
.clone()
|
||||
.unwrap_or_else(|| String::from("Unknown"))
|
||||
})
|
||||
})
|
||||
.unwrap_or(build_image),
|
||||
server_id: deployment.config.server_id,
|
||||
@@ -115,8 +117,8 @@ impl super::KomodoResource for Deployment {
|
||||
Operation::CreateDeployment
|
||||
}
|
||||
|
||||
fn user_can_create(_user: &User) -> bool {
|
||||
true
|
||||
fn user_can_create(user: &User) -> bool {
|
||||
user.admin || !core_config().disable_non_admin_create
|
||||
}
|
||||
|
||||
async fn validate_create_config(
|
||||
@@ -279,23 +281,15 @@ async fn validate_config(
|
||||
});
|
||||
}
|
||||
}
|
||||
if let Some(volumes) = &mut config.volumes {
|
||||
volumes.retain(|v| {
|
||||
!empty_or_only_spaces(&v.local)
|
||||
&& !empty_or_only_spaces(&v.container)
|
||||
})
|
||||
if let Some(volumes) = &config.volumes {
|
||||
conversions_from_str(volumes).context("Invalid volumes")?;
|
||||
}
|
||||
if let Some(ports) = &mut config.ports {
|
||||
ports.retain(|v| {
|
||||
!empty_or_only_spaces(&v.local)
|
||||
&& !empty_or_only_spaces(&v.container)
|
||||
})
|
||||
if let Some(ports) = &config.ports {
|
||||
conversions_from_str(ports).context("Invalid ports")?;
|
||||
}
|
||||
if let Some(environment) = &mut config.environment {
|
||||
environment.retain(|v| {
|
||||
!empty_or_only_spaces(&v.variable)
|
||||
&& !empty_or_only_spaces(&v.value)
|
||||
})
|
||||
if let Some(environment) = &config.environment {
|
||||
environment_vars_from_str(environment)
|
||||
.context("Invalid environment")?;
|
||||
}
|
||||
if let Some(extra_args) = &mut config.extra_args {
|
||||
extra_args.retain(|v| !empty_or_only_spaces(v))
|
||||
|
||||
@@ -12,9 +12,10 @@ use komodo_client::{
|
||||
komodo_timestamp,
|
||||
permission::PermissionLevel,
|
||||
resource::{AddFilters, Resource, ResourceQuery},
|
||||
tag::Tag,
|
||||
to_komodo_name,
|
||||
update::Update,
|
||||
user::User,
|
||||
user::{system_user, User},
|
||||
Operation, ResourceTarget, ResourceTargetVariant,
|
||||
},
|
||||
};
|
||||
@@ -49,6 +50,7 @@ mod build;
|
||||
mod builder;
|
||||
mod deployment;
|
||||
mod procedure;
|
||||
mod refresh;
|
||||
mod repo;
|
||||
mod server;
|
||||
mod server_template;
|
||||
@@ -61,6 +63,7 @@ pub use build::{
|
||||
pub use procedure::{
|
||||
refresh_procedure_state_cache, spawn_procedure_state_refresh_loop,
|
||||
};
|
||||
pub use refresh::spawn_resource_refresh_loop;
|
||||
pub use repo::{
|
||||
refresh_repo_state_cache, spawn_repo_state_refresh_loop,
|
||||
};
|
||||
@@ -82,7 +85,8 @@ pub trait KomodoResource {
|
||||
+ From<Self::PartialConfig>
|
||||
+ PartialDiff<Self::PartialConfig, Self::ConfigDiff>
|
||||
+ 'static;
|
||||
type PartialConfig: Default
|
||||
type PartialConfig: Clone
|
||||
+ Default
|
||||
+ From<Self::Config>
|
||||
+ Serialize
|
||||
+ MaybeNone;
|
||||
@@ -90,7 +94,8 @@ pub trait KomodoResource {
|
||||
+ Serialize
|
||||
+ Diff
|
||||
+ MaybeNone;
|
||||
type Info: Send
|
||||
type Info: Clone
|
||||
+ Send
|
||||
+ Sync
|
||||
+ Unpin
|
||||
+ Default
|
||||
@@ -274,7 +279,7 @@ pub async fn get_resource_ids_for_user<T: KomodoResource>(
|
||||
))),
|
||||
// And any ids using the permissions table
|
||||
find_collect(
|
||||
&db_client().await.permissions,
|
||||
&db_client().permissions,
|
||||
doc! {
|
||||
"$or": user_target_query(&user.id, &groups)?,
|
||||
"resource_target.type": resource_type.as_ref(),
|
||||
@@ -351,7 +356,7 @@ pub async fn get_user_permission_on_resource<T: KomodoResource>(
|
||||
|
||||
// Overlay any specific permissions
|
||||
let permission = find_collect(
|
||||
&db_client().await.permissions,
|
||||
&db_client().permissions,
|
||||
doc! {
|
||||
"$or": user_target_query(&user.id, &groups)?,
|
||||
"resource_target.type": resource_type.as_ref(),
|
||||
@@ -435,6 +440,8 @@ pub type IdResourceMap<T> = HashMap<
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn get_id_to_resource_map<T: KomodoResource>(
|
||||
id_to_tags: &HashMap<String, Tag>,
|
||||
match_tags: &[String],
|
||||
) -> anyhow::Result<IdResourceMap<T>> {
|
||||
let res = find_collect(T::coll().await, None, None)
|
||||
.await
|
||||
@@ -442,6 +449,34 @@ pub async fn get_id_to_resource_map<T: KomodoResource>(
|
||||
format!("failed to pull {}s from mongo", T::resource_type())
|
||||
})?
|
||||
.into_iter()
|
||||
.filter(|resource| {
|
||||
if match_tags.is_empty() {
|
||||
return true;
|
||||
}
|
||||
for tag in match_tags.iter() {
|
||||
for resource_tag in &resource.tags {
|
||||
match ObjectId::from_str(resource_tag) {
|
||||
Ok(_) => match id_to_tags
|
||||
.get(resource_tag)
|
||||
.map(|tag| tag.name.as_str())
|
||||
{
|
||||
Some(name) => {
|
||||
if tag != name {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
None => return false,
|
||||
},
|
||||
Err(_) => {
|
||||
if resource_tag != tag {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
})
|
||||
.map(|r| (r.id.clone(), r))
|
||||
.collect();
|
||||
Ok(res)
|
||||
@@ -473,6 +508,17 @@ pub async fn create<T: KomodoResource>(
|
||||
return Err(anyhow!("valid ObjectIds cannot be used as names."));
|
||||
}
|
||||
|
||||
// Ensure an existing resource with same name doesn't already exist
|
||||
// The database indexing also ensures this but doesn't give a good error message.
|
||||
if list_full_for_user::<T>(Default::default(), system_user())
|
||||
.await
|
||||
.context("Failed to list all resources for duplicate name check")?
|
||||
.into_iter()
|
||||
.any(|r| r.name == name)
|
||||
{
|
||||
return Err(anyhow!("Must provide unique name for resource."));
|
||||
}
|
||||
|
||||
let start_ts = komodo_timestamp();
|
||||
|
||||
T::validate_create_config(&mut config, user).await?;
|
||||
@@ -765,7 +811,6 @@ where
|
||||
let target: ResourceTarget = target.into();
|
||||
let (variant, id) = target.extract_variant_id();
|
||||
if let Err(e) = db_client()
|
||||
.await
|
||||
.permissions
|
||||
.delete_many(doc! {
|
||||
"resource_target.type": variant.as_ref(),
|
||||
@@ -799,7 +844,6 @@ where
|
||||
ResourceTarget::System(_) => return,
|
||||
};
|
||||
if let Err(e) = db_client()
|
||||
.await
|
||||
.users
|
||||
.update_many(
|
||||
doc! {},
|
||||
|
||||
@@ -27,7 +27,10 @@ use mungos::{
|
||||
mongodb::{bson::doc, options::FindOneOptions, Collection},
|
||||
};
|
||||
|
||||
use crate::state::{action_states, db_client, procedure_state_cache};
|
||||
use crate::{
|
||||
config::core_config,
|
||||
state::{action_states, db_client, procedure_state_cache},
|
||||
};
|
||||
|
||||
impl super::KomodoResource for Procedure {
|
||||
type Config = ProcedureConfig;
|
||||
@@ -43,7 +46,7 @@ impl super::KomodoResource for Procedure {
|
||||
|
||||
async fn coll(
|
||||
) -> &'static Collection<Resource<Self::Config, Self::Info>> {
|
||||
&db_client().await.procedures
|
||||
&db_client().procedures
|
||||
}
|
||||
|
||||
async fn to_list_item(
|
||||
@@ -77,8 +80,8 @@ impl super::KomodoResource for Procedure {
|
||||
Operation::CreateProcedure
|
||||
}
|
||||
|
||||
fn user_can_create(_user: &User) -> bool {
|
||||
true
|
||||
fn user_can_create(user: &User) -> bool {
|
||||
user.admin || !core_config().disable_non_admin_create
|
||||
}
|
||||
|
||||
async fn validate_create_config(
|
||||
@@ -455,6 +458,24 @@ async fn validate_config(
|
||||
.await?;
|
||||
params.server = server.id;
|
||||
}
|
||||
Execution::PruneDockerBuilders(params) => {
|
||||
let server = super::get_check_permissions::<Server>(
|
||||
¶ms.server,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.await?;
|
||||
params.server = server.id;
|
||||
}
|
||||
Execution::PruneBuildx(params) => {
|
||||
let server = super::get_check_permissions::<Server>(
|
||||
¶ms.server,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.await?;
|
||||
params.server = server.id;
|
||||
}
|
||||
Execution::PruneSystem(params) => {
|
||||
let server = super::get_check_permissions::<Server>(
|
||||
¶ms.server,
|
||||
@@ -556,7 +577,7 @@ pub fn spawn_procedure_state_refresh_loop() {
|
||||
pub async fn refresh_procedure_state_cache() {
|
||||
let _ = async {
|
||||
let procedures =
|
||||
find_collect(&db_client().await.procedures, None, None)
|
||||
find_collect(&db_client().procedures, None, None)
|
||||
.await
|
||||
.context("failed to get procedures from db")?;
|
||||
let cache = procedure_state_cache();
|
||||
@@ -591,7 +612,6 @@ async fn get_procedure_state(id: &String) -> ProcedureState {
|
||||
async fn get_procedure_state_from_db(id: &str) -> ProcedureState {
|
||||
async {
|
||||
let state = db_client()
|
||||
.await
|
||||
.updates
|
||||
.find_one(doc! {
|
||||
"target.type": "Procedure",
|
||||
|
||||
140
bin/core/src/resource/refresh.rs
Normal file
140
bin/core/src/resource/refresh.rs
Normal file
@@ -0,0 +1,140 @@
|
||||
use async_timing_util::{wait_until_timelength, Timelength};
|
||||
use komodo_client::{
|
||||
api::write::{
|
||||
RefreshBuildCache, RefreshRepoCache, RefreshResourceSyncPending,
|
||||
RefreshStackCache,
|
||||
},
|
||||
entities::user::{build_user, repo_user, stack_user, sync_user},
|
||||
};
|
||||
use mungos::find::find_collect;
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
state::{db_client, State},
|
||||
};
|
||||
|
||||
pub fn spawn_resource_refresh_loop() {
|
||||
let interval: Timelength = core_config()
|
||||
.resource_poll_interval
|
||||
.try_into()
|
||||
.expect("Invalid resource poll interval");
|
||||
tokio::spawn(async move {
|
||||
refresh_all().await;
|
||||
loop {
|
||||
wait_until_timelength(interval, 3000).await;
|
||||
refresh_all().await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn refresh_all() {
|
||||
refresh_stacks().await;
|
||||
refresh_builds().await;
|
||||
refresh_repos().await;
|
||||
refresh_syncs().await;
|
||||
}
|
||||
|
||||
async fn refresh_stacks() {
|
||||
let Ok(stacks) = find_collect(&db_client().stacks, None, None)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
warn!(
|
||||
"Failed to get Stacks from database in refresh task | {e:#}"
|
||||
)
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
for stack in stacks {
|
||||
State
|
||||
.resolve(
|
||||
RefreshStackCache { stack: stack.id },
|
||||
stack_user().clone(),
|
||||
)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
warn!("Failed to refresh Stack cache in refresh task | Stack: {} | {e:#}", stack.name)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
async fn refresh_builds() {
|
||||
let Ok(builds) = find_collect(&db_client().builds, None, None)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
warn!(
|
||||
"Failed to get Builds from database in refresh task | {e:#}"
|
||||
)
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
for build in builds {
|
||||
State
|
||||
.resolve(
|
||||
RefreshBuildCache { build: build.id },
|
||||
build_user().clone(),
|
||||
)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
warn!("Failed to refresh Build cache in refresh task | Build: {} | {e:#}", build.name)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
async fn refresh_repos() {
|
||||
let Ok(repos) = find_collect(&db_client().repos, None, None)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
warn!(
|
||||
"Failed to get Repos from database in refresh task | {e:#}"
|
||||
)
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
for repo in repos {
|
||||
State
|
||||
.resolve(
|
||||
RefreshRepoCache { repo: repo.id },
|
||||
repo_user().clone(),
|
||||
)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
warn!("Failed to refresh Repo cache in refresh task | Repo: {} | {e:#}", repo.name)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
async fn refresh_syncs() {
|
||||
let Ok(syncs) =
|
||||
find_collect(&db_client().resource_syncs, None, None)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
warn!(
|
||||
"failed to get resource syncs from db in refresh task | {e:#}"
|
||||
)
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
for sync in syncs {
|
||||
if sync.config.repo.is_empty() {
|
||||
continue;
|
||||
}
|
||||
State
|
||||
.resolve(
|
||||
RefreshResourceSyncPending { sync: sync.id },
|
||||
sync_user().clone(),
|
||||
)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
warn!("Failed to refresh ResourceSync in refresh task | Sync: {} | {e:#}", sync.name)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ use mungos::{
|
||||
use periphery_client::api::git::DeleteRepo;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::periphery_client,
|
||||
state::{
|
||||
action_states, db_client, repo_state_cache, repo_status_cache,
|
||||
@@ -44,7 +45,7 @@ impl super::KomodoResource for Repo {
|
||||
|
||||
async fn coll(
|
||||
) -> &'static Collection<Resource<Self::Config, Self::Info>> {
|
||||
&db_client().await.repos
|
||||
&db_client().repos
|
||||
}
|
||||
|
||||
async fn to_list_item(
|
||||
@@ -90,8 +91,8 @@ impl super::KomodoResource for Repo {
|
||||
Operation::CreateRepo
|
||||
}
|
||||
|
||||
fn user_can_create(_user: &User) -> bool {
|
||||
true
|
||||
fn user_can_create(user: &User) -> bool {
|
||||
user.admin || !core_config().disable_non_admin_create
|
||||
}
|
||||
|
||||
async fn validate_create_config(
|
||||
@@ -183,7 +184,7 @@ pub fn spawn_repo_state_refresh_loop() {
|
||||
|
||||
pub async fn refresh_repo_state_cache() {
|
||||
let _ = async {
|
||||
let repos = find_collect(&db_client().await.repos, None, None)
|
||||
let repos = find_collect(&db_client().repos, None, None)
|
||||
.await
|
||||
.context("failed to get repos from db")?;
|
||||
let cache = repo_state_cache();
|
||||
@@ -257,7 +258,6 @@ async fn get_repo_state(id: &String) -> RepoState {
|
||||
async fn get_repo_state_from_db(id: &str) -> RepoState {
|
||||
async {
|
||||
let state = db_client()
|
||||
.await
|
||||
.updates
|
||||
.find_one(doc! {
|
||||
"target.type": "Repo",
|
||||
|
||||
@@ -13,6 +13,7 @@ use komodo_client::entities::{
|
||||
use mungos::mongodb::{bson::doc, Collection};
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
monitor::update_cache_for_server,
|
||||
state::{action_states, db_client, server_status_cache},
|
||||
};
|
||||
@@ -31,7 +32,7 @@ impl super::KomodoResource for Server {
|
||||
|
||||
async fn coll(
|
||||
) -> &'static Collection<Resource<Self::Config, Self::Info>> {
|
||||
&db_client().await.servers
|
||||
&db_client().servers
|
||||
}
|
||||
|
||||
async fn to_list_item(
|
||||
@@ -72,7 +73,9 @@ impl super::KomodoResource for Server {
|
||||
}
|
||||
|
||||
fn user_can_create(user: &User) -> bool {
|
||||
user.admin || user.create_server_permissions
|
||||
user.admin
|
||||
|| (!core_config().disable_non_admin_create
|
||||
&& user.create_server_permissions)
|
||||
}
|
||||
|
||||
async fn validate_create_config(
|
||||
@@ -122,7 +125,7 @@ impl super::KomodoResource for Server {
|
||||
resource: &Resource<Self::Config, Self::Info>,
|
||||
_update: &mut Update,
|
||||
) -> anyhow::Result<()> {
|
||||
let db = db_client().await;
|
||||
let db = db_client();
|
||||
|
||||
let id = &resource.id;
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ impl super::KomodoResource for ServerTemplate {
|
||||
|
||||
async fn coll(
|
||||
) -> &'static Collection<Resource<Self::Config, Self::Info>> {
|
||||
&db_client().await.server_templates
|
||||
&db_client().server_templates
|
||||
}
|
||||
|
||||
async fn to_list_item(
|
||||
|
||||
@@ -21,6 +21,7 @@ use periphery_client::api::compose::ComposeExecution;
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::{periphery_client, query::get_stack_state},
|
||||
monitor::update_cache_for_server,
|
||||
state::{
|
||||
@@ -45,7 +46,7 @@ impl super::KomodoResource for Stack {
|
||||
|
||||
async fn coll(
|
||||
) -> &'static Collection<Resource<Self::Config, Self::Info>> {
|
||||
&db_client().await.stacks
|
||||
&db_client().stacks
|
||||
}
|
||||
|
||||
async fn to_list_item(
|
||||
@@ -107,8 +108,10 @@ impl super::KomodoResource for Stack {
|
||||
status,
|
||||
services,
|
||||
project_missing,
|
||||
file_contents: !stack.config.file_contents.is_empty(),
|
||||
server_id: stack.config.server_id,
|
||||
missing_files: stack.info.missing_files,
|
||||
files_on_host: stack.config.files_on_host,
|
||||
git_provider: stack.config.git_provider,
|
||||
repo: stack.config.repo,
|
||||
branch: stack.config.branch,
|
||||
@@ -134,7 +137,7 @@ impl super::KomodoResource for Stack {
|
||||
}
|
||||
|
||||
fn user_can_create(user: &User) -> bool {
|
||||
user.admin
|
||||
user.admin || !core_config().disable_non_admin_create
|
||||
}
|
||||
|
||||
async fn validate_create_config(
|
||||
@@ -158,7 +161,7 @@ impl super::KomodoResource for Stack {
|
||||
.await
|
||||
{
|
||||
update.push_error_log(
|
||||
"refresh stack cache",
|
||||
"Refresh stack cache",
|
||||
format_serror(&e.context("The stack cache has failed to refresh. This is likely due to a misconfiguration of the Stack").into())
|
||||
);
|
||||
};
|
||||
@@ -320,7 +323,7 @@ async fn validate_config(
|
||||
// pub async fn refresh_resource_sync_state_cache() {
|
||||
// let _ = async {
|
||||
// let resource_syncs =
|
||||
// find_collect(&db_client().await.resource_syncs, None, None)
|
||||
// find_collect(&db_client().resource_syncs, None, None)
|
||||
// .await
|
||||
// .context("failed to get resource_syncs from db")?;
|
||||
// let cache = resource_sync_state_cache();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user