mirror of
https://github.com/moghtech/komodo.git
synced 2026-03-15 21:21:05 -05:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d05c81864e | ||
|
|
f1a09f34ab | ||
|
|
23c6e6306d | ||
|
|
800da90561 | ||
|
|
b24bf6ed89 | ||
|
|
d66a781a13 | ||
|
|
f9b2994d44 | ||
|
|
c0d6d96b64 | ||
|
|
34496b948a | ||
|
|
90c6adf923 |
33
.devcontainer/dev.compose.yaml
Normal file
33
.devcontainer/dev.compose.yaml
Normal file
@@ -0,0 +1,33 @@
|
||||
services:
|
||||
dev:
|
||||
image: mcr.microsoft.com/devcontainers/rust:1-1-bullseye
|
||||
volumes:
|
||||
# Mount the root folder that contains .git
|
||||
- ../:/workspace:cached
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /proc:/proc
|
||||
- repos:/etc/komodo/repos
|
||||
- stacks:/etc/komodo/stacks
|
||||
command: sleep infinity
|
||||
ports:
|
||||
- "9121:9121"
|
||||
environment:
|
||||
KOMODO_FIRST_SERVER: http://localhost:8120
|
||||
KOMODO_DATABASE_ADDRESS: db
|
||||
KOMODO_ENABLE_NEW_USERS: true
|
||||
KOMODO_LOCAL_AUTH: true
|
||||
KOMODO_JWT_SECRET: a_random_secret
|
||||
links:
|
||||
- db
|
||||
# ...
|
||||
|
||||
db:
|
||||
extends:
|
||||
file: ../test.compose.yaml
|
||||
service: ferretdb
|
||||
|
||||
volumes:
|
||||
data:
|
||||
repo-cache:
|
||||
repos:
|
||||
stacks:
|
||||
46
.devcontainer/devcontainer.json
Normal file
46
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,46 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/rust
|
||||
{
|
||||
"name": "Komodo",
|
||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||
//"image": "mcr.microsoft.com/devcontainers/rust:1-1-bullseye",
|
||||
"dockerComposeFile": ["dev.compose.yaml"],
|
||||
"workspaceFolder": "/workspace",
|
||||
"service": "dev",
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"version": "18.18.0"
|
||||
},
|
||||
"ghcr.io/devcontainers-community/features/deno:1": {
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
// Use 'mounts' to make the cargo cache persistent in a Docker Volume.
|
||||
"mounts": [
|
||||
{
|
||||
"source": "devcontainer-cargo-cache-${devcontainerId}",
|
||||
"target": "/usr/local/cargo",
|
||||
"type": "volume"
|
||||
}
|
||||
],
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
"forwardPorts": [
|
||||
9121
|
||||
],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "./.devcontainer/postCreate.sh",
|
||||
|
||||
"runServices": [
|
||||
"db"
|
||||
]
|
||||
|
||||
// Configure tool-specific properties.
|
||||
// "customizations": {},
|
||||
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "root"
|
||||
}
|
||||
3
.devcontainer/postCreate.sh
Executable file
3
.devcontainer/postCreate.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
cargo install typeshare-cli
|
||||
8
.vscode/extensions.json
vendored
Normal file
8
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"rust-lang.rust-analyzer",
|
||||
"tamasfe.even-better-toml",
|
||||
"vadimcn.vscode-lldb",
|
||||
"denoland.vscode-deno"
|
||||
]
|
||||
}
|
||||
179
.vscode/tasks.json
vendored
Normal file
179
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,179 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Run Core",
|
||||
"command": "cargo",
|
||||
"args": [
|
||||
"run",
|
||||
"-p",
|
||||
"komodo_core",
|
||||
"--release"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {
|
||||
"KOMODO_CONFIG_PATH": "test.core.config.toml"
|
||||
}
|
||||
},
|
||||
"problemMatcher": [
|
||||
"$rustc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Build Core",
|
||||
"command": "cargo",
|
||||
"args": [
|
||||
"build",
|
||||
"-p",
|
||||
"komodo_core",
|
||||
"--release"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {
|
||||
"KOMODO_CONFIG_PATH": "test.core.config.toml"
|
||||
}
|
||||
},
|
||||
"problemMatcher": [
|
||||
"$rustc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Run Periphery",
|
||||
"command": "cargo",
|
||||
"args": [
|
||||
"run",
|
||||
"-p",
|
||||
"komodo_periphery",
|
||||
"--release"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {
|
||||
"KOMODO_CONFIG_PATH": "test.periphery.config.toml"
|
||||
}
|
||||
},
|
||||
"problemMatcher": [
|
||||
"$rustc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Build Periphery",
|
||||
"command": "cargo",
|
||||
"args": [
|
||||
"build",
|
||||
"-p",
|
||||
"komodo_periphery",
|
||||
"--release"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {
|
||||
"KOMODO_CONFIG_PATH": "test.periphery.config.toml"
|
||||
}
|
||||
},
|
||||
"problemMatcher": [
|
||||
"$rustc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Run Backend",
|
||||
"dependsOn": [
|
||||
"Run Core",
|
||||
"Run Periphery"
|
||||
],
|
||||
"problemMatcher": [
|
||||
"$rustc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Build TS Client Types",
|
||||
"type": "process",
|
||||
"command": "node",
|
||||
"args": [
|
||||
"./client/core/ts/generate_types.mjs"
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Init TS Client",
|
||||
"type": "shell",
|
||||
"command": "yarn && yarn build && yarn link",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/client/core/ts",
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Init Frontend Client",
|
||||
"type": "shell",
|
||||
"command": "yarn link komodo_client && yarn install",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/frontend",
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Init Frontend",
|
||||
"dependsOn": [
|
||||
"Build TS Client Types",
|
||||
"Init TS Client",
|
||||
"Init Frontend Client"
|
||||
],
|
||||
"dependsOrder": "sequence",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Build Frontend",
|
||||
"type": "shell",
|
||||
"command": "yarn build",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/frontend",
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Prepare Frontend For Run",
|
||||
"type": "shell",
|
||||
"command": "cp -r ./client/core/ts/dist/. frontend/public/client/.",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}",
|
||||
},
|
||||
"dependsOn": [
|
||||
"Build TS Client Types",
|
||||
"Build Frontend"
|
||||
],
|
||||
"dependsOrder": "sequence",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Run Frontend",
|
||||
"type": "shell",
|
||||
"command": "yarn dev",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/frontend",
|
||||
},
|
||||
"dependsOn": ["Prepare Frontend For Run"],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Init",
|
||||
"dependsOn": [
|
||||
"Build Backend",
|
||||
"Init Frontend"
|
||||
],
|
||||
"dependsOrder": "sequence",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Run Komodo",
|
||||
"dependsOn": [
|
||||
"Run Core",
|
||||
"Run Periphery",
|
||||
"Run Frontend"
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
]
|
||||
}
|
||||
24
Cargo.lock
generated
24
Cargo.lock
generated
@@ -41,7 +41,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "alerter"
|
||||
version = "1.16.1"
|
||||
version = "1.16.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -943,7 +943,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "command"
|
||||
version = "1.16.1"
|
||||
version = "1.16.3"
|
||||
dependencies = [
|
||||
"komodo_client",
|
||||
"run_command",
|
||||
@@ -1355,7 +1355,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "environment_file"
|
||||
version = "1.16.1"
|
||||
version = "1.16.3"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
@@ -1439,7 +1439,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "formatting"
|
||||
version = "1.16.1"
|
||||
version = "1.16.3"
|
||||
dependencies = [
|
||||
"serror",
|
||||
]
|
||||
@@ -1571,7 +1571,7 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||
|
||||
[[package]]
|
||||
name = "git"
|
||||
version = "1.16.1"
|
||||
version = "1.16.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"command",
|
||||
@@ -2191,7 +2191,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_cli"
|
||||
version = "1.16.1"
|
||||
version = "1.16.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -2207,7 +2207,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_client"
|
||||
version = "1.16.1"
|
||||
version = "1.16.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -2238,7 +2238,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_core"
|
||||
version = "1.16.1"
|
||||
version = "1.16.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -2296,7 +2296,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_periphery"
|
||||
version = "1.16.1"
|
||||
version = "1.16.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -2383,7 +2383,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logger"
|
||||
version = "1.16.1"
|
||||
version = "1.16.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"komodo_client",
|
||||
@@ -3089,7 +3089,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||
|
||||
[[package]]
|
||||
name = "periphery_client"
|
||||
version = "1.16.1"
|
||||
version = "1.16.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"komodo_client",
|
||||
@@ -4863,7 +4863,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "update_logger"
|
||||
version = "1.16.1"
|
||||
version = "1.16.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"komodo_client",
|
||||
|
||||
@@ -9,7 +9,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.16.1"
|
||||
version = "1.16.3"
|
||||
edition = "2021"
|
||||
authors = ["mbecker20 <becker.maxh@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
@@ -97,7 +97,10 @@ impl Resolve<RunAction, (User, Update)> for State {
|
||||
// Keep this stage name as is, the UI will find the latest update log by matching the stage name
|
||||
"Execute Action",
|
||||
None,
|
||||
format!("deno run --allow-read --allow-net --allow-import {}", path.display()),
|
||||
format!(
|
||||
"deno run --allow-read --allow-net --allow-import {}",
|
||||
path.display()
|
||||
),
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -69,15 +69,16 @@ impl Resolve<GetAlertersSummary, User> for State {
|
||||
GetAlertersSummary {}: GetAlertersSummary,
|
||||
user: User,
|
||||
) -> anyhow::Result<GetAlertersSummaryResponse> {
|
||||
let query =
|
||||
match resource::get_resource_object_ids_for_user::<Alerter>(&user)
|
||||
.await?
|
||||
{
|
||||
Some(ids) => doc! {
|
||||
"_id": { "$in": ids }
|
||||
},
|
||||
None => Document::new(),
|
||||
};
|
||||
let query = match resource::get_resource_object_ids_for_user::<
|
||||
Alerter,
|
||||
>(&user)
|
||||
.await?
|
||||
{
|
||||
Some(ids) => doc! {
|
||||
"_id": { "$in": ids }
|
||||
},
|
||||
None => Document::new(),
|
||||
};
|
||||
let total = db_client()
|
||||
.alerters
|
||||
.count_documents(query)
|
||||
|
||||
@@ -69,15 +69,16 @@ impl Resolve<GetBuildersSummary, User> for State {
|
||||
GetBuildersSummary {}: GetBuildersSummary,
|
||||
user: User,
|
||||
) -> anyhow::Result<GetBuildersSummaryResponse> {
|
||||
let query =
|
||||
match resource::get_resource_object_ids_for_user::<Builder>(&user)
|
||||
.await?
|
||||
{
|
||||
Some(ids) => doc! {
|
||||
"_id": { "$in": ids }
|
||||
},
|
||||
None => Document::new(),
|
||||
};
|
||||
let query = match resource::get_resource_object_ids_for_user::<
|
||||
Builder,
|
||||
>(&user)
|
||||
.await?
|
||||
{
|
||||
Some(ids) => doc! {
|
||||
"_id": { "$in": ids }
|
||||
},
|
||||
None => Document::new(),
|
||||
};
|
||||
let total = db_client()
|
||||
.builders
|
||||
.count_documents(query)
|
||||
|
||||
@@ -6,7 +6,7 @@ use komodo_client::{
|
||||
ListApiKeysForServiceUserResponse, ListApiKeysResponse,
|
||||
ListUsers, ListUsersResponse,
|
||||
},
|
||||
entities::user::{User, UserConfig},
|
||||
entities::user::{admin_service_user, User, UserConfig},
|
||||
};
|
||||
use mungos::{
|
||||
by_id::find_one_by_id,
|
||||
@@ -26,6 +26,13 @@ impl Resolve<GetUsername, User> for State {
|
||||
GetUsername { user_id }: GetUsername,
|
||||
_: User,
|
||||
) -> anyhow::Result<GetUsernameResponse> {
|
||||
if let Some(user) = admin_service_user(&user_id) {
|
||||
return Ok(GetUsernameResponse {
|
||||
username: user.username,
|
||||
avatar: None,
|
||||
});
|
||||
}
|
||||
|
||||
let user = find_one_by_id(&db_client().users, &user_id)
|
||||
.await
|
||||
.context("failed at mongo query for user")?
|
||||
|
||||
@@ -130,7 +130,7 @@ impl Resolve<RenameRepo, User> for State {
|
||||
|
||||
let server =
|
||||
resource::get::<Server>(&repo.config.server_id).await?;
|
||||
|
||||
|
||||
let log = match periphery_client(&server)?
|
||||
.request(api::git::RenameRepo {
|
||||
curr_name: to_komodo_name(&repo.name),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{str::FromStr, time::Duration};
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use futures::future::join_all;
|
||||
@@ -54,10 +54,6 @@ pub fn empty_or_only_spaces(word: &str) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
pub fn random_duration(min_ms: u64, max_ms: u64) -> Duration {
|
||||
Duration::from_millis(thread_rng().gen_range(min_ms..max_ms))
|
||||
}
|
||||
|
||||
pub fn random_string(length: usize) -> String {
|
||||
thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use axum::http::HeaderMap;
|
||||
use komodo_client::{
|
||||
api::execute::RunBuild,
|
||||
entities::{build::Build, user::git_webhook_user},
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCode;
|
||||
|
||||
use crate::{
|
||||
api::execute::ExecuteRequest,
|
||||
helpers::update::init_execution_update, resource, state::State,
|
||||
};
|
||||
|
||||
use super::{extract_branch, verify_gh_signature, ListenerLockCache};
|
||||
|
||||
fn build_locks() -> &'static ListenerLockCache {
|
||||
static BUILD_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();
|
||||
BUILD_LOCKS.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
pub async fn auth_build_webhook(
|
||||
build_id: &str,
|
||||
headers: HeaderMap,
|
||||
body: &str,
|
||||
) -> serror::Result<Build> {
|
||||
let build = resource::get::<Build>(build_id)
|
||||
.await
|
||||
.status_code(StatusCode::NOT_FOUND)?;
|
||||
verify_gh_signature(headers, body, &build.config.webhook_secret)
|
||||
.await
|
||||
.status_code(StatusCode::UNAUTHORIZED)?;
|
||||
Ok(build)
|
||||
}
|
||||
|
||||
pub async fn handle_build_webhook(
|
||||
build: Build,
|
||||
body: String,
|
||||
) -> anyhow::Result<()> {
|
||||
// Acquire and hold lock to make a task queue for
|
||||
// subsequent listener calls on same resource.
|
||||
// It would fail if we let it go through from action state busy.
|
||||
let lock = build_locks().get_or_insert_default(&build.id).await;
|
||||
let _lock = lock.lock().await;
|
||||
|
||||
if !build.config.webhook_enabled {
|
||||
return Err(anyhow!("build does not have webhook enabled"));
|
||||
}
|
||||
|
||||
let request_branch = extract_branch(&body)?;
|
||||
if request_branch != build.config.branch {
|
||||
return Err(anyhow!("request branch does not match expected"));
|
||||
}
|
||||
|
||||
let user = git_webhook_user().to_owned();
|
||||
let req = ExecuteRequest::RunBuild(RunBuild { build: build.id });
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::RunBuild(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
State.resolve(req, (user, update)).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,278 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use axum::{extract::Path, http::HeaderMap, routing::post, Router};
|
||||
use hex::ToHex;
|
||||
use hmac::{Hmac, Mac};
|
||||
use serde::Deserialize;
|
||||
use sha2::Sha256;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::Instrument;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::{cache::Cache, random_duration},
|
||||
};
|
||||
|
||||
mod build;
|
||||
mod procedure;
|
||||
mod repo;
|
||||
mod stack;
|
||||
mod sync;
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Id {
|
||||
id: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct IdBranch {
|
||||
id: String,
|
||||
branch: Option<String>,
|
||||
}
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/build/:id",
|
||||
post(
|
||||
|Path(Id { id }), headers: HeaderMap, body: String| async move {
|
||||
let build = build::auth_build_webhook(&id, headers, &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("build_webhook", id);
|
||||
async {
|
||||
let res = build::handle_build_webhook(build, body).await;
|
||||
if let Err(e) = res {
|
||||
warn!("failed to run build webook for build {id} | {e:#}");
|
||||
}
|
||||
}
|
||||
.instrument(span)
|
||||
.await
|
||||
});
|
||||
serror::Result::Ok(())
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/repo/:id/clone",
|
||||
post(
|
||||
|Path(Id { id }), headers: HeaderMap, body: String| async move {
|
||||
let repo = repo::auth_repo_webhook(&id, headers, &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("repo_clone_webhook", id);
|
||||
async {
|
||||
let res = repo::handle_repo_clone_webhook(repo, body).await;
|
||||
if let Err(e) = res {
|
||||
warn!("failed to run repo clone webook for repo {id} | {e:#}");
|
||||
}
|
||||
}
|
||||
.instrument(span)
|
||||
.await
|
||||
});
|
||||
serror::Result::Ok(())
|
||||
},
|
||||
)
|
||||
)
|
||||
.route(
|
||||
"/repo/:id/pull",
|
||||
post(
|
||||
|Path(Id { id }), headers: HeaderMap, body: String| async move {
|
||||
let repo = repo::auth_repo_webhook(&id, headers, &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("repo_pull_webhook", id);
|
||||
async {
|
||||
let res = repo::handle_repo_pull_webhook(repo, body).await;
|
||||
if let Err(e) = res {
|
||||
warn!("failed to run repo pull webook for repo {id} | {e:#}");
|
||||
}
|
||||
}
|
||||
.instrument(span)
|
||||
.await
|
||||
});
|
||||
serror::Result::Ok(())
|
||||
},
|
||||
)
|
||||
)
|
||||
.route(
|
||||
"/repo/:id/build",
|
||||
post(
|
||||
|Path(Id { id }), headers: HeaderMap, body: String| async move {
|
||||
let repo = repo::auth_repo_webhook(&id, headers, &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("repo_build_webhook", id);
|
||||
async {
|
||||
let res = repo::handle_repo_build_webhook(repo, body).await;
|
||||
if let Err(e) = res {
|
||||
warn!("failed to run repo build webook for repo {id} | {e:#}");
|
||||
}
|
||||
}
|
||||
.instrument(span)
|
||||
.await
|
||||
});
|
||||
serror::Result::Ok(())
|
||||
},
|
||||
)
|
||||
)
|
||||
.route(
|
||||
"/stack/:id/refresh",
|
||||
post(
|
||||
|Path(Id { id }), headers: HeaderMap, body: String| async move {
|
||||
let stack = stack::auth_stack_webhook(&id, headers, &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("stack_clone_webhook", id);
|
||||
async {
|
||||
let res = stack::handle_stack_refresh_webhook(stack, body).await;
|
||||
if let Err(e) = res {
|
||||
warn!("failed to run stack clone webook for stack {id} | {e:#}");
|
||||
}
|
||||
}
|
||||
.instrument(span)
|
||||
.await
|
||||
});
|
||||
serror::Result::Ok(())
|
||||
},
|
||||
)
|
||||
)
|
||||
.route(
|
||||
"/stack/:id/deploy",
|
||||
post(
|
||||
|Path(Id { id }), headers: HeaderMap, body: String| async move {
|
||||
let stack = stack::auth_stack_webhook(&id, headers, &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("stack_pull_webhook", id);
|
||||
async {
|
||||
let res = stack::handle_stack_deploy_webhook(stack, body).await;
|
||||
if let Err(e) = res {
|
||||
warn!("failed to run stack pull webook for stack {id} | {e:#}");
|
||||
}
|
||||
}
|
||||
.instrument(span)
|
||||
.await
|
||||
});
|
||||
serror::Result::Ok(())
|
||||
},
|
||||
)
|
||||
)
|
||||
.route(
|
||||
"/procedure/:id/:branch",
|
||||
post(
|
||||
|Path(IdBranch { id, branch }), headers: HeaderMap, body: String| async move {
|
||||
let procedure = procedure::auth_procedure_webhook(&id, headers, &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("procedure_webhook", id, branch);
|
||||
async {
|
||||
let res = procedure::handle_procedure_webhook(
|
||||
procedure,
|
||||
branch.unwrap_or_else(|| String::from("main")),
|
||||
body
|
||||
).await;
|
||||
if let Err(e) = res {
|
||||
warn!("failed to run procedure webook for procedure {id} | {e:#}");
|
||||
}
|
||||
}
|
||||
.instrument(span)
|
||||
.await
|
||||
});
|
||||
serror::Result::Ok(())
|
||||
},
|
||||
)
|
||||
)
|
||||
.route(
|
||||
"/sync/:id/refresh",
|
||||
post(
|
||||
|Path(Id { id }), headers: HeaderMap, body: String| async move {
|
||||
let sync = sync::auth_sync_webhook(&id, headers, &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("sync_refresh_webhook", id);
|
||||
async {
|
||||
let res = sync::handle_sync_refresh_webhook(
|
||||
sync,
|
||||
body
|
||||
).await;
|
||||
if let Err(e) = res {
|
||||
warn!("failed to run sync webook for sync {id} | {e:#}");
|
||||
}
|
||||
}
|
||||
.instrument(span)
|
||||
.await
|
||||
});
|
||||
serror::Result::Ok(())
|
||||
},
|
||||
)
|
||||
)
|
||||
.route(
|
||||
"/sync/:id/sync",
|
||||
post(
|
||||
|Path(Id { id }), headers: HeaderMap, body: String| async move {
|
||||
let sync = sync::auth_sync_webhook(&id, headers, &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("sync_execute_webhook", id);
|
||||
async {
|
||||
let res = sync::handle_sync_execute_webhook(
|
||||
sync,
|
||||
body
|
||||
).await;
|
||||
if let Err(e) = res {
|
||||
warn!("failed to run sync webook for sync {id} | {e:#}");
|
||||
}
|
||||
}
|
||||
.instrument(span)
|
||||
.await
|
||||
});
|
||||
serror::Result::Ok(())
|
||||
},
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn verify_gh_signature(
|
||||
headers: HeaderMap,
|
||||
body: &str,
|
||||
custom_secret: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
// wait random amount of time
|
||||
tokio::time::sleep(random_duration(0, 500)).await;
|
||||
|
||||
let signature = headers.get("x-hub-signature-256");
|
||||
if signature.is_none() {
|
||||
return Err(anyhow!("no signature in headers"));
|
||||
}
|
||||
let signature = signature.unwrap().to_str();
|
||||
if signature.is_err() {
|
||||
return Err(anyhow!("failed to unwrap signature"));
|
||||
}
|
||||
let signature = signature.unwrap().replace("sha256=", "");
|
||||
let secret_bytes = if custom_secret.is_empty() {
|
||||
core_config().webhook_secret.as_bytes()
|
||||
} else {
|
||||
custom_secret.as_bytes()
|
||||
};
|
||||
let mut mac = HmacSha256::new_from_slice(secret_bytes)
|
||||
.expect("github webhook | failed to create hmac sha256");
|
||||
mac.update(body.as_bytes());
|
||||
let expected = mac.finalize().into_bytes().encode_hex::<String>();
|
||||
if signature == expected {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("signature does not equal expected"))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct GithubWebhookBody {
|
||||
#[serde(rename = "ref")]
|
||||
branch: String,
|
||||
}
|
||||
|
||||
fn extract_branch(body: &str) -> anyhow::Result<String> {
|
||||
let branch = serde_json::from_str::<GithubWebhookBody>(body)
|
||||
.context("failed to parse github request body")?
|
||||
.branch
|
||||
.replace("refs/heads/", "");
|
||||
Ok(branch)
|
||||
}
|
||||
|
||||
type ListenerLockCache = Cache<String, Arc<Mutex<()>>>;
|
||||
@@ -1,74 +0,0 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use axum::http::HeaderMap;
|
||||
use komodo_client::{
|
||||
api::execute::RunProcedure,
|
||||
entities::{procedure::Procedure, user::git_webhook_user},
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCode;
|
||||
|
||||
use crate::{
|
||||
api::execute::ExecuteRequest,
|
||||
helpers::update::init_execution_update, resource, state::State,
|
||||
};
|
||||
|
||||
use super::{extract_branch, verify_gh_signature, ListenerLockCache};
|
||||
|
||||
fn procedure_locks() -> &'static ListenerLockCache {
|
||||
static BUILD_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();
|
||||
BUILD_LOCKS.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
pub async fn auth_procedure_webhook(
|
||||
procedure_id: &str,
|
||||
headers: HeaderMap,
|
||||
body: &str,
|
||||
) -> serror::Result<Procedure> {
|
||||
let procedure = resource::get::<Procedure>(procedure_id)
|
||||
.await
|
||||
.status_code(StatusCode::NOT_FOUND)?;
|
||||
verify_gh_signature(
|
||||
headers,
|
||||
body,
|
||||
&procedure.config.webhook_secret,
|
||||
)
|
||||
.await
|
||||
.status_code(StatusCode::UNAUTHORIZED)?;
|
||||
Ok(procedure)
|
||||
}
|
||||
|
||||
pub async fn handle_procedure_webhook(
|
||||
procedure: Procedure,
|
||||
target_branch: String,
|
||||
body: String,
|
||||
) -> anyhow::Result<()> {
|
||||
// Acquire and hold lock to make a task queue for
|
||||
// subsequent listener calls on same resource.
|
||||
// It would fail if we let it go through from action state busy.
|
||||
let lock =
|
||||
procedure_locks().get_or_insert_default(&procedure.id).await;
|
||||
let _lock = lock.lock().await;
|
||||
|
||||
if !procedure.config.webhook_enabled {
|
||||
return Err(anyhow!("procedure does not have webhook enabled"));
|
||||
}
|
||||
|
||||
let request_branch = extract_branch(&body)?;
|
||||
if request_branch != target_branch {
|
||||
return Err(anyhow!("request branch does not match expected"));
|
||||
}
|
||||
|
||||
let user = git_webhook_user().to_owned();
|
||||
let req = ExecuteRequest::RunProcedure(RunProcedure {
|
||||
procedure: procedure.id,
|
||||
});
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::RunProcedure(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
State.resolve(req, (user, update)).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use axum::http::HeaderMap;
|
||||
use komodo_client::{
|
||||
api::execute::{BuildRepo, CloneRepo, PullRepo},
|
||||
entities::{repo::Repo, user::git_webhook_user},
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCode;
|
||||
|
||||
use crate::{
|
||||
helpers::update::init_execution_update, resource, state::State,
|
||||
};
|
||||
|
||||
use super::{extract_branch, verify_gh_signature, ListenerLockCache};
|
||||
|
||||
fn repo_locks() -> &'static ListenerLockCache {
|
||||
static REPO_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();
|
||||
REPO_LOCKS.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
pub async fn auth_repo_webhook(
|
||||
repo_id: &str,
|
||||
headers: HeaderMap,
|
||||
body: &str,
|
||||
) -> serror::Result<Repo> {
|
||||
let repo = resource::get::<Repo>(repo_id)
|
||||
.await
|
||||
.status_code(StatusCode::NOT_FOUND)?;
|
||||
verify_gh_signature(headers, body, &repo.config.webhook_secret)
|
||||
.await
|
||||
.status_code(StatusCode::UNAUTHORIZED)?;
|
||||
Ok(repo)
|
||||
}
|
||||
|
||||
pub async fn handle_repo_clone_webhook(
|
||||
repo: Repo,
|
||||
body: String,
|
||||
) -> anyhow::Result<()> {
|
||||
// Acquire and hold lock to make a task queue for
|
||||
// subsequent listener calls on same resource.
|
||||
// It would fail if we let it go through from action state busy.
|
||||
let lock = repo_locks().get_or_insert_default(&repo.id).await;
|
||||
let _lock = lock.lock().await;
|
||||
|
||||
if !repo.config.webhook_enabled {
|
||||
return Err(anyhow!("repo does not have webhook enabled"));
|
||||
}
|
||||
|
||||
let request_branch = extract_branch(&body)?;
|
||||
if request_branch != repo.config.branch {
|
||||
return Err(anyhow!("request branch does not match expected"));
|
||||
}
|
||||
|
||||
let user = git_webhook_user().to_owned();
|
||||
let req =
|
||||
crate::api::execute::ExecuteRequest::CloneRepo(CloneRepo {
|
||||
repo: repo.id,
|
||||
});
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let crate::api::execute::ExecuteRequest::CloneRepo(req) = req
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
State.resolve(req, (user, update)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_repo_pull_webhook(
|
||||
repo: Repo,
|
||||
body: String,
|
||||
) -> anyhow::Result<()> {
|
||||
// Acquire and hold lock to make a task queue for
|
||||
// subsequent listener calls on same resource.
|
||||
// It would fail if we let it go through from action state busy.
|
||||
let lock = repo_locks().get_or_insert_default(&repo.id).await;
|
||||
let _lock = lock.lock().await;
|
||||
|
||||
if !repo.config.webhook_enabled {
|
||||
return Err(anyhow!("repo does not have webhook enabled"));
|
||||
}
|
||||
|
||||
let request_branch = extract_branch(&body)?;
|
||||
if request_branch != repo.config.branch {
|
||||
return Err(anyhow!("request branch does not match expected"));
|
||||
}
|
||||
|
||||
let user = git_webhook_user().to_owned();
|
||||
let req = crate::api::execute::ExecuteRequest::PullRepo(PullRepo {
|
||||
repo: repo.id,
|
||||
});
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let crate::api::execute::ExecuteRequest::PullRepo(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
State.resolve(req, (user, update)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_repo_build_webhook(
|
||||
repo: Repo,
|
||||
body: String,
|
||||
) -> anyhow::Result<()> {
|
||||
// Acquire and hold lock to make a task queue for
|
||||
// subsequent listener calls on same resource.
|
||||
// It would fail if we let it go through from action state busy.
|
||||
let lock = repo_locks().get_or_insert_default(&repo.id).await;
|
||||
let _lock = lock.lock().await;
|
||||
|
||||
if !repo.config.webhook_enabled {
|
||||
return Err(anyhow!("repo does not have webhook enabled"));
|
||||
}
|
||||
|
||||
let request_branch = extract_branch(&body)?;
|
||||
if request_branch != repo.config.branch {
|
||||
return Err(anyhow!("request branch does not match expected"));
|
||||
}
|
||||
|
||||
let user = git_webhook_user().to_owned();
|
||||
let req =
|
||||
crate::api::execute::ExecuteRequest::BuildRepo(BuildRepo {
|
||||
repo: repo.id,
|
||||
});
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let crate::api::execute::ExecuteRequest::BuildRepo(req) = req
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
State.resolve(req, (user, update)).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use axum::http::HeaderMap;
|
||||
use komodo_client::{
|
||||
api::{
|
||||
execute::{DeployStack, DeployStackIfChanged},
|
||||
write::RefreshStackCache,
|
||||
},
|
||||
entities::{stack::Stack, user::git_webhook_user},
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCode;
|
||||
|
||||
use crate::{
|
||||
api::execute::ExecuteRequest,
|
||||
helpers::update::init_execution_update, resource, state::State,
|
||||
};
|
||||
|
||||
use super::{extract_branch, verify_gh_signature, ListenerLockCache};
|
||||
|
||||
fn stack_locks() -> &'static ListenerLockCache {
|
||||
static STACK_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();
|
||||
STACK_LOCKS.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
pub async fn auth_stack_webhook(
|
||||
stack_id: &str,
|
||||
headers: HeaderMap,
|
||||
body: &str,
|
||||
) -> serror::Result<Stack> {
|
||||
let stack = resource::get::<Stack>(stack_id)
|
||||
.await
|
||||
.status_code(StatusCode::NOT_FOUND)?;
|
||||
verify_gh_signature(headers, body, &stack.config.webhook_secret)
|
||||
.await
|
||||
.status_code(StatusCode::UNAUTHORIZED)?;
|
||||
Ok(stack)
|
||||
}
|
||||
|
||||
pub async fn handle_stack_refresh_webhook(
|
||||
stack: Stack,
|
||||
body: String,
|
||||
) -> anyhow::Result<()> {
|
||||
// Acquire and hold lock to make a task queue for
|
||||
// subsequent listener calls on same resource.
|
||||
// It would fail if we let it go through, from "action state busy".
|
||||
let lock = stack_locks().get_or_insert_default(&stack.id).await;
|
||||
let _lock = lock.lock().await;
|
||||
|
||||
if !stack.config.webhook_enabled {
|
||||
return Err(anyhow!("stack does not have webhook enabled"));
|
||||
}
|
||||
|
||||
let request_branch = extract_branch(&body)?;
|
||||
if request_branch != stack.config.branch {
|
||||
return Err(anyhow!("request branch does not match expected"));
|
||||
}
|
||||
|
||||
let user = git_webhook_user().to_owned();
|
||||
State
|
||||
.resolve(RefreshStackCache { stack: stack.id }, user)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_stack_deploy_webhook(
|
||||
stack: Stack,
|
||||
body: String,
|
||||
) -> anyhow::Result<()> {
|
||||
// Acquire and hold lock to make a task queue for
|
||||
// subsequent listener calls on same resource.
|
||||
// It would fail if we let it go through from action state busy.
|
||||
let lock = stack_locks().get_or_insert_default(&stack.id).await;
|
||||
let _lock = lock.lock().await;
|
||||
|
||||
if !stack.config.webhook_enabled {
|
||||
return Err(anyhow!("stack does not have webhook enabled"));
|
||||
}
|
||||
|
||||
let request_branch = extract_branch(&body)?;
|
||||
if request_branch != stack.config.branch {
|
||||
return Err(anyhow!("request branch does not match expected"));
|
||||
}
|
||||
|
||||
let user = git_webhook_user().to_owned();
|
||||
if stack.config.webhook_force_deploy {
|
||||
let req = ExecuteRequest::DeployStack(DeployStack {
|
||||
stack: stack.id,
|
||||
stop_time: None,
|
||||
});
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::DeployStack(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
State.resolve(req, (user, update)).await?;
|
||||
} else {
|
||||
let req =
|
||||
ExecuteRequest::DeployStackIfChanged(DeployStackIfChanged {
|
||||
stack: stack.id,
|
||||
stop_time: None,
|
||||
});
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::DeployStackIfChanged(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
State.resolve(req, (user, update)).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use axum::http::HeaderMap;
|
||||
use komodo_client::{
|
||||
api::{execute::RunSync, write::RefreshResourceSyncPending},
|
||||
entities::{sync::ResourceSync, user::git_webhook_user},
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCode;
|
||||
|
||||
use crate::{
|
||||
api::execute::ExecuteRequest,
|
||||
helpers::update::init_execution_update, resource, state::State,
|
||||
};
|
||||
|
||||
use super::{extract_branch, verify_gh_signature, ListenerLockCache};
|
||||
|
||||
fn sync_locks() -> &'static ListenerLockCache {
|
||||
static SYNC_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();
|
||||
SYNC_LOCKS.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
pub async fn auth_sync_webhook(
|
||||
sync_id: &str,
|
||||
headers: HeaderMap,
|
||||
body: &str,
|
||||
) -> serror::Result<ResourceSync> {
|
||||
let sync = resource::get::<ResourceSync>(sync_id)
|
||||
.await
|
||||
.status_code(StatusCode::NOT_FOUND)?;
|
||||
verify_gh_signature(headers, body, &sync.config.webhook_secret)
|
||||
.await
|
||||
.status_code(StatusCode::UNAUTHORIZED)?;
|
||||
Ok(sync)
|
||||
}
|
||||
|
||||
pub async fn handle_sync_refresh_webhook(
|
||||
sync: ResourceSync,
|
||||
body: String,
|
||||
) -> anyhow::Result<()> {
|
||||
// Acquire and hold lock to make a task queue for
|
||||
// subsequent listener calls on same resource.
|
||||
// It would fail if we let it go through from action state busy.
|
||||
let lock = sync_locks().get_or_insert_default(&sync.id).await;
|
||||
let _lock = lock.lock().await;
|
||||
|
||||
if !sync.config.webhook_enabled {
|
||||
return Err(anyhow!("sync does not have webhook enabled"));
|
||||
}
|
||||
|
||||
let request_branch = extract_branch(&body)?;
|
||||
if request_branch != sync.config.branch {
|
||||
return Err(anyhow!("request branch does not match expected"));
|
||||
}
|
||||
|
||||
let user = git_webhook_user().to_owned();
|
||||
State
|
||||
.resolve(RefreshResourceSyncPending { sync: sync.id }, user)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_sync_execute_webhook(
|
||||
sync: ResourceSync,
|
||||
body: String,
|
||||
) -> anyhow::Result<()> {
|
||||
// Acquire and hold lock to make a task queue for
|
||||
// subsequent listener calls on same resource.
|
||||
// It would fail if we let it go through from action state busy.
|
||||
let lock = sync_locks().get_or_insert_default(&sync.id).await;
|
||||
let _lock = lock.lock().await;
|
||||
|
||||
if !sync.config.webhook_enabled {
|
||||
return Err(anyhow!("sync does not have webhook enabled"));
|
||||
}
|
||||
|
||||
let request_branch = extract_branch(&body)?;
|
||||
if request_branch != sync.config.branch {
|
||||
return Err(anyhow!("request branch does not match expected"));
|
||||
}
|
||||
|
||||
let user = git_webhook_user().to_owned();
|
||||
let req = ExecuteRequest::RunSync(RunSync {
|
||||
sync: sync.id,
|
||||
resource_type: None,
|
||||
resources: None,
|
||||
});
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::RunSync(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
State.resolve(req, (user, update)).await?;
|
||||
Ok(())
|
||||
}
|
||||
71
bin/core/src/listener/integrations/github.rs
Normal file
71
bin/core/src/listener/integrations/github.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use axum::http::HeaderMap;
|
||||
use hex::ToHex;
|
||||
use hmac::{Hmac, Mac};
|
||||
use serde::Deserialize;
|
||||
use sha2::Sha256;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
listener::{VerifyBranch, VerifySecret},
|
||||
};
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
|
||||
/// Listener implementation for Github type API, including Gitea
|
||||
pub struct Github;
|
||||
|
||||
impl VerifySecret for Github {
|
||||
#[instrument("VerifyGithubSecret", skip_all)]
|
||||
fn verify_secret(
|
||||
headers: HeaderMap,
|
||||
body: &str,
|
||||
custom_secret: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let signature = headers
|
||||
.get("x-hub-signature-256")
|
||||
.context("No github signature in headers")?;
|
||||
let signature = signature
|
||||
.to_str()
|
||||
.context("Failed to get signature as string")?;
|
||||
let signature =
|
||||
signature.strip_prefix("sha256=").unwrap_or(signature);
|
||||
let secret_bytes = if custom_secret.is_empty() {
|
||||
core_config().webhook_secret.as_bytes()
|
||||
} else {
|
||||
custom_secret.as_bytes()
|
||||
};
|
||||
let mut mac = HmacSha256::new_from_slice(secret_bytes)
|
||||
.context("Failed to create hmac sha256 from secret")?;
|
||||
mac.update(body.as_bytes());
|
||||
let expected = mac.finalize().into_bytes().encode_hex::<String>();
|
||||
if signature == expected {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("Signature does not equal expected"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct GithubWebhookBody {
|
||||
#[serde(rename = "ref")]
|
||||
branch: String,
|
||||
}
|
||||
|
||||
impl VerifyBranch for Github {
|
||||
fn verify_branch(
|
||||
body: &str,
|
||||
expected_branch: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let branch = serde_json::from_str::<GithubWebhookBody>(body)
|
||||
.context("Failed to parse github request body")?
|
||||
.branch
|
||||
.replace("refs/heads/", "");
|
||||
if branch == expected_branch {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("request branch does not match expected"))
|
||||
}
|
||||
}
|
||||
}
|
||||
58
bin/core/src/listener/integrations/gitlab.rs
Normal file
58
bin/core/src/listener/integrations/gitlab.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
listener::{VerifyBranch, VerifySecret},
|
||||
};
|
||||
|
||||
/// Listener implementation for Gitlab type API
|
||||
pub struct Gitlab;
|
||||
|
||||
impl VerifySecret for Gitlab {
|
||||
#[instrument("VerifyGitlabSecret", skip_all)]
|
||||
fn verify_secret(
|
||||
headers: axum::http::HeaderMap,
|
||||
_body: &str,
|
||||
custom_secret: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let token = headers
|
||||
.get("x-gitlab-token")
|
||||
.context("No gitlab token in headers")?;
|
||||
let token =
|
||||
token.to_str().context("Failed to get token as string")?;
|
||||
let secret = if custom_secret.is_empty() {
|
||||
core_config().webhook_secret.as_str()
|
||||
} else {
|
||||
custom_secret
|
||||
};
|
||||
if token == secret {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("Webhook secret does not match expected."))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct GitlabWebhookBody {
|
||||
#[serde(rename = "ref")]
|
||||
branch: String,
|
||||
}
|
||||
|
||||
impl VerifyBranch for Gitlab {
|
||||
fn verify_branch(
|
||||
body: &str,
|
||||
expected_branch: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let branch = serde_json::from_str::<GitlabWebhookBody>(body)
|
||||
.context("Failed to parse gitlab request body")?
|
||||
.branch
|
||||
.replace("refs/heads/", "");
|
||||
if branch == expected_branch {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("request branch does not match expected"))
|
||||
}
|
||||
}
|
||||
}
|
||||
2
bin/core/src/listener/integrations/mod.rs
Normal file
2
bin/core/src/listener/integrations/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod github;
|
||||
pub mod gitlab;
|
||||
@@ -1,7 +1,52 @@
|
||||
use axum::Router;
|
||||
use std::sync::Arc;
|
||||
|
||||
mod github;
|
||||
use axum::{http::HeaderMap, Router};
|
||||
use komodo_client::entities::resource::Resource;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{helpers::cache::Cache, resource::KomodoResource};
|
||||
|
||||
mod integrations;
|
||||
mod resources;
|
||||
mod router;
|
||||
|
||||
use integrations::*;
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new().nest("/github", github::router())
|
||||
Router::new()
|
||||
.nest("/github", router::router::<github::Github>())
|
||||
.nest("/gitlab", router::router::<gitlab::Gitlab>())
|
||||
}
|
||||
|
||||
type ListenerLockCache = Cache<String, Arc<Mutex<()>>>;
|
||||
|
||||
/// Implemented for all resources which can recieve webhook.
|
||||
trait CustomSecret: KomodoResource {
|
||||
fn custom_secret(
|
||||
resource: &Resource<Self::Config, Self::Info>,
|
||||
) -> &str;
|
||||
}
|
||||
|
||||
/// Implemented on the integration struct, eg [integrations::github::Github]
|
||||
trait VerifySecret {
|
||||
fn verify_secret(
|
||||
headers: HeaderMap,
|
||||
body: &str,
|
||||
custom_secret: &str,
|
||||
) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
/// Implemented on the integration struct, eg [integrations::github::Github]
|
||||
trait VerifyBranch {
|
||||
/// Returns Err if the branch extracted from request
|
||||
/// body does not match the expected branch.
|
||||
fn verify_branch(
|
||||
body: &str,
|
||||
expected_branch: &str,
|
||||
) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
/// For Procedures and Actions, incoming webhook
|
||||
/// can be triggered by any branch by using `__ANY__`
|
||||
/// as the branch in the webhook URL.
|
||||
const ANY_BRANCH: &str = "__ANY__";
|
||||
|
||||
486
bin/core/src/listener/resources.rs
Normal file
486
bin/core/src/listener/resources.rs
Normal file
@@ -0,0 +1,486 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use komodo_client::{
|
||||
api::{
|
||||
execute::*,
|
||||
write::{RefreshResourceSyncPending, RefreshStackCache},
|
||||
},
|
||||
entities::{
|
||||
action::Action, build::Build, procedure::Procedure, repo::Repo,
|
||||
stack::Stack, sync::ResourceSync, user::git_webhook_user,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
api::execute::ExecuteRequest,
|
||||
helpers::update::init_execution_update, state::State,
|
||||
};
|
||||
|
||||
use super::{ListenerLockCache, ANY_BRANCH};
|
||||
|
||||
// =======
|
||||
// BUILD
|
||||
// =======
|
||||
|
||||
impl super::CustomSecret for Build {
|
||||
fn custom_secret(resource: &Self) -> &str {
|
||||
&resource.config.webhook_secret
|
||||
}
|
||||
}
|
||||
|
||||
fn build_locks() -> &'static ListenerLockCache {
|
||||
static BUILD_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();
|
||||
BUILD_LOCKS.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
pub async fn handle_build_webhook<B: super::VerifyBranch>(
|
||||
build: Build,
|
||||
body: String,
|
||||
) -> anyhow::Result<()> {
|
||||
// Acquire and hold lock to make a task queue for
|
||||
// subsequent listener calls on same resource.
|
||||
// It would fail if we let it go through from action state busy.
|
||||
let lock = build_locks().get_or_insert_default(&build.id).await;
|
||||
let _lock = lock.lock().await;
|
||||
|
||||
if !build.config.webhook_enabled {
|
||||
return Err(anyhow!("build does not have webhook enabled"));
|
||||
}
|
||||
|
||||
B::verify_branch(&body, &build.config.branch)?;
|
||||
|
||||
let user = git_webhook_user().to_owned();
|
||||
let req = ExecuteRequest::RunBuild(RunBuild { build: build.id });
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::RunBuild(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
State.resolve(req, (user, update)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ======
|
||||
// REPO
|
||||
// ======
|
||||
|
||||
impl super::CustomSecret for Repo {
|
||||
fn custom_secret(resource: &Self) -> &str {
|
||||
&resource.config.webhook_secret
|
||||
}
|
||||
}
|
||||
|
||||
fn repo_locks() -> &'static ListenerLockCache {
|
||||
static REPO_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();
|
||||
REPO_LOCKS.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
pub trait RepoExecution {
|
||||
async fn resolve(repo: Repo) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
impl RepoExecution for CloneRepo {
|
||||
async fn resolve(repo: Repo) -> anyhow::Result<()> {
|
||||
let user = git_webhook_user().to_owned();
|
||||
let req =
|
||||
crate::api::execute::ExecuteRequest::CloneRepo(CloneRepo {
|
||||
repo: repo.id,
|
||||
});
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let crate::api::execute::ExecuteRequest::CloneRepo(req) = req
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
State.resolve(req, (user, update)).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl RepoExecution for PullRepo {
|
||||
async fn resolve(repo: Repo) -> anyhow::Result<()> {
|
||||
let user = git_webhook_user().to_owned();
|
||||
let req =
|
||||
crate::api::execute::ExecuteRequest::PullRepo(PullRepo {
|
||||
repo: repo.id,
|
||||
});
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let crate::api::execute::ExecuteRequest::PullRepo(req) = req
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
State.resolve(req, (user, update)).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl RepoExecution for BuildRepo {
|
||||
async fn resolve(repo: Repo) -> anyhow::Result<()> {
|
||||
let user = git_webhook_user().to_owned();
|
||||
let req =
|
||||
crate::api::execute::ExecuteRequest::BuildRepo(BuildRepo {
|
||||
repo: repo.id,
|
||||
});
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let crate::api::execute::ExecuteRequest::BuildRepo(req) = req
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
State.resolve(req, (user, update)).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RepoWebhookPath {
|
||||
pub option: RepoWebhookOption,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum RepoWebhookOption {
|
||||
Clone,
|
||||
Pull,
|
||||
Build,
|
||||
}
|
||||
|
||||
pub async fn handle_repo_webhook<B: super::VerifyBranch>(
|
||||
option: RepoWebhookOption,
|
||||
repo: Repo,
|
||||
body: String,
|
||||
) -> anyhow::Result<()> {
|
||||
match option {
|
||||
RepoWebhookOption::Clone => {
|
||||
handle_repo_webhook_inner::<B, CloneRepo>(repo, body).await
|
||||
}
|
||||
RepoWebhookOption::Pull => {
|
||||
handle_repo_webhook_inner::<B, PullRepo>(repo, body).await
|
||||
}
|
||||
RepoWebhookOption::Build => {
|
||||
handle_repo_webhook_inner::<B, BuildRepo>(repo, body).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_repo_webhook_inner<
|
||||
B: super::VerifyBranch,
|
||||
E: RepoExecution,
|
||||
>(
|
||||
repo: Repo,
|
||||
body: String,
|
||||
) -> anyhow::Result<()> {
|
||||
// Acquire and hold lock to make a task queue for
|
||||
// subsequent listener calls on same resource.
|
||||
// It would fail if we let it go through from action state busy.
|
||||
let lock = repo_locks().get_or_insert_default(&repo.id).await;
|
||||
let _lock = lock.lock().await;
|
||||
|
||||
if !repo.config.webhook_enabled {
|
||||
return Err(anyhow!("repo does not have webhook enabled"));
|
||||
}
|
||||
|
||||
B::verify_branch(&body, &repo.config.branch)?;
|
||||
|
||||
E::resolve(repo).await
|
||||
}
|
||||
|
||||
// =======
|
||||
// STACK
|
||||
// =======
|
||||
|
||||
impl super::CustomSecret for Stack {
|
||||
fn custom_secret(resource: &Self) -> &str {
|
||||
&resource.config.webhook_secret
|
||||
}
|
||||
}
|
||||
|
||||
fn stack_locks() -> &'static ListenerLockCache {
|
||||
static STACK_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();
|
||||
STACK_LOCKS.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
pub trait StackExecution {
|
||||
async fn resolve(stack: Stack) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
impl StackExecution for RefreshStackCache {
|
||||
async fn resolve(stack: Stack) -> anyhow::Result<()> {
|
||||
let user = git_webhook_user().to_owned();
|
||||
State
|
||||
.resolve(RefreshStackCache { stack: stack.id }, user)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl StackExecution for DeployStack {
|
||||
async fn resolve(stack: Stack) -> anyhow::Result<()> {
|
||||
let user = git_webhook_user().to_owned();
|
||||
if stack.config.webhook_force_deploy {
|
||||
let req = ExecuteRequest::DeployStack(DeployStack {
|
||||
stack: stack.id,
|
||||
stop_time: None,
|
||||
});
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::DeployStack(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
State.resolve(req, (user, update)).await?;
|
||||
} else {
|
||||
let req =
|
||||
ExecuteRequest::DeployStackIfChanged(DeployStackIfChanged {
|
||||
stack: stack.id,
|
||||
stop_time: None,
|
||||
});
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::DeployStackIfChanged(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
State.resolve(req, (user, update)).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct StackWebhookPath {
|
||||
pub option: StackWebhookOption,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum StackWebhookOption {
|
||||
Refresh,
|
||||
Deploy,
|
||||
}
|
||||
|
||||
pub async fn handle_stack_webhook<B: super::VerifyBranch>(
|
||||
option: StackWebhookOption,
|
||||
stack: Stack,
|
||||
body: String,
|
||||
) -> anyhow::Result<()> {
|
||||
match option {
|
||||
StackWebhookOption::Refresh => {
|
||||
handle_stack_webhook_inner::<B, RefreshStackCache>(stack, body)
|
||||
.await
|
||||
}
|
||||
StackWebhookOption::Deploy => {
|
||||
handle_stack_webhook_inner::<B, DeployStack>(stack, body).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_stack_webhook_inner<
|
||||
B: super::VerifyBranch,
|
||||
E: StackExecution,
|
||||
>(
|
||||
stack: Stack,
|
||||
body: String,
|
||||
) -> anyhow::Result<()> {
|
||||
// Acquire and hold lock to make a task queue for
|
||||
// subsequent listener calls on same resource.
|
||||
// It would fail if we let it go through, from "action state busy".
|
||||
let lock = stack_locks().get_or_insert_default(&stack.id).await;
|
||||
let _lock = lock.lock().await;
|
||||
|
||||
if !stack.config.webhook_enabled {
|
||||
return Err(anyhow!("stack does not have webhook enabled"));
|
||||
}
|
||||
|
||||
B::verify_branch(&body, &stack.config.branch)?;
|
||||
|
||||
E::resolve(stack).await
|
||||
}
|
||||
|
||||
// ======
|
||||
// SYNC
|
||||
// ======
|
||||
|
||||
impl super::CustomSecret for ResourceSync {
|
||||
fn custom_secret(resource: &Self) -> &str {
|
||||
&resource.config.webhook_secret
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_locks() -> &'static ListenerLockCache {
|
||||
static SYNC_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();
|
||||
SYNC_LOCKS.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
pub trait SyncExecution {
|
||||
async fn resolve(sync: ResourceSync) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
impl SyncExecution for RefreshResourceSyncPending {
|
||||
async fn resolve(sync: ResourceSync) -> anyhow::Result<()> {
|
||||
let user = git_webhook_user().to_owned();
|
||||
State
|
||||
.resolve(RefreshResourceSyncPending { sync: sync.id }, user)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SyncExecution for RunSync {
|
||||
async fn resolve(sync: ResourceSync) -> anyhow::Result<()> {
|
||||
let user = git_webhook_user().to_owned();
|
||||
let req = ExecuteRequest::RunSync(RunSync {
|
||||
sync: sync.id,
|
||||
resource_type: None,
|
||||
resources: None,
|
||||
});
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::RunSync(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
State.resolve(req, (user, update)).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SyncWebhookPath {
|
||||
pub option: SyncWebhookOption,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum SyncWebhookOption {
|
||||
Refresh,
|
||||
Sync,
|
||||
}
|
||||
|
||||
pub async fn handle_sync_webhook<B: super::VerifyBranch>(
|
||||
option: SyncWebhookOption,
|
||||
sync: ResourceSync,
|
||||
body: String,
|
||||
) -> anyhow::Result<()> {
|
||||
match option {
|
||||
SyncWebhookOption::Refresh => {
|
||||
handle_sync_webhook_inner::<B, RefreshResourceSyncPending>(
|
||||
sync, body,
|
||||
)
|
||||
.await
|
||||
}
|
||||
SyncWebhookOption::Sync => {
|
||||
handle_sync_webhook_inner::<B, RunSync>(sync, body).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_sync_webhook_inner<
|
||||
B: super::VerifyBranch,
|
||||
E: SyncExecution,
|
||||
>(
|
||||
sync: ResourceSync,
|
||||
body: String,
|
||||
) -> anyhow::Result<()> {
|
||||
// Acquire and hold lock to make a task queue for
|
||||
// subsequent listener calls on same resource.
|
||||
// It would fail if we let it go through from action state busy.
|
||||
let lock = sync_locks().get_or_insert_default(&sync.id).await;
|
||||
let _lock = lock.lock().await;
|
||||
|
||||
if !sync.config.webhook_enabled {
|
||||
return Err(anyhow!("sync does not have webhook enabled"));
|
||||
}
|
||||
|
||||
B::verify_branch(&body, &sync.config.branch)?;
|
||||
|
||||
E::resolve(sync).await
|
||||
}
|
||||
|
||||
// ===========
|
||||
// PROCEDURE
|
||||
// ===========
|
||||
|
||||
impl super::CustomSecret for Procedure {
|
||||
fn custom_secret(resource: &Self) -> &str {
|
||||
&resource.config.webhook_secret
|
||||
}
|
||||
}
|
||||
|
||||
fn procedure_locks() -> &'static ListenerLockCache {
|
||||
static PROCEDURE_LOCKS: OnceLock<ListenerLockCache> =
|
||||
OnceLock::new();
|
||||
PROCEDURE_LOCKS.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
pub async fn handle_procedure_webhook<B: super::VerifyBranch>(
|
||||
procedure: Procedure,
|
||||
target_branch: String,
|
||||
body: String,
|
||||
) -> anyhow::Result<()> {
|
||||
// Acquire and hold lock to make a task queue for
|
||||
// subsequent listener calls on same resource.
|
||||
// It would fail if we let it go through from action state busy.
|
||||
let lock =
|
||||
procedure_locks().get_or_insert_default(&procedure.id).await;
|
||||
let _lock = lock.lock().await;
|
||||
|
||||
if !procedure.config.webhook_enabled {
|
||||
return Err(anyhow!("procedure does not have webhook enabled"));
|
||||
}
|
||||
|
||||
if target_branch != ANY_BRANCH {
|
||||
B::verify_branch(&body, &target_branch)?;
|
||||
}
|
||||
|
||||
let user = git_webhook_user().to_owned();
|
||||
let req = ExecuteRequest::RunProcedure(RunProcedure {
|
||||
procedure: procedure.id,
|
||||
});
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::RunProcedure(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
State.resolve(req, (user, update)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ========
|
||||
// ACTION
|
||||
// ========
|
||||
|
||||
impl super::CustomSecret for Action {
|
||||
fn custom_secret(resource: &Self) -> &str {
|
||||
&resource.config.webhook_secret
|
||||
}
|
||||
}
|
||||
|
||||
fn action_locks() -> &'static ListenerLockCache {
|
||||
static ACTION_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();
|
||||
ACTION_LOCKS.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
pub async fn handle_action_webhook<B: super::VerifyBranch>(
|
||||
action: Action,
|
||||
target_branch: String,
|
||||
body: String,
|
||||
) -> anyhow::Result<()> {
|
||||
// Acquire and hold lock to make a task queue for
|
||||
// subsequent listener calls on same resource.
|
||||
// It would fail if we let it go through from action state busy.
|
||||
let lock = action_locks().get_or_insert_default(&action.id).await;
|
||||
let _lock = lock.lock().await;
|
||||
|
||||
if !action.config.webhook_enabled {
|
||||
return Err(anyhow!("action does not have webhook enabled"));
|
||||
}
|
||||
|
||||
if target_branch != ANY_BRANCH {
|
||||
B::verify_branch(&body, &target_branch)?;
|
||||
}
|
||||
|
||||
let user = git_webhook_user().to_owned();
|
||||
let req =
|
||||
ExecuteRequest::RunAction(RunAction { action: action.id });
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::RunAction(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
State.resolve(req, (user, update)).await?;
|
||||
Ok(())
|
||||
}
|
||||
208
bin/core/src/listener/router.rs
Normal file
208
bin/core/src/listener/router.rs
Normal file
@@ -0,0 +1,208 @@
|
||||
use axum::{extract::Path, http::HeaderMap, routing::post, Router};
|
||||
use komodo_client::entities::{
|
||||
action::Action, build::Build, procedure::Procedure, repo::Repo,
|
||||
resource::Resource, stack::Stack, sync::ResourceSync,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use tracing::Instrument;
|
||||
|
||||
use crate::resource::KomodoResource;
|
||||
|
||||
use super::{
|
||||
resources::{
|
||||
handle_action_webhook, handle_build_webhook,
|
||||
handle_procedure_webhook, handle_repo_webhook,
|
||||
handle_stack_webhook, handle_sync_webhook, RepoWebhookPath,
|
||||
StackWebhookPath, SyncWebhookPath,
|
||||
},
|
||||
CustomSecret, VerifyBranch, VerifySecret,
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Id {
|
||||
id: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Branch {
|
||||
#[serde(default = "default_branch")]
|
||||
branch: String,
|
||||
}
|
||||
|
||||
fn default_branch() -> String {
|
||||
String::from("main")
|
||||
}
|
||||
|
||||
pub fn router<P: VerifySecret + VerifyBranch>() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/build/:id",
|
||||
post(
|
||||
|Path(Id { id }), headers: HeaderMap, body: String| async move {
|
||||
let build =
|
||||
auth_webhook::<P, Build>(&id, headers, &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("BuildWebhook", id);
|
||||
async {
|
||||
let res = handle_build_webhook::<P>(
|
||||
build, body,
|
||||
)
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
warn!(
|
||||
"Failed at running webhook for build {id} | {e:#}"
|
||||
);
|
||||
}
|
||||
}
|
||||
.instrument(span)
|
||||
.await
|
||||
});
|
||||
serror::Result::Ok(())
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/repo/:id/:option",
|
||||
post(
|
||||
|Path(Id { id }), Path(RepoWebhookPath { option }), headers: HeaderMap, body: String| async move {
|
||||
let repo =
|
||||
auth_webhook::<P, Repo>(&id, headers, &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("RepoWebhook", id);
|
||||
async {
|
||||
let res = handle_repo_webhook::<P>(
|
||||
option, repo, body,
|
||||
)
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
warn!(
|
||||
"Failed at running webhook for repo {id} | {e:#}"
|
||||
);
|
||||
}
|
||||
}
|
||||
.instrument(span)
|
||||
.await
|
||||
});
|
||||
serror::Result::Ok(())
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/stack/:id/:option",
|
||||
post(
|
||||
|Path(Id { id }), Path(StackWebhookPath { option }), headers: HeaderMap, body: String| async move {
|
||||
let stack =
|
||||
auth_webhook::<P, Stack>(&id, headers, &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("StackWebhook", id);
|
||||
async {
|
||||
let res = handle_stack_webhook::<P>(
|
||||
option, stack, body,
|
||||
)
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
warn!(
|
||||
"Failed at running webhook for stack {id} | {e:#}"
|
||||
);
|
||||
}
|
||||
}
|
||||
.instrument(span)
|
||||
.await
|
||||
});
|
||||
serror::Result::Ok(())
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/sync/:id/:option",
|
||||
post(
|
||||
|Path(Id { id }), Path(SyncWebhookPath { option }), headers: HeaderMap, body: String| async move {
|
||||
let sync =
|
||||
auth_webhook::<P, ResourceSync>(&id, headers, &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("ResourceSyncWebhook", id);
|
||||
async {
|
||||
let res = handle_sync_webhook::<P>(
|
||||
option, sync, body,
|
||||
)
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
warn!(
|
||||
"Failed at running webhook for resource sync {id} | {e:#}"
|
||||
);
|
||||
}
|
||||
}
|
||||
.instrument(span)
|
||||
.await
|
||||
});
|
||||
serror::Result::Ok(())
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/procedure/:id/:branch",
|
||||
post(
|
||||
|Path(Id { id }), Path(Branch { branch }), headers: HeaderMap, body: String| async move {
|
||||
let procedure =
|
||||
auth_webhook::<P, Procedure>(&id, headers, &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("ProcedureWebhook", id);
|
||||
async {
|
||||
let res = handle_procedure_webhook::<P>(
|
||||
procedure, branch, body,
|
||||
)
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
warn!(
|
||||
"Failed at running webhook for procedure {id} | {e:#}"
|
||||
);
|
||||
}
|
||||
}
|
||||
.instrument(span)
|
||||
.await
|
||||
});
|
||||
serror::Result::Ok(())
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/action/:id/:branch",
|
||||
post(
|
||||
|Path(Id { id }), Path(Branch { branch }), headers: HeaderMap, body: String| async move {
|
||||
let action =
|
||||
auth_webhook::<P, Action>(&id, headers, &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("ActionWebhook", id);
|
||||
async {
|
||||
let res = handle_action_webhook::<P>(
|
||||
action, branch, body,
|
||||
)
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
warn!(
|
||||
"Failed at running webhook for action {id} | {e:#}"
|
||||
);
|
||||
}
|
||||
}
|
||||
.instrument(span)
|
||||
.await
|
||||
});
|
||||
serror::Result::Ok(())
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
async fn auth_webhook<P, R>(
|
||||
id: &str,
|
||||
headers: HeaderMap,
|
||||
body: &str,
|
||||
) -> serror::Result<Resource<R::Config, R::Info>>
|
||||
where
|
||||
P: VerifySecret,
|
||||
R: KomodoResource + CustomSecret,
|
||||
{
|
||||
let resource = crate::resource::get::<R>(id).await?;
|
||||
P::verify_secret(headers, body, R::custom_secret(&resource))?;
|
||||
Ok(resource)
|
||||
}
|
||||
@@ -134,17 +134,22 @@ impl super::KomodoResource for Builder {
|
||||
resource: &Resource<Self::Config, Self::Info>,
|
||||
_update: &mut Update,
|
||||
) -> anyhow::Result<()> {
|
||||
// remove the builder from any attached builds
|
||||
db_client()
|
||||
.builds
|
||||
.update_many(
|
||||
doc! { "config.builder.params.builder_id": &resource.id },
|
||||
mungos::update::Update::Set(
|
||||
doc! { "config.builder.params.builder_id": "" },
|
||||
),
|
||||
doc! { "config.builder_id": &resource.id },
|
||||
mungos::update::Update::Set(doc! { "config.builder_id": "" }),
|
||||
)
|
||||
.await
|
||||
.context("failed to update_many builds on database")?;
|
||||
db_client()
|
||||
.repos
|
||||
.update_many(
|
||||
doc! { "config.builder_id": &resource.id },
|
||||
mungos::update::Update::Set(doc! { "config.builder_id": "" }),
|
||||
)
|
||||
.await
|
||||
.context("failed to update_many repos on database")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -182,11 +182,10 @@ impl AllResourcesById {
|
||||
id_to_tags, match_tags,
|
||||
)
|
||||
.await?,
|
||||
actions:
|
||||
crate::resource::get_id_to_resource_map::<Action>(
|
||||
id_to_tags, match_tags,
|
||||
)
|
||||
.await?,
|
||||
actions: crate::resource::get_id_to_resource_map::<Action>(
|
||||
id_to_tags, match_tags,
|
||||
)
|
||||
.await?,
|
||||
builders: crate::resource::get_id_to_resource_map::<Builder>(
|
||||
id_to_tags, match_tags,
|
||||
)
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use command::run_komodo_command;
|
||||
use formatting::format_serror;
|
||||
use komodo_client::entities::{
|
||||
build::{Build, BuildConfig},
|
||||
environment_vars_from_str, get_image_name, optional_string,
|
||||
to_komodo_name,
|
||||
update::Log,
|
||||
EnvironmentVar, Version,
|
||||
use komodo_client::{
|
||||
entities::{
|
||||
build::{Build, BuildConfig},
|
||||
environment_vars_from_str, get_image_name, optional_string,
|
||||
to_komodo_name,
|
||||
update::Log,
|
||||
EnvironmentVar, Version,
|
||||
},
|
||||
parsers::QUOTE_PATTERN,
|
||||
};
|
||||
use periphery_client::api::build::{
|
||||
self, PruneBuilders, PruneBuildx,
|
||||
@@ -101,8 +104,9 @@ impl Resolve<build::Build> for State {
|
||||
|
||||
let secret_args = environment_vars_from_str(secret_args)
|
||||
.context("Invalid secret_args")?;
|
||||
let _secret_args =
|
||||
let command_secret_args =
|
||||
parse_secret_args(&secret_args, *skip_secret_interp)?;
|
||||
|
||||
let labels = parse_labels(
|
||||
&environment_vars_from_str(labels).context("Invalid labels")?,
|
||||
);
|
||||
@@ -118,7 +122,7 @@ impl Resolve<build::Build> for State {
|
||||
|
||||
// Construct command
|
||||
let command = format!(
|
||||
"docker{buildx} build{build_args}{_secret_args}{extra_args}{labels}{image_tags} -f {dockerfile_path} .{push_command}",
|
||||
"docker{buildx} build{build_args}{command_secret_args}{extra_args}{labels}{image_tags} -f {dockerfile_path} .{push_command}",
|
||||
);
|
||||
|
||||
if *skip_secret_interp {
|
||||
@@ -190,7 +194,16 @@ fn image_tags(
|
||||
fn parse_build_args(build_args: &[EnvironmentVar]) -> String {
|
||||
build_args
|
||||
.iter()
|
||||
.map(|p| format!(" --build-arg {}=\"{}\"", p.variable, p.value))
|
||||
.map(|p| {
|
||||
if p.value.starts_with(QUOTE_PATTERN)
|
||||
&& p.value.ends_with(QUOTE_PATTERN)
|
||||
{
|
||||
// If the value already wrapped in quotes, don't wrap it again
|
||||
format!(" --build-arg {}={}", p.variable, p.value)
|
||||
} else {
|
||||
format!(" --build-arg {}=\"{}\"", p.variable, p.value)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("")
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
use anyhow::Context;
|
||||
use command::run_komodo_command;
|
||||
use formatting::format_serror;
|
||||
use komodo_client::entities::{
|
||||
deployment::{
|
||||
conversions_from_str, extract_registry_domain, Conversion,
|
||||
Deployment, DeploymentConfig, DeploymentImage, RestartMode,
|
||||
use komodo_client::{
|
||||
entities::{
|
||||
deployment::{
|
||||
conversions_from_str, extract_registry_domain, Conversion,
|
||||
Deployment, DeploymentConfig, DeploymentImage, RestartMode,
|
||||
},
|
||||
environment_vars_from_str, to_komodo_name,
|
||||
update::Log,
|
||||
EnvironmentVar,
|
||||
},
|
||||
environment_vars_from_str, to_komodo_name,
|
||||
update::Log,
|
||||
EnvironmentVar,
|
||||
parsers::QUOTE_PATTERN,
|
||||
};
|
||||
use periphery_client::api::container::{Deploy, RemoveContainer};
|
||||
use resolver_api::Resolve;
|
||||
@@ -175,7 +178,16 @@ fn parse_conversions(
|
||||
fn parse_environment(environment: &[EnvironmentVar]) -> String {
|
||||
environment
|
||||
.iter()
|
||||
.map(|p| format!(" --env {}=\"{}\"", p.variable, p.value))
|
||||
.map(|p| {
|
||||
if p.value.starts_with(QUOTE_PATTERN)
|
||||
&& p.value.ends_with(QUOTE_PATTERN)
|
||||
{
|
||||
// If the value already wrapped in quotes, don't wrap it again
|
||||
format!(" --env {}={}", p.variable, p.value)
|
||||
} else {
|
||||
format!(" --env {}=\"{}\"", p.variable, p.value)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("")
|
||||
}
|
||||
|
||||
@@ -222,7 +222,7 @@ impl Resolve<RenameRepo> for State {
|
||||
)
|
||||
.await;
|
||||
let msg = match renamed {
|
||||
Ok(_) => format!("Renamed Repo directory on Server"),
|
||||
Ok(_) => String::from("Renamed Repo directory on Server"),
|
||||
Err(_) => format!("No Repo cloned at {curr_name} to rename"),
|
||||
};
|
||||
Ok(Log::simple("Rename Repo on Server", msg))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::path::PathBuf;
|
||||
use std::{fmt::Write, path::PathBuf};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use command::run_komodo_command;
|
||||
@@ -145,8 +145,10 @@ pub async fn compose_up(
|
||||
.config
|
||||
.additional_env_files
|
||||
.iter()
|
||||
.map(|file| format!(" --env-file {file}"))
|
||||
.collect::<String>();
|
||||
.fold(String::new(), |mut output, file| {
|
||||
let _ = write!(output, " --env-file {file}");
|
||||
output
|
||||
});
|
||||
|
||||
// Build images before destroying to minimize downtime.
|
||||
// If this fails, do not continue.
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use anyhow::Context;
|
||||
use komodo_client::entities::{EnvironmentVar, SearchCombinator};
|
||||
use komodo_client::{
|
||||
entities::{EnvironmentVar, SearchCombinator},
|
||||
parsers::QUOTE_PATTERN,
|
||||
};
|
||||
|
||||
use crate::config::periphery_config;
|
||||
|
||||
@@ -43,7 +46,16 @@ pub fn parse_extra_args(extra_args: &[String]) -> String {
|
||||
pub fn parse_labels(labels: &[EnvironmentVar]) -> String {
|
||||
labels
|
||||
.iter()
|
||||
.map(|p| format!(" --label {}=\"{}\"", p.variable, p.value))
|
||||
.map(|p| {
|
||||
if p.value.starts_with(QUOTE_PATTERN)
|
||||
&& p.value.ends_with(QUOTE_PATTERN)
|
||||
{
|
||||
// If the value already wrapped in quotes, don't wrap it again
|
||||
format!(" --label {}={}", p.variable, p.value)
|
||||
} else {
|
||||
format!(" --label {}=\"{}\"", p.variable, p.value)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("")
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ async fn task(
|
||||
request: crate::api::PeripheryRequest,
|
||||
) -> anyhow::Result<String> {
|
||||
let variant = request.extract_variant();
|
||||
|
||||
|
||||
let res =
|
||||
State
|
||||
.resolve_request(request, ())
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
//! - X-Api-Secret: `your_api_secret`
|
||||
//! - Use either Authorization *or* X-Api-Key and X-Api-Secret to authenticate requests.
|
||||
//! - Body: JSON specifying the request type (`type`) and the parameters (`params`).
|
||||
//!
|
||||
//!
|
||||
//! You can create API keys for your user, or for a Service User with limited permissions,
|
||||
//! from the Komodo UI Settings page.
|
||||
//!
|
||||
@@ -31,17 +31,17 @@
|
||||
//!
|
||||
//! The request's parent module (eg. [read], [mod@write]) determines the http path which
|
||||
//! must be used for the requests. For example, requests under [read] are made using http path `/read`.
|
||||
//!
|
||||
//!
|
||||
//! ## Curl Example
|
||||
//!
|
||||
//!
|
||||
//! Putting it all together, here is an example `curl` for [write::UpdateBuild], to update the version:
|
||||
//!
|
||||
//!
|
||||
//! ```text
|
||||
//! curl --header "Content-Type: application/json" \
|
||||
//! --header "X-Api-Key: your_api_key" \
|
||||
//! --header "X-Api-Secret: your_api_secret" \
|
||||
//! --data '{ "type": "UpdateBuild", "params": { "id": "67076689ed600cfdd52ac637", "config": { "version": "1.15.9" } } }' \
|
||||
//! https://komodo.example.com/write
|
||||
//! --header "X-Api-Key: your_api_key" \
|
||||
//! --header "X-Api-Secret: your_api_secret" \
|
||||
//! --data '{ "type": "UpdateBuild", "params": { "id": "67076689ed600cfdd52ac637", "config": { "version": "1.15.9" } } }' \
|
||||
//! https://komodo.example.com/write
|
||||
//! ```
|
||||
//!
|
||||
//! ## Modules
|
||||
|
||||
@@ -3,7 +3,10 @@ use resolver_api::derive::Request;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typeshare::typeshare;
|
||||
|
||||
use crate::entities::{builder::{Builder, PartialBuilderConfig}, update::Update};
|
||||
use crate::entities::{
|
||||
builder::{Builder, PartialBuilderConfig},
|
||||
update::Update,
|
||||
};
|
||||
|
||||
use super::KomodoWriteRequest;
|
||||
|
||||
@@ -94,4 +97,4 @@ pub struct RenameBuilder {
|
||||
pub id: String,
|
||||
/// The new name.
|
||||
pub name: String,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@ use resolver_api::derive::Request;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typeshare::typeshare;
|
||||
|
||||
use crate::entities::{procedure::{
|
||||
Procedure, _PartialProcedureConfig,
|
||||
}, update::Update};
|
||||
use crate::entities::{
|
||||
procedure::{Procedure, _PartialProcedureConfig},
|
||||
update::Update,
|
||||
};
|
||||
|
||||
use super::KomodoWriteRequest;
|
||||
|
||||
@@ -108,4 +109,4 @@ pub struct RenameProcedure {
|
||||
pub id: String,
|
||||
/// The new name.
|
||||
pub name: String,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ use serde::{Deserialize, Serialize};
|
||||
use typeshare::typeshare;
|
||||
|
||||
use crate::entities::{
|
||||
repo::{Repo, _PartialRepoConfig}, update::Update, NoData
|
||||
repo::{Repo, _PartialRepoConfig},
|
||||
update::Update,
|
||||
NoData,
|
||||
};
|
||||
|
||||
use super::KomodoWriteRequest;
|
||||
|
||||
@@ -3,9 +3,10 @@ use resolver_api::derive::Request;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typeshare::typeshare;
|
||||
|
||||
use crate::entities::{server_template::{
|
||||
PartialServerTemplateConfig, ServerTemplate,
|
||||
}, update::Update};
|
||||
use crate::entities::{
|
||||
server_template::{PartialServerTemplateConfig, ServerTemplate},
|
||||
update::Update,
|
||||
};
|
||||
|
||||
use super::KomodoWriteRequest;
|
||||
|
||||
@@ -96,4 +97,4 @@ pub struct RenameServerTemplate {
|
||||
pub id: String,
|
||||
/// The new name.
|
||||
pub name: String,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ pub type UpdateUserPasswordResponse = NoData;
|
||||
/// **Admin only**. Delete a user.
|
||||
/// Admins can delete any non-admin user.
|
||||
/// Only Super Admin can delete an admin.
|
||||
/// No users can delete a Super Admin user.
|
||||
/// No users can delete a Super Admin user.
|
||||
/// User cannot delete themselves.
|
||||
/// Response: [NoData].
|
||||
#[typeshare]
|
||||
|
||||
@@ -70,6 +70,22 @@ pub struct ActionConfig {
|
||||
))]
|
||||
#[builder(default)]
|
||||
pub file_contents: String,
|
||||
|
||||
/// Whether incoming webhooks actually trigger action.
|
||||
#[serde(default = "default_webhook_enabled")]
|
||||
#[builder(default = "default_webhook_enabled()")]
|
||||
#[partial_default(default_webhook_enabled())]
|
||||
pub webhook_enabled: bool,
|
||||
|
||||
/// Optionally provide an alternate webhook secret for this procedure.
|
||||
/// If its an empty string, use the default secret from the config.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub webhook_secret: String,
|
||||
}
|
||||
|
||||
fn default_webhook_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl ActionConfig {
|
||||
@@ -82,6 +98,8 @@ impl Default for ActionConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
file_contents: Default::default(),
|
||||
webhook_enabled: default_webhook_enabled(),
|
||||
webhook_secret: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,10 +34,10 @@ use serde::Deserialize;
|
||||
|
||||
pub mod api;
|
||||
pub mod busy;
|
||||
pub mod deserializers;
|
||||
pub mod entities;
|
||||
pub mod parsers;
|
||||
pub mod ws;
|
||||
pub mod deserializers;
|
||||
|
||||
mod request;
|
||||
|
||||
|
||||
@@ -1,24 +1,40 @@
|
||||
use anyhow::Context;
|
||||
|
||||
pub const QUOTE_PATTERN: &[char] = &['"', '\''];
|
||||
|
||||
/// Parses a list of key value pairs from a multiline string
|
||||
///
|
||||
/// Example source:
|
||||
/// ```text
|
||||
/// # Supports comments
|
||||
/// KEY_1 = value_1 # end of line comments
|
||||
///
|
||||
///
|
||||
/// # Supports string wrapped values
|
||||
/// KEY_2="value_2"
|
||||
/// 'KEY_3 = value_3'
|
||||
///
|
||||
///
|
||||
/// # Also supports yaml list formats
|
||||
/// - KEY_4: 'value_4'
|
||||
/// - "KEY_5=value_5"
|
||||
///
|
||||
/// # Wrapping outer quotes are removed while inner quotes are preserved
|
||||
/// "KEY_6 = 'value_6'"
|
||||
/// ```
|
||||
///
|
||||
/// Note this preserves the wrapping string around value.
|
||||
/// Writing environment file should format the value exactly as it comes in,
|
||||
/// including the given wrapping quotes.
|
||||
///
|
||||
/// Returns:
|
||||
/// ```text
|
||||
/// [("KEY_1", "value_1"), ("KEY_2", "value_2"), ("KEY_3", "value_3"), ("KEY_4", "value_4"), ("KEY_5", "value_5")]
|
||||
/// [
|
||||
/// ("KEY_1", "value_1"),
|
||||
/// ("KEY_2", "\"value_2\""),
|
||||
/// ("KEY_3", "value_3"),
|
||||
/// ("KEY_4", "'value_4'"),
|
||||
/// ("KEY_5", "value_5"),
|
||||
/// ("KEY_6", "'value_6'"),
|
||||
/// ]
|
||||
/// ```
|
||||
pub fn parse_key_value_list(
|
||||
input: &str,
|
||||
@@ -46,13 +62,6 @@ pub fn parse_key_value_list(
|
||||
// Remove preceding '-' (yaml list)
|
||||
.trim_start_matches('-')
|
||||
.trim();
|
||||
// Remove wrapping quotes (from yaml list)
|
||||
let line = if let Some(line) = line.strip_prefix(['"', '\'']) {
|
||||
line.strip_suffix(['"', '\'']).unwrap_or(line)
|
||||
} else {
|
||||
line
|
||||
};
|
||||
// Remove any preceding '"' (from yaml list) (wrapping quotes open)
|
||||
let (key, value) = line
|
||||
.split_once(['=', ':'])
|
||||
.with_context(|| {
|
||||
@@ -61,15 +70,23 @@ pub fn parse_key_value_list(
|
||||
)
|
||||
})
|
||||
.map(|(key, value)| {
|
||||
let key = key.trim();
|
||||
let value = value.trim();
|
||||
// Remove wrapping quotes around value
|
||||
let value =
|
||||
if let Some(value) = value.strip_prefix(['"', '\'']) {
|
||||
value.strip_suffix(['"', '\'']).unwrap_or(value)
|
||||
} else {
|
||||
value
|
||||
};
|
||||
(key.trim().to_string(), value.trim().to_string())
|
||||
|
||||
// Remove wrapping quotes when around key AND value
|
||||
let (key, value) = if key.starts_with(QUOTE_PATTERN)
|
||||
&& !key.ends_with(QUOTE_PATTERN)
|
||||
&& value.ends_with(QUOTE_PATTERN)
|
||||
{
|
||||
(
|
||||
key.strip_prefix(QUOTE_PATTERN).unwrap().trim(),
|
||||
value.strip_suffix(QUOTE_PATTERN).unwrap().trim(),
|
||||
)
|
||||
} else {
|
||||
(key, value)
|
||||
};
|
||||
|
||||
(key.to_string(), value.to_string())
|
||||
})?;
|
||||
anyhow::Ok((key, value))
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "komodo_client",
|
||||
"version": "1.16.0",
|
||||
"version": "1.16.3",
|
||||
"description": "Komodo client package",
|
||||
"homepage": "https://komo.do",
|
||||
"main": "dist/lib.js",
|
||||
|
||||
@@ -54,6 +54,13 @@ export interface Resource<Config, Info> {
|
||||
export interface ActionConfig {
|
||||
/** Typescript file contents using pre-initialized `komodo` client. */
|
||||
file_contents?: string;
|
||||
/** Whether incoming webhooks actually trigger action. */
|
||||
webhook_enabled: boolean;
|
||||
/**
|
||||
* Optionally provide an alternate webhook secret for this procedure.
|
||||
* If its an empty string, use the default secret from the config.
|
||||
*/
|
||||
webhook_secret?: string;
|
||||
}
|
||||
|
||||
export interface ActionInfo {
|
||||
|
||||
54
docsite/docs/development.md
Normal file
54
docsite/docs/development.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Development
|
||||
|
||||
If you are looking to contribute to Komodo, this page is a launching point for setting up your Komodo development environment.
|
||||
|
||||
## Dependencies
|
||||
|
||||
Running Komodo from [source](https://github.com/mbecker20/komodo) requires either [Docker](https://www.docker.com/) (and can use the included [devcontainer](https://code.visualstudio.com/docs/devcontainers/containers)), or can have the development dependencies installed locally:
|
||||
|
||||
* Backend (Core / Periphery APIs)
|
||||
* [Rust](https://www.rust-lang.org/) stable via [rustup installer](https://rustup.rs/)
|
||||
* [MongoDB](https://www.mongodb.com/) or [FerretDB](https://www.ferretdb.com/) available locally.
|
||||
* On Debian/Ubuntu: `apt install build-essential pkg-config libssl-dev` required to build the rust source.
|
||||
* Frontend (Web UI)
|
||||
* [Node](https://nodejs.org/en) >= 18.18 + NPM
|
||||
* [Yarn](https://yarnpkg.com/) - (Tip: use `corepack enable` after installing `node` to use `yarn`)
|
||||
* [typeshare](https://github.com/1password/typeshare)
|
||||
* [Deno](https://deno.com/) >= 2.0.2
|
||||
|
||||
### runnables-cli
|
||||
|
||||
[mbecker20/runnables-cli](https://github.com/mbecker20/runnables-cli) can be used as a convience CLI for running common project tasks found in `runfile.toml`. Otherwise, you can create your own project tasks by references the `cmd`s found in `runfile.toml`. All instructions below will use runnables-cli v1.3.7+.
|
||||
|
||||
## Docker
|
||||
|
||||
After making changes to the project, run `run -r test-compose-build` to rebuild Komodo and then `run -r test-compose-exposed` to start a Komodo container with the UI accessible at `localhost:9120`. Any changes made to source files will require re-running the `test-compose-build` and `test-compose-exposed` commands.
|
||||
|
||||
## Devcontainer
|
||||
|
||||
Use the included `.devcontainer.json` with VSCode or other compatible IDE to stand-up a full environment, including database, with one click.
|
||||
|
||||
[VSCode Tasks](https://code.visualstudio.com/Docs/editor/tasks) are provided for building and running Komodo.
|
||||
|
||||
After opening the repository with the devcontainer run the task `Init` to build the frontend/backend. Then, the task `Run Komodo` can be used to run frontend/backend. Other tasks for rebuilding/running just one component of the stack (Core API, Periphery API, Frontend) are also provided.
|
||||
|
||||
## Local
|
||||
|
||||
To run a full Komodo instance from a non-container environment run commands in this order:
|
||||
|
||||
* Ensure dependencies are up to date
|
||||
* `rustup update` -- ensure rust toolchain is up to date
|
||||
* Build and Run backend
|
||||
* `run -r test-core` -- Build and run Core API
|
||||
* `run -r test-periphery` -- Build and run Periphery API
|
||||
* Build Frontend
|
||||
* **Run this once** -- `run -r link-client` -- generates TS client and links to the frontend
|
||||
* After running the above once:
|
||||
* `run -r gen-client` -- Rebuild client
|
||||
* `run -r start-frontend` -- Start in dev (watch) mode
|
||||
* `run -r build-frontend` -- Typecheck and build
|
||||
|
||||
|
||||
## Docsite Development
|
||||
|
||||
Use `run -r docsite-start` to start the [Docusaurus](https://docusaurus.io/) Komodo docs site in development mode. Changes made to files in `./docsite` will be automatically reloaded by the server.
|
||||
@@ -63,6 +63,7 @@ const sidebars: SidebarsConfig = {
|
||||
"permissioning",
|
||||
"version-upgrades",
|
||||
"api",
|
||||
"development"
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
6
expose.compose.yaml
Normal file
6
expose.compose.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
services:
|
||||
core:
|
||||
ports:
|
||||
- 9120:9120
|
||||
environment:
|
||||
KOMODO_FIRST_SERVER: http://periphery:8120
|
||||
7
frontend/public/client/types.d.ts
vendored
7
frontend/public/client/types.d.ts
vendored
@@ -45,6 +45,13 @@ export interface Resource<Config, Info> {
|
||||
export interface ActionConfig {
|
||||
/** Typescript file contents using pre-initialized `komodo` client. */
|
||||
file_contents?: string;
|
||||
/** Whether incoming webhooks actually trigger action. */
|
||||
webhook_enabled: boolean;
|
||||
/**
|
||||
* Optionally provide an alternate webhook secret for this procedure.
|
||||
* If its an empty string, use the default secret from the config.
|
||||
*/
|
||||
webhook_secret?: string;
|
||||
}
|
||||
export interface ActionInfo {
|
||||
/** When action was last run */
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
SelectValue,
|
||||
} from "@ui/select";
|
||||
import { AlertTriangle, History, Settings } from "lucide-react";
|
||||
import { Fragment, ReactNode, SetStateAction, useMemo } from "react";
|
||||
import { Fragment, ReactNode, SetStateAction } from "react";
|
||||
|
||||
const keys = <T extends Record<string, unknown>>(obj: T) =>
|
||||
Object.keys(obj) as Array<keyof T>;
|
||||
@@ -114,11 +114,10 @@ export type ConfigComponent<T> = {
|
||||
};
|
||||
|
||||
export const Config = <T,>({
|
||||
// resource_id,
|
||||
// resource_type,
|
||||
config,
|
||||
update,
|
||||
disabled,
|
||||
disableSidebar,
|
||||
set,
|
||||
onSave,
|
||||
components,
|
||||
@@ -126,11 +125,10 @@ export const Config = <T,>({
|
||||
titleOther,
|
||||
file_contents_language,
|
||||
}: {
|
||||
resource_id: string;
|
||||
resource_type: Types.ResourceTarget["type"];
|
||||
config: T;
|
||||
update: Partial<T>;
|
||||
disabled: boolean;
|
||||
disableSidebar?: boolean;
|
||||
set: React.Dispatch<SetStateAction<Partial<T>>>;
|
||||
onSave: () => Promise<void>;
|
||||
selector?: ReactNode;
|
||||
@@ -141,27 +139,6 @@ export const Config = <T,>({
|
||||
>;
|
||||
file_contents_language?: MonacoLanguage;
|
||||
}) => {
|
||||
// let component_keys = keys(components);
|
||||
// const [_show, setShow] = useLocalStorage(
|
||||
// `config-${resource_type}-${resource_id}`,
|
||||
// component_keys[0]
|
||||
// );
|
||||
// const show = (components[_show] && _show) || component_keys[0];
|
||||
|
||||
const showSidebar = useMemo(() => {
|
||||
let activeCount = 0;
|
||||
for (const key in components) {
|
||||
for (const component of components[key] || []) {
|
||||
for (const key in component.components || {}) {
|
||||
if (component.components[key]) {
|
||||
activeCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return activeCount > 1;
|
||||
}, [components]);
|
||||
|
||||
const sections = keys(components).filter((section) => !!components[section]);
|
||||
|
||||
return (
|
||||
@@ -179,7 +156,7 @@ export const Config = <T,>({
|
||||
file_contents_language={file_contents_language}
|
||||
>
|
||||
<div className="flex gap-6">
|
||||
{showSidebar && (
|
||||
{!disableSidebar && (
|
||||
<div className="hidden xl:block relative pr-6 border-r">
|
||||
<div className="sticky top-24 hidden xl:flex flex-col gap-8 w-[140px] h-fit pb-24">
|
||||
{sections.map((section) => (
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import {
|
||||
WebhookIdOrName,
|
||||
useCtrlKeyListener,
|
||||
useInvalidate,
|
||||
useRead,
|
||||
useWebhookIdOrName,
|
||||
useWrite,
|
||||
WebhookIntegration,
|
||||
useWebhookIntegrations,
|
||||
} from "@lib/hooks";
|
||||
import { Types } from "komodo_client";
|
||||
import {
|
||||
@@ -1183,3 +1187,82 @@ export const RenameResource = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const WebhookBuilder = ({
|
||||
git_provider,
|
||||
children,
|
||||
}: {
|
||||
git_provider: string;
|
||||
children?: ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<ConfigItem>
|
||||
<div className="grid items-center grid-cols-[auto_1fr] gap-x-6 gap-y-2 w-fit">
|
||||
<div className="text-muted-foreground text-sm">Auth style?</div>
|
||||
<WebhookIntegrationSelector git_provider={git_provider} />
|
||||
|
||||
<div className="text-muted-foreground text-sm">
|
||||
Resource Id or Name?
|
||||
</div>
|
||||
<WebhookIdOrNameSelector />
|
||||
|
||||
{children}
|
||||
</div>
|
||||
</ConfigItem>
|
||||
);
|
||||
};
|
||||
|
||||
/** Should call `useWebhookIntegrations` in util/hooks to get the current value */
|
||||
export const WebhookIntegrationSelector = ({
|
||||
git_provider,
|
||||
}: {
|
||||
git_provider: string;
|
||||
}) => {
|
||||
const { integrations, setIntegration } = useWebhookIntegrations();
|
||||
const integration = integrations[git_provider]
|
||||
? integrations[git_provider]
|
||||
: git_provider === "gitlab.com"
|
||||
? "Gitlab"
|
||||
: "Github";
|
||||
return (
|
||||
<Select
|
||||
value={integration}
|
||||
onValueChange={(integration) =>
|
||||
setIntegration(git_provider, integration as WebhookIntegration)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{["Github", "Gitlab"].map((integration) => (
|
||||
<SelectItem key={integration} value={integration}>
|
||||
{integration}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
/** Should call `useWebhookIdOrName` in util/hooks to get the current value */
|
||||
export const WebhookIdOrNameSelector = () => {
|
||||
const [idOrName, setIdOrName] = useWebhookIdOrName();
|
||||
return (
|
||||
<Select
|
||||
value={idOrName}
|
||||
onValueChange={(idOrName) => setIdOrName(idOrName as WebhookIdOrName)}
|
||||
>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{["Id", "Name"].map((idOrName) => (
|
||||
<SelectItem key={idOrName} value={idOrName}>
|
||||
{idOrName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -181,23 +181,25 @@ export const Section = ({
|
||||
itemsCenterTitleRow,
|
||||
}: SectionProps) => (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-wrap gap-2 justify-between py-1",
|
||||
itemsCenterTitleRow ? "items-center" : "items-start"
|
||||
)}
|
||||
>
|
||||
{title || icon ? (
|
||||
<div className="px-2 flex items-center gap-2 text-muted-foreground">
|
||||
{icon}
|
||||
{title && <h2 className="text-xl">{title}</h2>}
|
||||
{titleRight}
|
||||
</div>
|
||||
) : (
|
||||
titleOther
|
||||
)}
|
||||
{actions}
|
||||
</div>
|
||||
{(title || icon || titleRight || titleOther || actions) && (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-wrap gap-2 justify-between py-1",
|
||||
itemsCenterTitleRow ? "items-center" : "items-start"
|
||||
)}
|
||||
>
|
||||
{title || icon ? (
|
||||
<div className="px-2 flex items-center gap-2 text-muted-foreground">
|
||||
{icon}
|
||||
{title && <h2 className="text-xl">{title}</h2>}
|
||||
{titleRight}
|
||||
</div>
|
||||
) : (
|
||||
titleOther
|
||||
)}
|
||||
{actions}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,15 +1,32 @@
|
||||
import { useLocalStorage, useRead, useWrite } from "@lib/hooks";
|
||||
import {
|
||||
useLocalStorage,
|
||||
useRead,
|
||||
useWebhookIdOrName,
|
||||
useWebhookIntegrations,
|
||||
useWrite,
|
||||
} from "@lib/hooks";
|
||||
import { Types } from "komodo_client";
|
||||
import { Config } from "@components/config";
|
||||
import { MonacoEditor } from "@components/monaco";
|
||||
import { SecretsSearch } from "@components/config/env_vars";
|
||||
import { Button } from "@ui/button";
|
||||
import { ConfigItem, WebhookBuilder } from "@components/config/util";
|
||||
import { Input } from "@ui/input";
|
||||
import { useState } from "react";
|
||||
import { CopyWebhook } from "../common";
|
||||
import { ActionInfo } from "./info";
|
||||
import { Switch } from "@ui/switch";
|
||||
|
||||
const ACTION_GIT_PROVIDER = "Action";
|
||||
|
||||
export const ActionConfig = ({ id }: { id: string }) => {
|
||||
const [branch, setBranch] = useState("main");
|
||||
const perms = useRead("GetPermissionLevel", {
|
||||
target: { type: "Action", id },
|
||||
}).data;
|
||||
const config = useRead("GetAction", { action: id }).data?.config;
|
||||
const action = useRead("GetAction", { action: id }).data;
|
||||
const config = action?.config;
|
||||
const name = action?.name;
|
||||
const global_disabled =
|
||||
useRead("GetCoreInfo", {}).data?.ui_write_disabled ?? false;
|
||||
const [update, set] = useLocalStorage<Partial<Types.ActionConfig>>(
|
||||
@@ -17,16 +34,18 @@ export const ActionConfig = ({ id }: { id: string }) => {
|
||||
{}
|
||||
);
|
||||
const { mutateAsync } = useWrite("UpdateAction");
|
||||
const { integrations } = useWebhookIntegrations();
|
||||
const [id_or_name] = useWebhookIdOrName();
|
||||
|
||||
if (!config) return null;
|
||||
|
||||
const disabled = global_disabled || perms !== Types.PermissionLevel.Write;
|
||||
const webhook_integration = integrations[ACTION_GIT_PROVIDER] ?? "Github";
|
||||
|
||||
return (
|
||||
<Config
|
||||
resource_id={id}
|
||||
resource_type="Action"
|
||||
disabled={disabled}
|
||||
disableSidebar
|
||||
config={config}
|
||||
update={update}
|
||||
set={set}
|
||||
@@ -78,11 +97,63 @@ export const ActionConfig = ({ id }: { id: string }) => {
|
||||
language="typescript"
|
||||
readOnly={disabled}
|
||||
/>
|
||||
<ActionInfo id={id} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Webhook",
|
||||
description: `Configure your ${webhook_integration}-style repo provider to send webhooks to Komodo`,
|
||||
components: {
|
||||
["Builder" as any]: () => (
|
||||
<WebhookBuilder git_provider={ACTION_GIT_PROVIDER}>
|
||||
<div className="text-nowrap text-muted-foreground text-sm">
|
||||
Listen on branch:
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Input
|
||||
placeholder="Branch"
|
||||
value={branch}
|
||||
onChange={(e) => setBranch(e.target.value)}
|
||||
className="w-[200px]"
|
||||
disabled={branch === "__ALL__"}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-muted-foreground text-sm">
|
||||
All branches:
|
||||
</div>
|
||||
<Switch
|
||||
checked={branch === "__ALL__"}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
setBranch("__ALL__");
|
||||
} else {
|
||||
setBranch("main");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</WebhookBuilder>
|
||||
),
|
||||
["run" as any]: () => (
|
||||
<ConfigItem label="Webhook Url - Run">
|
||||
<CopyWebhook
|
||||
integration={webhook_integration}
|
||||
path={`/action/${id_or_name === "Id" ? id : name}/${branch}`}
|
||||
/>
|
||||
</ConfigItem>
|
||||
),
|
||||
webhook_enabled: true,
|
||||
webhook_secret: {
|
||||
description:
|
||||
"Provide a custom webhook secret for this resource, or use the global default.",
|
||||
placeholder: "Input custom secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
import { cn } from "@lib/utils";
|
||||
import { Types } from "komodo_client";
|
||||
import { DashboardPieChart } from "@pages/home/dashboard";
|
||||
import { ActionInfo } from "./info";
|
||||
import { RenameResource } from "@components/config/util";
|
||||
|
||||
const useAction = (id?: string) =>
|
||||
@@ -28,15 +27,6 @@ const ActionIcon = ({ id, size }: { id?: string; size: number }) => {
|
||||
return <Clapperboard className={cn(`w-${size} h-${size}`, state && color)} />;
|
||||
};
|
||||
|
||||
const ConfigInfo = ({ id }: { id: string }) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<ActionConfig id={id} />
|
||||
<ActionInfo id={id} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ActionComponents: RequiredResourceComponents = {
|
||||
list_item: (id) => useAction(id),
|
||||
resource_links: () => undefined,
|
||||
@@ -112,7 +102,7 @@ export const ActionComponents: RequiredResourceComponents = {
|
||||
|
||||
Page: {},
|
||||
|
||||
Config: ConfigInfo,
|
||||
Config: ActionConfig,
|
||||
|
||||
DangerZone: ({ id }) => (
|
||||
<>
|
||||
|
||||
@@ -20,9 +20,9 @@ export const ActionInfo = ({ id }: { id: string }) => {
|
||||
|
||||
const log = full_update?.logs.find((log) => log.stage === "Execute Action");
|
||||
|
||||
return (
|
||||
<Section>
|
||||
{!log?.stdout && !log?.stderr && (
|
||||
if (!log?.stdout && !log?.stderr) {
|
||||
return (
|
||||
<Section>
|
||||
<Card className="flex flex-col gap-4">
|
||||
<CardHeader
|
||||
className={cn(
|
||||
@@ -33,17 +33,18 @@ export const ActionInfo = ({ id }: { id: string }) => {
|
||||
Never run
|
||||
</CardHeader>
|
||||
</Card>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Section>
|
||||
{/* Last run */}
|
||||
{log?.stdout && (
|
||||
<Card className="flex flex-col gap-4">
|
||||
<CardHeader
|
||||
className={cn(
|
||||
"flex flex-row justify-between items-center pb-0",
|
||||
text_color_class_by_intention("Good")
|
||||
)}
|
||||
>
|
||||
Stdout
|
||||
<CardHeader className="flex flex-row items-center gap-1 pb-0">
|
||||
Last run -
|
||||
<div className={text_color_class_by_intention("Good")}>Stdout</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pr-8">
|
||||
<pre
|
||||
@@ -57,13 +58,11 @@ export const ActionInfo = ({ id }: { id: string }) => {
|
||||
)}
|
||||
{log?.stderr && (
|
||||
<Card className="flex flex-col gap-4">
|
||||
<CardHeader
|
||||
className={cn(
|
||||
"flex flex-row justify-between items-center pb-0",
|
||||
text_color_class_by_intention("Critical")
|
||||
)}
|
||||
>
|
||||
Stderr
|
||||
<CardHeader className="flex flex-row items-center gap-1 pb-0">
|
||||
Last run -
|
||||
<div className={text_color_class_by_intention("Critical")}>
|
||||
Stderr
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pr-8">
|
||||
<pre
|
||||
|
||||
@@ -23,8 +23,6 @@ export const AlerterConfig = ({ id }: { id: string }) => {
|
||||
|
||||
return (
|
||||
<Config
|
||||
resource_id={id}
|
||||
resource_type="Alerter"
|
||||
disabled={disabled}
|
||||
config={config}
|
||||
update={update}
|
||||
|
||||
@@ -8,13 +8,22 @@ import {
|
||||
InputList,
|
||||
ProviderSelectorConfig,
|
||||
SystemCommand,
|
||||
WebhookBuilder,
|
||||
} from "@components/config/util";
|
||||
import { useInvalidate, useLocalStorage, useRead, useWrite } from "@lib/hooks";
|
||||
import {
|
||||
getWebhookIntegration,
|
||||
useInvalidate,
|
||||
useLocalStorage,
|
||||
useRead,
|
||||
useWebhookIdOrName,
|
||||
useWebhookIntegrations,
|
||||
useWrite,
|
||||
} from "@lib/hooks";
|
||||
import { Types } from "komodo_client";
|
||||
import { Button } from "@ui/button";
|
||||
import { Ban, CirclePlus, PlusCircle } from "lucide-react";
|
||||
import { ReactNode } from "react";
|
||||
import { CopyGithubWebhook, ResourceLink, ResourceSelector } from "../common";
|
||||
import { CopyWebhook, ResourceLink, ResourceSelector } from "../common";
|
||||
import { useToast } from "@ui/use-toast";
|
||||
import { text_color_class_by_intention } from "@lib/color";
|
||||
import { ConfirmButton } from "@components/util";
|
||||
@@ -32,7 +41,9 @@ export const BuildConfig = ({
|
||||
const perms = useRead("GetPermissionLevel", {
|
||||
target: { type: "Build", id },
|
||||
}).data;
|
||||
const config = useRead("GetBuild", { build: id }).data?.config;
|
||||
const build = useRead("GetBuild", { build: id }).data;
|
||||
const config = build?.config;
|
||||
const name = build?.name;
|
||||
const webhook = useRead("GetBuildWebhookEnabled", { build: id }).data;
|
||||
const global_disabled =
|
||||
useRead("GetCoreInfo", {}).data?.ui_write_disabled ?? false;
|
||||
@@ -41,15 +52,18 @@ export const BuildConfig = ({
|
||||
{}
|
||||
);
|
||||
const { mutateAsync } = useWrite("UpdateBuild");
|
||||
const { integrations } = useWebhookIntegrations();
|
||||
const [id_or_name] = useWebhookIdOrName();
|
||||
|
||||
if (!config) return null;
|
||||
|
||||
const disabled = global_disabled || perms !== Types.PermissionLevel.Write;
|
||||
|
||||
const git_provider = update.git_provider ?? config.git_provider;
|
||||
const webhook_integration = getWebhookIntegration(integrations, git_provider);
|
||||
|
||||
return (
|
||||
<Config
|
||||
resource_id={id}
|
||||
resource_type="Build"
|
||||
titleOther={titleOther}
|
||||
disabled={disabled}
|
||||
config={config}
|
||||
@@ -360,8 +374,7 @@ export const BuildConfig = ({
|
||||
},
|
||||
{
|
||||
label: "Webhook",
|
||||
description:
|
||||
"Configure your repo provider to send webhooks to Komodo",
|
||||
description: `Configure your ${webhook_integration}-style repo provider to send webhooks to Komodo`,
|
||||
components: {
|
||||
["Guard" as any]: () => {
|
||||
if (update.branch ?? config.branch) {
|
||||
@@ -373,9 +386,15 @@ export const BuildConfig = ({
|
||||
</ConfigItem>
|
||||
);
|
||||
},
|
||||
["Builder" as any]: () => (
|
||||
<WebhookBuilder git_provider={git_provider} />
|
||||
),
|
||||
["build" as any]: () => (
|
||||
<ConfigItem label="Webhook Url">
|
||||
<CopyGithubWebhook path={`/build/${id}`} />
|
||||
<ConfigItem label="Webhook Url - Build">
|
||||
<CopyWebhook
|
||||
integration={webhook_integration}
|
||||
path={`/build/${id_or_name === "Id" ? id : name}`}
|
||||
/>
|
||||
</ConfigItem>
|
||||
),
|
||||
webhook_enabled: webhook !== undefined && !webhook.managed,
|
||||
|
||||
@@ -43,8 +43,6 @@ const AwsBuilderConfig = ({ id }: { id: string }) => {
|
||||
|
||||
return (
|
||||
<Config
|
||||
resource_id={id}
|
||||
resource_type="Builder"
|
||||
disabled={disabled}
|
||||
config={config}
|
||||
update={update}
|
||||
@@ -257,8 +255,6 @@ const ServerBuilderConfig = ({ id }: { id: string }) => {
|
||||
|
||||
return (
|
||||
<Config
|
||||
resource_id={id}
|
||||
resource_type="Builder"
|
||||
disabled={disabled}
|
||||
config={config.params as Types.ServerBuilderConfig}
|
||||
update={update}
|
||||
|
||||
@@ -4,7 +4,12 @@ import {
|
||||
CopyButton,
|
||||
TextUpdateMenu2,
|
||||
} from "@components/util";
|
||||
import { useInvalidate, useRead, useWrite } from "@lib/hooks";
|
||||
import {
|
||||
useInvalidate,
|
||||
useRead,
|
||||
useWrite,
|
||||
WebhookIntegration,
|
||||
} from "@lib/hooks";
|
||||
import { UsableResource } from "@types";
|
||||
import { Button } from "@ui/button";
|
||||
import {
|
||||
@@ -50,8 +55,8 @@ export const ResourceDescription = ({
|
||||
type === "ServerTemplate"
|
||||
? "server_template"
|
||||
: type === "ResourceSync"
|
||||
? "sync"
|
||||
: type.toLowerCase();
|
||||
? "sync"
|
||||
: type.toLowerCase();
|
||||
|
||||
const resource = useRead(`Get${type}`, {
|
||||
[key]: id,
|
||||
@@ -281,8 +286,8 @@ export const NewResource = ({
|
||||
type === "ServerTemplate"
|
||||
? "server-template"
|
||||
: type === "ResourceSync"
|
||||
? "resource-sync"
|
||||
: type.toLowerCase();
|
||||
? "resource-sync"
|
||||
: type.toLowerCase();
|
||||
const config: Types._PartialDeploymentConfig | Types._PartialRepoConfig =
|
||||
type === "Deployment"
|
||||
? {
|
||||
@@ -292,12 +297,12 @@ export const NewResource = ({
|
||||
: { type: "Image", params: { image: "" } },
|
||||
}
|
||||
: type === "Stack"
|
||||
? { server_id }
|
||||
: type === "Repo"
|
||||
? { server_id, builder_id }
|
||||
: type === "Build"
|
||||
? { builder_id }
|
||||
: {};
|
||||
? { server_id }
|
||||
: type === "Repo"
|
||||
? { server_id, builder_id }
|
||||
: type === "Build"
|
||||
? { builder_id }
|
||||
: {};
|
||||
const onConfirm = async () => {
|
||||
if (!name) toast({ title: "Name cannot be empty" });
|
||||
const id = (await mutateAsync({ name, config }))._id?.$oid!;
|
||||
@@ -340,8 +345,8 @@ export const DeleteResource = ({
|
||||
type === "ServerTemplate"
|
||||
? "server_template"
|
||||
: type === "ResourceSync"
|
||||
? "sync"
|
||||
: type.toLowerCase();
|
||||
? "sync"
|
||||
: type.toLowerCase();
|
||||
const resource = useRead(`Get${type}`, {
|
||||
[key]: id,
|
||||
} as any).data;
|
||||
@@ -367,9 +372,15 @@ export const DeleteResource = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const CopyGithubWebhook = ({ path }: { path: string }) => {
|
||||
export const CopyWebhook = ({
|
||||
integration,
|
||||
path,
|
||||
}: {
|
||||
integration: WebhookIntegration;
|
||||
path: string;
|
||||
}) => {
|
||||
const base_url = useRead("GetCoreInfo", {}).data?.webhook_base_url;
|
||||
const url = base_url + "/listener/github" + path;
|
||||
const url = base_url + "/listener/" + integration.toLowerCase() + path;
|
||||
return (
|
||||
<div className="flex gap-2 items-center">
|
||||
<Input className="w-[400px] max-w-[70vw]" value={url} readOnly />
|
||||
|
||||
@@ -51,8 +51,6 @@ export const DeploymentConfig = ({
|
||||
|
||||
return (
|
||||
<Config
|
||||
resource_id={id}
|
||||
resource_type="Deployment"
|
||||
titleOther={titleOther}
|
||||
disabled={disabled}
|
||||
config={config}
|
||||
@@ -112,9 +110,9 @@ export const DeploymentConfig = ({
|
||||
image?.type === "Image" && image.params.image
|
||||
? extract_registry_domain(image.params.image)
|
||||
: image?.type === "Build" && image.params.build_id
|
||||
? builds?.find((b) => b.id === image.params.build_id)?.info
|
||||
.image_registry_domain
|
||||
: undefined;
|
||||
? builds?.find((b) => b.id === image.params.build_id)
|
||||
?.info.image_registry_domain
|
||||
: undefined;
|
||||
return (
|
||||
<AccountSelectorConfig
|
||||
id={update.server_id ?? config.server_id ?? undefined}
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
import { ConfigItem } from "@components/config/util";
|
||||
import {
|
||||
ConfigInput,
|
||||
ConfigItem,
|
||||
ConfigSwitch,
|
||||
WebhookBuilder,
|
||||
} from "@components/config/util";
|
||||
import { Section } from "@components/layouts";
|
||||
import { useLocalStorage, useRead, useWrite } from "@lib/hooks";
|
||||
import {
|
||||
useLocalStorage,
|
||||
useRead,
|
||||
useWebhookIdOrName,
|
||||
useWebhookIntegrations,
|
||||
useWrite,
|
||||
} from "@lib/hooks";
|
||||
import { Types } from "komodo_client";
|
||||
import { Card, CardHeader } from "@ui/card";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@ui/card";
|
||||
import { Input } from "@ui/input";
|
||||
import { useEffect, useState } from "react";
|
||||
import { CopyGithubWebhook, ResourceSelector } from "../common";
|
||||
import { CopyWebhook, ResourceSelector } from "../common";
|
||||
import { ConfigLayout } from "@components/config";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@ui/popover";
|
||||
import { Button } from "@ui/button";
|
||||
@@ -50,6 +61,8 @@ export const ProcedureConfig = ({ id }: { id: string }) => {
|
||||
return <ProcedureConfigInner procedure={procedure} />;
|
||||
};
|
||||
|
||||
const PROCEDURE_GIT_PROVIDER = "Procedure";
|
||||
|
||||
const ProcedureConfigInner = ({
|
||||
procedure,
|
||||
}: {
|
||||
@@ -66,8 +79,11 @@ const ProcedureConfigInner = ({
|
||||
const global_disabled =
|
||||
useRead("GetCoreInfo", {}).data?.ui_write_disabled ?? false;
|
||||
const { mutateAsync } = useWrite("UpdateProcedure");
|
||||
const stages = config.stages || procedure.config?.stages || [];
|
||||
const { integrations } = useWebhookIntegrations();
|
||||
const [id_or_name] = useWebhookIdOrName();
|
||||
const webhook_integration = integrations[PROCEDURE_GIT_PROVIDER] ?? "Github";
|
||||
|
||||
const stages = config.stages || procedure.config?.stages || [];
|
||||
const disabled = global_disabled || perms !== Types.PermissionLevel.Write;
|
||||
|
||||
const add_stage = () =>
|
||||
@@ -196,54 +212,75 @@ const ProcedureConfigInner = ({
|
||||
</ConfigLayout>
|
||||
<Section>
|
||||
<Card>
|
||||
<CardHeader className="p-4">
|
||||
<ConfigItem label="Git Webhook" className="items-start">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-nowrap text-muted-foreground">
|
||||
Listen on branch:
|
||||
</div>
|
||||
<CardHeader>
|
||||
<CardTitle>Webhook</CardTitle>
|
||||
<CardDescription>
|
||||
Trigger this Procedure with a webhook.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-4">
|
||||
<ConfigItem>
|
||||
<WebhookBuilder git_provider={PROCEDURE_GIT_PROVIDER}>
|
||||
<div className="text-nowrap text-muted-foreground text-sm">
|
||||
Listen on branch:
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Input
|
||||
placeholder="Branch"
|
||||
value={branch}
|
||||
onChange={(e) => setBranch(e.target.value)}
|
||||
className="w-[200px]"
|
||||
disabled={branch === "__ALL__"}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-muted-foreground text-sm">
|
||||
All branches:
|
||||
</div>
|
||||
<Switch
|
||||
checked={branch === "__ALL__"}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
setBranch("__ALL__");
|
||||
} else {
|
||||
setBranch("main");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<CopyGithubWebhook
|
||||
path={`/procedure/${procedure._id?.$oid!}/${branch}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-4 w-full">
|
||||
<div className="text-muted-foreground">Enabled:</div>
|
||||
<Switch
|
||||
checked={
|
||||
config.webhook_enabled ??
|
||||
procedure.config?.webhook_enabled
|
||||
}
|
||||
onCheckedChange={(webhook_enabled) =>
|
||||
setConfig({ ...config, webhook_enabled })
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-4 w-full">
|
||||
<div className="text-muted-foreground">Custom Secret:</div>
|
||||
<Input
|
||||
value={
|
||||
config.webhook_secret ?? procedure.config?.webhook_secret
|
||||
}
|
||||
onChange={(e) =>
|
||||
setConfig({ ...config, webhook_secret: e.target.value })
|
||||
}
|
||||
disabled={disabled}
|
||||
className="w-[400px] max-w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ConfigItem>
|
||||
</CardHeader>
|
||||
</WebhookBuilder>
|
||||
</ConfigItem>
|
||||
<ConfigItem label="Webhook Url - Run">
|
||||
<CopyWebhook
|
||||
integration={webhook_integration}
|
||||
path={`/procedure/${id_or_name === "Id" ? procedure._id?.$oid! : procedure.name}/${branch}`}
|
||||
/>
|
||||
</ConfigItem>
|
||||
<ConfigSwitch
|
||||
label="Webhook Enabled"
|
||||
value={
|
||||
config.webhook_enabled ?? procedure.config?.webhook_enabled
|
||||
}
|
||||
disabled={disabled}
|
||||
onChange={(webhook_enabled) =>
|
||||
setConfig({ ...config, webhook_enabled })
|
||||
}
|
||||
/>
|
||||
<ConfigInput
|
||||
label="Custom Secret"
|
||||
description="Provide a custom webhook secret for this resource, or use the global default."
|
||||
placeholder="Input custom secret"
|
||||
value={
|
||||
config.webhook_secret ?? procedure.config?.webhook_secret
|
||||
}
|
||||
disabled={disabled}
|
||||
onChange={(webhook_secret) =>
|
||||
setConfig({ ...config, webhook_secret })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Section>
|
||||
</div>
|
||||
@@ -566,7 +603,7 @@ type ExecutionType = Types.Execution["type"];
|
||||
|
||||
type ExecutionConfigComponent<
|
||||
T extends ExecutionType,
|
||||
P = Extract<Types.Execution, { type: T }>["params"]
|
||||
P = Extract<Types.Execution, { type: T }>["params"],
|
||||
> = React.FC<{
|
||||
params: P;
|
||||
setParams: React.Dispatch<React.SetStateAction<P>>;
|
||||
|
||||
@@ -5,10 +5,19 @@ import {
|
||||
InputList,
|
||||
ProviderSelectorConfig,
|
||||
SystemCommand,
|
||||
WebhookBuilder,
|
||||
} from "@components/config/util";
|
||||
import { useInvalidate, useLocalStorage, useRead, useWrite } from "@lib/hooks";
|
||||
import {
|
||||
getWebhookIntegration,
|
||||
useInvalidate,
|
||||
useLocalStorage,
|
||||
useRead,
|
||||
useWebhookIdOrName,
|
||||
useWebhookIntegrations,
|
||||
useWrite,
|
||||
} from "@lib/hooks";
|
||||
import { Types } from "komodo_client";
|
||||
import { CopyGithubWebhook, ResourceLink, ResourceSelector } from "../common";
|
||||
import { CopyWebhook, ResourceLink, ResourceSelector } from "../common";
|
||||
import { useToast } from "@ui/use-toast";
|
||||
import { text_color_class_by_intention } from "@lib/color";
|
||||
import { ConfirmButton } from "@components/util";
|
||||
@@ -21,7 +30,9 @@ export const RepoConfig = ({ id }: { id: string }) => {
|
||||
const perms = useRead("GetPermissionLevel", {
|
||||
target: { type: "Repo", id },
|
||||
}).data;
|
||||
const config = useRead("GetRepo", { repo: id }).data?.config;
|
||||
const repo = useRead("GetRepo", { repo: id }).data;
|
||||
const config = repo?.config;
|
||||
const name = repo?.name;
|
||||
const webhooks = useRead("GetRepoWebhooksEnabled", { repo: id }).data;
|
||||
const global_disabled =
|
||||
useRead("GetCoreInfo", {}).data?.ui_write_disabled ?? false;
|
||||
@@ -30,14 +41,18 @@ export const RepoConfig = ({ id }: { id: string }) => {
|
||||
{}
|
||||
);
|
||||
const { mutateAsync } = useWrite("UpdateRepo");
|
||||
const { integrations } = useWebhookIntegrations();
|
||||
const [id_or_name] = useWebhookIdOrName();
|
||||
|
||||
if (!config) return null;
|
||||
|
||||
const disabled = global_disabled || perms !== Types.PermissionLevel.Write;
|
||||
|
||||
const git_provider = update.git_provider ?? config.git_provider;
|
||||
const webhook_integration = getWebhookIntegration(integrations, git_provider);
|
||||
|
||||
return (
|
||||
<Config
|
||||
resource_id={id}
|
||||
resource_type="Repo"
|
||||
disabled={disabled}
|
||||
config={config}
|
||||
update={update}
|
||||
@@ -222,7 +237,7 @@ export const RepoConfig = ({ id }: { id: string }) => {
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Git Webhooks",
|
||||
label: "Webhooks",
|
||||
description:
|
||||
"Configure your repo provider to send webhooks to Komodo",
|
||||
components: {
|
||||
@@ -236,19 +251,31 @@ export const RepoConfig = ({ id }: { id: string }) => {
|
||||
</ConfigItem>
|
||||
);
|
||||
},
|
||||
["Builder" as any]: () => (
|
||||
<WebhookBuilder git_provider={git_provider} />
|
||||
),
|
||||
["pull" as any]: () => (
|
||||
<ConfigItem label="Pull">
|
||||
<CopyGithubWebhook path={`/repo/${id}/pull`} />
|
||||
<ConfigItem label="Webhook Url - Pull">
|
||||
<CopyWebhook
|
||||
integration={webhook_integration}
|
||||
path={`/repo/${id_or_name === "Id" ? id : name}/pull`}
|
||||
/>
|
||||
</ConfigItem>
|
||||
),
|
||||
["clone" as any]: () => (
|
||||
<ConfigItem label="Clone">
|
||||
<CopyGithubWebhook path={`/repo/${id}/clone`} />
|
||||
<ConfigItem label="Webhook Url - Clone">
|
||||
<CopyWebhook
|
||||
integration={webhook_integration}
|
||||
path={`/repo/${id_or_name === "Id" ? id : name}/clone`}
|
||||
/>
|
||||
</ConfigItem>
|
||||
),
|
||||
["build" as any]: () => (
|
||||
<ConfigItem label="Build">
|
||||
<CopyGithubWebhook path={`/repo/${id}/build`} />
|
||||
<ConfigItem label="Webhook Url - Build">
|
||||
<CopyWebhook
|
||||
integration={webhook_integration}
|
||||
path={`/repo/${id_or_name === "Id" ? id : name}/build`}
|
||||
/>
|
||||
</ConfigItem>
|
||||
),
|
||||
webhook_enabled: webhooks !== undefined && !webhooks.managed,
|
||||
|
||||
@@ -4,11 +4,20 @@ import {
|
||||
ConfigItem,
|
||||
ConfigList,
|
||||
ProviderSelectorConfig,
|
||||
WebhookBuilder,
|
||||
} from "@components/config/util";
|
||||
import { useInvalidate, useLocalStorage, useRead, useWrite } from "@lib/hooks";
|
||||
import {
|
||||
getWebhookIntegration,
|
||||
useInvalidate,
|
||||
useLocalStorage,
|
||||
useRead,
|
||||
useWebhookIdOrName,
|
||||
useWebhookIntegrations,
|
||||
useWrite,
|
||||
} from "@lib/hooks";
|
||||
import { Types } from "komodo_client";
|
||||
import { ReactNode, useState } from "react";
|
||||
import { CopyGithubWebhook } from "../common";
|
||||
import { CopyWebhook } from "../common";
|
||||
import { useToast } from "@ui/use-toast";
|
||||
import { text_color_class_by_intention } from "@lib/color";
|
||||
import { ConfirmButton, ShowHideButton } from "@components/util";
|
||||
@@ -61,7 +70,9 @@ export const ResourceSyncConfig = ({
|
||||
const perms = useRead("GetPermissionLevel", {
|
||||
target: { type: "ResourceSync", id },
|
||||
}).data;
|
||||
const config = useRead("GetResourceSync", { sync: id }).data?.config;
|
||||
const sync = useRead("GetResourceSync", { sync: id }).data;
|
||||
const config = sync?.config;
|
||||
const name = sync?.name;
|
||||
const webhooks = useRead("GetSyncWebhooksEnabled", { sync: id }).data;
|
||||
const global_disabled =
|
||||
useRead("GetCoreInfo", {}).data?.ui_write_disabled ?? false;
|
||||
@@ -70,11 +81,16 @@ export const ResourceSyncConfig = ({
|
||||
{}
|
||||
);
|
||||
const { mutateAsync } = useWrite("UpdateResourceSync");
|
||||
const { integrations } = useWebhookIntegrations();
|
||||
const [id_or_name] = useWebhookIdOrName();
|
||||
|
||||
if (!config) return null;
|
||||
|
||||
const disabled = global_disabled || perms !== Types.PermissionLevel.Write;
|
||||
|
||||
const git_provider = update.git_provider ?? config.git_provider;
|
||||
const integration = getWebhookIntegration(integrations, git_provider);
|
||||
|
||||
const mode = getSyncMode(update, config);
|
||||
const managed = update.managed ?? config.managed;
|
||||
|
||||
@@ -300,20 +316,29 @@ export const ResourceSyncConfig = ({
|
||||
</ConfigItem>
|
||||
);
|
||||
},
|
||||
["refresh" as any]: () => (
|
||||
["Builder" as any]: () => (
|
||||
<WebhookBuilder git_provider={git_provider} />
|
||||
),
|
||||
["Refresh" as any]: () => (
|
||||
<ConfigItem
|
||||
label="Refresh Pending"
|
||||
label="Webhook Url - Refresh Pending"
|
||||
description="Trigger an update of the pending sync cache, to display the changes in the UI on push."
|
||||
>
|
||||
<CopyGithubWebhook path={`/sync/${id}/refresh`} />
|
||||
<CopyWebhook
|
||||
integration={integration}
|
||||
path={`/sync/${id_or_name === "Id" ? id : name}/refresh`}
|
||||
/>
|
||||
</ConfigItem>
|
||||
),
|
||||
["sync" as any]: () => (
|
||||
["Sync" as any]: () => (
|
||||
<ConfigItem
|
||||
label="Execute Sync"
|
||||
label="Webhook Url - Execute Sync"
|
||||
description="Trigger an execution of the sync on push."
|
||||
>
|
||||
<CopyGithubWebhook path={`/sync/${id}/sync`} />
|
||||
<CopyWebhook
|
||||
integration={integration}
|
||||
path={`/sync/${id_or_name === "Id" ? id : name}/sync`}
|
||||
/>
|
||||
</ConfigItem>
|
||||
),
|
||||
webhook_enabled: webhooks !== undefined && !webhooks.managed,
|
||||
@@ -480,8 +505,6 @@ export const ResourceSyncConfig = ({
|
||||
|
||||
return (
|
||||
<Config
|
||||
resource_id={id}
|
||||
resource_type="ResourceSync"
|
||||
titleOther={titleOther}
|
||||
disabled={disabled}
|
||||
config={config}
|
||||
|
||||
@@ -42,8 +42,6 @@ export const AwsServerTemplateConfig = ({
|
||||
|
||||
return (
|
||||
<Config
|
||||
resource_id={id}
|
||||
resource_type="ServerTemplate"
|
||||
disabled={disabled}
|
||||
config={config}
|
||||
update={update}
|
||||
|
||||
@@ -50,8 +50,6 @@ export const HetznerServerTemplateConfig = ({
|
||||
|
||||
return (
|
||||
<Config
|
||||
resource_id={id}
|
||||
resource_type="ServerTemplate"
|
||||
disabled={disabled}
|
||||
config={config}
|
||||
update={update}
|
||||
|
||||
@@ -34,8 +34,6 @@ export const ServerConfig = ({
|
||||
|
||||
return (
|
||||
<Config
|
||||
resource_id={id}
|
||||
resource_type="Server"
|
||||
titleOther={titleOther}
|
||||
disabled={disabled}
|
||||
config={config}
|
||||
|
||||
@@ -7,11 +7,20 @@ import {
|
||||
InputList,
|
||||
ProviderSelectorConfig,
|
||||
SystemCommand,
|
||||
WebhookBuilder,
|
||||
} from "@components/config/util";
|
||||
import { Types } from "komodo_client";
|
||||
import { useInvalidate, useLocalStorage, useRead, useWrite } from "@lib/hooks";
|
||||
import {
|
||||
getWebhookIntegration,
|
||||
useInvalidate,
|
||||
useLocalStorage,
|
||||
useRead,
|
||||
useWebhookIdOrName,
|
||||
useWebhookIntegrations,
|
||||
useWrite,
|
||||
} from "@lib/hooks";
|
||||
import { ReactNode } from "react";
|
||||
import { CopyGithubWebhook, ResourceLink, ResourceSelector } from "../common";
|
||||
import { CopyWebhook, ResourceLink, ResourceSelector } from "../common";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -55,7 +64,9 @@ export const StackConfig = ({
|
||||
const perms = useRead("GetPermissionLevel", {
|
||||
target: { type: "Stack", id },
|
||||
}).data;
|
||||
const config = useRead("GetStack", { stack: id }).data?.config;
|
||||
const stack = useRead("GetStack", { stack: id }).data;
|
||||
const config = stack?.config;
|
||||
const name = stack?.name;
|
||||
const webhooks = useRead("GetStackWebhooksEnabled", { stack: id }).data;
|
||||
const global_disabled =
|
||||
useRead("GetCoreInfo", {}).data?.ui_write_disabled ?? false;
|
||||
@@ -64,13 +75,19 @@ export const StackConfig = ({
|
||||
{}
|
||||
);
|
||||
const { mutateAsync } = useWrite("UpdateStack");
|
||||
const { integrations } = useWebhookIntegrations();
|
||||
const [id_or_name] = useWebhookIdOrName();
|
||||
|
||||
if (!config) return null;
|
||||
|
||||
const disabled = global_disabled || perms !== Types.PermissionLevel.Write;
|
||||
|
||||
const run_build = update.run_build ?? config.run_build;
|
||||
const mode = getStackMode(update, config);
|
||||
|
||||
const git_provider = update.git_provider ?? config.git_provider;
|
||||
const webhook_integration = getWebhookIntegration(integrations, git_provider);
|
||||
|
||||
const setMode = (mode: StackMode) => {
|
||||
if (mode === "Files On Server") {
|
||||
set({ ...update, files_on_host: true });
|
||||
@@ -561,26 +578,35 @@ export const StackConfig = ({
|
||||
</ConfigItem>
|
||||
);
|
||||
},
|
||||
["Refresh" as any]: () =>
|
||||
(update.branch ?? config.branch) && (
|
||||
<ConfigItem label="Refresh Cache">
|
||||
<CopyGithubWebhook path={`/stack/${id}/refresh`} />
|
||||
</ConfigItem>
|
||||
),
|
||||
["Builder" as any]: () => (
|
||||
<WebhookBuilder git_provider={git_provider} />
|
||||
),
|
||||
// ["Refresh" as any]: () =>
|
||||
// (update.branch ?? config.branch) && (
|
||||
// <ConfigItem label="Refresh Cache">
|
||||
// <CopyWebhook
|
||||
// integration={webhook_integration}
|
||||
// path={`/stack/${id_or_name === "Id" ? id : name}/refresh`}
|
||||
// />
|
||||
// </ConfigItem>
|
||||
// ),
|
||||
["Deploy" as any]: () =>
|
||||
(update.branch ?? config.branch) && (
|
||||
<ConfigItem label="Auto Redeploy">
|
||||
<CopyGithubWebhook path={`/stack/${id}/deploy`} />
|
||||
<ConfigItem label="Webhook Url - Deploy">
|
||||
<CopyWebhook
|
||||
integration={webhook_integration}
|
||||
path={`/stack/${id_or_name === "Id" ? id : name}/deploy`}
|
||||
/>
|
||||
</ConfigItem>
|
||||
),
|
||||
webhook_enabled:
|
||||
!!(update.branch ?? config.branch) &&
|
||||
webhooks !== undefined &&
|
||||
!webhooks.managed,
|
||||
webhook_force_deploy: {
|
||||
description:
|
||||
"Usually the Stack won't deploy unless there are changes to the files. Use this to force deploy.",
|
||||
},
|
||||
webhook_enabled:
|
||||
!!(update.branch ?? config.branch) &&
|
||||
webhooks !== undefined &&
|
||||
!webhooks.managed,
|
||||
webhook_secret: {
|
||||
description:
|
||||
"Provide a custom webhook secret for this resource, or use the global default.",
|
||||
@@ -757,8 +783,6 @@ export const StackConfig = ({
|
||||
|
||||
return (
|
||||
<Config
|
||||
resource_id={id}
|
||||
resource_type="Stack"
|
||||
titleOther={titleOther}
|
||||
disabled={disabled}
|
||||
config={config}
|
||||
|
||||
@@ -28,7 +28,6 @@ import { Link } from "react-router-dom";
|
||||
import { fmt_duration, fmt_operation, fmt_version } from "@lib/formatting";
|
||||
import {
|
||||
cn,
|
||||
is_service_user,
|
||||
updateLogToHtml,
|
||||
usableResourcePath,
|
||||
version_is_none,
|
||||
@@ -50,44 +49,6 @@ export const UpdateUser = ({
|
||||
iconSize?: number;
|
||||
defaultAvatar?: boolean;
|
||||
muted?: boolean;
|
||||
}) => {
|
||||
if (is_service_user(user_id)) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-nowrap",
|
||||
muted && "text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<User className={`w-${iconSize} h-${iconSize}`} />
|
||||
{user_id}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<RealUpdateUser
|
||||
user_id={user_id}
|
||||
className={className}
|
||||
iconSize={iconSize}
|
||||
defaultAvatar={defaultAvatar}
|
||||
muted={muted}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const RealUpdateUser = ({
|
||||
user_id,
|
||||
className,
|
||||
iconSize = 4,
|
||||
defaultAvatar,
|
||||
muted,
|
||||
}: {
|
||||
user_id: string;
|
||||
className?: string;
|
||||
iconSize?: number;
|
||||
defaultAvatar?: boolean;
|
||||
muted?: boolean;
|
||||
}) => {
|
||||
const res = useRead("GetUsername", { user_id }).data;
|
||||
const username = res?.username;
|
||||
|
||||
@@ -67,7 +67,7 @@ export const useRead = <
|
||||
(T | P)[]
|
||||
>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
>,
|
||||
>(
|
||||
type: T,
|
||||
params: P,
|
||||
@@ -83,7 +83,7 @@ export const useInvalidate = () => {
|
||||
const qc = useQueryClient();
|
||||
return <
|
||||
Type extends Types.ReadRequest["type"],
|
||||
Params extends Extract<Types.ReadRequest, { type: Type }>["params"]
|
||||
Params extends Extract<Types.ReadRequest, { type: Type }>["params"],
|
||||
>(
|
||||
...keys: Array<[Type] | [Type, Params]>
|
||||
) => keys.forEach((key) => qc.invalidateQueries({ queryKey: key }));
|
||||
@@ -96,7 +96,7 @@ export const useManageUser = <
|
||||
C extends Omit<
|
||||
UseMutationOptions<UserResponses[T], unknown, P, unknown>,
|
||||
"mutationKey" | "mutationFn"
|
||||
>
|
||||
>,
|
||||
>(
|
||||
type: T,
|
||||
config?: C
|
||||
@@ -130,7 +130,7 @@ export const useWrite = <
|
||||
C extends Omit<
|
||||
UseMutationOptions<WriteResponses[R["type"]], unknown, P, unknown>,
|
||||
"mutationKey" | "mutationFn"
|
||||
>
|
||||
>,
|
||||
>(
|
||||
type: T,
|
||||
config?: C
|
||||
@@ -164,7 +164,7 @@ export const useExecute = <
|
||||
C extends Omit<
|
||||
UseMutationOptions<ExecuteResponses[T], unknown, P, unknown>,
|
||||
"mutationKey" | "mutationFn"
|
||||
>
|
||||
>,
|
||||
>(
|
||||
type: T,
|
||||
config?: C
|
||||
@@ -198,7 +198,7 @@ export const useAuth = <
|
||||
C extends Omit<
|
||||
UseMutationOptions<AuthResponses[T], unknown, P, unknown>,
|
||||
"mutationKey" | "mutationFn"
|
||||
>
|
||||
>,
|
||||
>(
|
||||
type: T,
|
||||
config?: C
|
||||
@@ -446,3 +446,49 @@ export const useNoResources = () => {
|
||||
syncs === 0
|
||||
);
|
||||
};
|
||||
|
||||
export type WebhookIntegration = "Github" | "Gitlab";
|
||||
export type WebhookIntegrations = {
|
||||
[key: string]: WebhookIntegration;
|
||||
};
|
||||
|
||||
const WEBHOOK_INTEGRATIONS_ATOM = atomWithStorage<WebhookIntegrations>(
|
||||
"webhook-integrations-v2",
|
||||
{}
|
||||
);
|
||||
|
||||
export const useWebhookIntegrations = () => {
|
||||
const [integrations, setIntegrations] = useAtom<WebhookIntegrations>(
|
||||
WEBHOOK_INTEGRATIONS_ATOM
|
||||
);
|
||||
return {
|
||||
integrations,
|
||||
setIntegration: (provider: string, integration: WebhookIntegration) =>
|
||||
setIntegrations({
|
||||
...integrations,
|
||||
[provider]: integration,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
export const getWebhookIntegration = (
|
||||
integrations: WebhookIntegrations,
|
||||
git_provider: string
|
||||
) => {
|
||||
return integrations[git_provider]
|
||||
? integrations[git_provider]
|
||||
: git_provider.includes("gitlab")
|
||||
? "Gitlab"
|
||||
: "Github";
|
||||
};
|
||||
|
||||
export type WebhookIdOrName = "Id" | "Name";
|
||||
|
||||
const WEBHOOK_ID_OR_NAME_ATOM = atomWithStorage<WebhookIdOrName>(
|
||||
"webhook-id-or-name-v1",
|
||||
"Id"
|
||||
);
|
||||
|
||||
export const useWebhookIdOrName = () => {
|
||||
return useAtom<WebhookIdOrName>(WEBHOOK_ID_OR_NAME_ATOM);
|
||||
};
|
||||
|
||||
@@ -231,20 +231,6 @@ export const sync_no_changes = (sync: Types.ResourceSync) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const is_service_user = (user_id: string) => {
|
||||
return (
|
||||
user_id === "System" ||
|
||||
user_id === "Procedure" ||
|
||||
user_id === "Github" ||
|
||||
user_id === "Git Webhook" ||
|
||||
user_id === "Auto Redeploy" ||
|
||||
user_id === "Resource Sync" ||
|
||||
user_id === "Stack Wizard" ||
|
||||
user_id === "Build Manager" ||
|
||||
user_id === "Repo Manager"
|
||||
);
|
||||
};
|
||||
|
||||
export const extract_registry_domain = (image_name: string) => {
|
||||
if (!image_name) return "docker.io";
|
||||
const maybe_domain = image_name.split("/")[0];
|
||||
|
||||
16
runfile.toml
16
runfile.toml
@@ -1,4 +1,5 @@
|
||||
[start-frontend]
|
||||
description = "starts the frontend in dev mode"
|
||||
path = "frontend"
|
||||
cmd = "yarn dev"
|
||||
|
||||
@@ -9,7 +10,16 @@ node ./client/core/ts/generate_types.mjs && \
|
||||
cd ./client/core/ts && yarn build && \
|
||||
cp -r dist/. ../../../frontend/public/client/."""
|
||||
|
||||
[link-client]
|
||||
description = "yarn links the ts client to the frontend"
|
||||
after = "gen-client"
|
||||
cmd = """
|
||||
cd ./client/core/ts && yarn link && \
|
||||
cd ../../../frontend && yarn link komodo_client && yarn
|
||||
"""
|
||||
|
||||
[build-frontend]
|
||||
description = "generates fresh ts client and builds the frontend"
|
||||
path = "frontend"
|
||||
cmd = "yarn build"
|
||||
after = "gen-client"
|
||||
@@ -24,6 +34,12 @@ cmd = """
|
||||
docker compose -p komodo-dev -f test.compose.yaml down --remove-orphans && \
|
||||
docker compose -p komodo-dev -f test.compose.yaml up -d"""
|
||||
|
||||
[test-compose-exposed]
|
||||
description = "deploys test.compose.yaml with exposed port and non-ssl periphery"
|
||||
cmd = """
|
||||
docker compose -p komodo-dev -f test.compose.yaml -f expose.compose.yaml down --remove-orphans && \
|
||||
docker compose -p komodo-dev -f test.compose.yaml -f expose.compose.yaml up -d"""
|
||||
|
||||
[test-compose-build]
|
||||
description = "builds and deploys test.compose.yaml"
|
||||
cmd = """
|
||||
|
||||
@@ -61,5 +61,5 @@ to the current default.
|
||||
Example:
|
||||
|
||||
```sh
|
||||
curl -sSL https://raw.githubusercontent.com/mbecker20/komodo/main/scripts/setup-periphery.py | python3 --force-service-file
|
||||
curl -sSL https://raw.githubusercontent.com/mbecker20/komodo/main/scripts/setup-periphery.py | python3 - --force-service-file
|
||||
```
|
||||
Reference in New Issue
Block a user