forked from github-starred/komodo
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a9ad42203 | ||
|
|
3f1cfa9064 | ||
|
|
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": []
|
||||
},
|
||||
]
|
||||
}
|
||||
25
Cargo.lock
generated
25
Cargo.lock
generated
@@ -41,7 +41,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "alerter"
|
||||
version = "1.16.1"
|
||||
version = "1.16.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -943,7 +943,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "command"
|
||||
version = "1.16.1"
|
||||
version = "1.16.4"
|
||||
dependencies = [
|
||||
"komodo_client",
|
||||
"run_command",
|
||||
@@ -1355,7 +1355,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "environment_file"
|
||||
version = "1.16.1"
|
||||
version = "1.16.4"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
@@ -1439,7 +1439,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "formatting"
|
||||
version = "1.16.1"
|
||||
version = "1.16.4"
|
||||
dependencies = [
|
||||
"serror",
|
||||
]
|
||||
@@ -1571,7 +1571,7 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||
|
||||
[[package]]
|
||||
name = "git"
|
||||
version = "1.16.1"
|
||||
version = "1.16.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"command",
|
||||
@@ -2191,7 +2191,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_cli"
|
||||
version = "1.16.1"
|
||||
version = "1.16.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -2207,7 +2207,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_client"
|
||||
version = "1.16.1"
|
||||
version = "1.16.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -2238,7 +2238,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_core"
|
||||
version = "1.16.1"
|
||||
version = "1.16.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -2296,7 +2296,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_periphery"
|
||||
version = "1.16.1"
|
||||
version = "1.16.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -2383,7 +2383,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logger"
|
||||
version = "1.16.1"
|
||||
version = "1.16.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"komodo_client",
|
||||
@@ -3089,7 +3089,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||
|
||||
[[package]]
|
||||
name = "periphery_client"
|
||||
version = "1.16.1"
|
||||
version = "1.16.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"komodo_client",
|
||||
@@ -3391,6 +3391,7 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2 0.4.6",
|
||||
@@ -4863,7 +4864,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "update_logger"
|
||||
version = "1.16.1"
|
||||
version = "1.16.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"komodo_client",
|
||||
|
||||
@@ -9,7 +9,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.16.1"
|
||||
version = "1.16.4"
|
||||
edition = "2021"
|
||||
authors = ["mbecker20 <becker.maxh@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
@@ -40,7 +40,9 @@ pub fn komodo_client() -> &'static KomodoClient {
|
||||
creds
|
||||
}
|
||||
};
|
||||
futures::executor::block_on(KomodoClient::new(url, key, secret))
|
||||
.expect("failed to initialize Komodo client")
|
||||
futures::executor::block_on(
|
||||
KomodoClient::new(url, key, secret).with_healthcheck(),
|
||||
)
|
||||
.expect("failed to initialize Komodo client")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -34,6 +34,12 @@ COPY --from=core-builder /builder/target/release/core /app
|
||||
COPY --from=frontend-builder /builder/frontend/dist /app/frontend
|
||||
COPY --from=denoland/deno:bin /deno /usr/local/bin/deno
|
||||
|
||||
# Set $DENO_DIR and preload external Deno deps
|
||||
ENV DENO_DIR=/action-cache/deno
|
||||
RUN mkdir /action-cache && \
|
||||
cd /action-cache && \
|
||||
deno install jsr:@std/yaml jsr:@std/toml
|
||||
|
||||
# Hint at the port
|
||||
EXPOSE 9120
|
||||
|
||||
|
||||
@@ -29,6 +29,12 @@ COPY --from=core-builder /builder/target/release/core /app
|
||||
COPY --from=frontend-builder /builder/frontend/dist /app/frontend
|
||||
COPY --from=denoland/deno:bin /deno /usr/local/bin/deno
|
||||
|
||||
# Set $DENO_DIR and preload external Deno deps
|
||||
ENV DENO_DIR=/action-cache/deno
|
||||
RUN mkdir /action-cache && \
|
||||
cd /action-cache && \
|
||||
deno install jsr:@std/yaml jsr:@std/toml
|
||||
|
||||
# Hint at the port
|
||||
EXPOSE 9120
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
use std::collections::HashSet;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
sync::OnceLock,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use command::run_komodo_command;
|
||||
@@ -81,23 +86,22 @@ impl Resolve<RunAction, (User, Update)> for State {
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let path = core_config()
|
||||
.action_directory
|
||||
.join(format!("{}.ts", random_string(10)));
|
||||
let file = format!("{}.ts", random_string(10));
|
||||
let path = core_config().action_directory.join(&file);
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = fs::create_dir_all(parent).await;
|
||||
}
|
||||
|
||||
fs::write(&path, contents).await.with_context(|| {
|
||||
format!("Faild to write action file to {path:?}")
|
||||
format!("Failed to write action file to {path:?}")
|
||||
})?;
|
||||
|
||||
let mut res = run_komodo_command(
|
||||
// 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-all {}", path.display()),
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
@@ -107,11 +111,7 @@ impl Resolve<RunAction, (User, Update)> for State {
|
||||
res.stderr = svi::replace_in_string(&res.stderr, &replacers)
|
||||
.replace(&secret, "<ACTION_API_SECRET>");
|
||||
|
||||
if let Err(e) = fs::remove_file(path).await {
|
||||
warn!(
|
||||
"Failed to delete action file after action execution | {e:#}"
|
||||
);
|
||||
}
|
||||
cleanup_run(file + ".js", &path).await;
|
||||
|
||||
if let Err(e) = State
|
||||
.resolve(DeleteApiKey { key }, action_user().to_owned())
|
||||
@@ -184,6 +184,22 @@ fn full_contents(contents: &str, key: &str, secret: &str) -> String {
|
||||
let base_url = format!("{protocol}://localhost:{port}");
|
||||
format!(
|
||||
"import {{ KomodoClient }} from '{base_url}/client/lib.js';
|
||||
import * as __YAML__ from 'jsr:@std/yaml';
|
||||
import * as __TOML__ from 'jsr:@std/toml';
|
||||
|
||||
const YAML = {{
|
||||
stringify: __YAML__.stringify,
|
||||
parse: __YAML__.parse,
|
||||
parseAll: __YAML__.parseAll,
|
||||
parseDockerCompose: __YAML__.parse,
|
||||
}}
|
||||
|
||||
const TOML = {{
|
||||
stringify: __TOML__.stringify,
|
||||
parse: __TOML__.parse,
|
||||
parseResourceToml: __TOML__.parse,
|
||||
parseCargoToml: __TOML__.parse,
|
||||
}}
|
||||
|
||||
const komodo = KomodoClient('{base_url}', {{
|
||||
type: 'api-key',
|
||||
@@ -204,3 +220,84 @@ main().catch(error => {{
|
||||
}}).then(() => console.log('🦎 Action completed successfully 🦎'));"
|
||||
)
|
||||
}
|
||||
|
||||
/// Cleans up file at given path.
|
||||
/// ALSO if $DENO_DIR is set,
|
||||
/// will clean up the generated file matching "file"
|
||||
async fn cleanup_run(file: String, path: &Path) {
|
||||
if let Err(e) = fs::remove_file(path).await {
|
||||
warn!(
|
||||
"Failed to delete action file after action execution | {e:#}"
|
||||
);
|
||||
}
|
||||
// If $DENO_DIR is set (will be in container),
|
||||
// will clean up the generated file matching "file" (NOT under path)
|
||||
let Some(deno_dir) = deno_dir() else {
|
||||
return;
|
||||
};
|
||||
delete_file(deno_dir.join("gen/file"), file).await;
|
||||
}
|
||||
|
||||
fn deno_dir() -> Option<&'static Path> {
|
||||
static DENO_DIR: OnceLock<Option<PathBuf>> = OnceLock::new();
|
||||
DENO_DIR
|
||||
.get_or_init(|| {
|
||||
let deno_dir = std::env::var("DENO_DIR").ok()?;
|
||||
PathBuf::from_str(&deno_dir).ok()
|
||||
})
|
||||
.as_deref()
|
||||
}
|
||||
|
||||
/// file is just the terminating file path,
|
||||
/// it may be nested multiple folder under path,
|
||||
/// this will find the nested file and delete it.
|
||||
/// Assumes the file is only there once.
|
||||
fn delete_file(
|
||||
dir: PathBuf,
|
||||
file: String,
|
||||
) -> std::pin::Pin<Box<dyn std::future::Future<Output = bool> + Send>>
|
||||
{
|
||||
Box::pin(async move {
|
||||
let Ok(mut dir) = fs::read_dir(dir).await else {
|
||||
return false;
|
||||
};
|
||||
// Collect the nested folders for recursing
|
||||
// only after checking all the files in directory.
|
||||
let mut folders = Vec::<PathBuf>::new();
|
||||
|
||||
while let Ok(Some(entry)) = dir.next_entry().await {
|
||||
let Ok(meta) = entry.metadata().await else {
|
||||
continue;
|
||||
};
|
||||
if meta.is_file() {
|
||||
let Ok(name) = entry.file_name().into_string() else {
|
||||
continue;
|
||||
};
|
||||
if name == file {
|
||||
if let Err(e) = fs::remove_file(entry.path()).await {
|
||||
warn!(
|
||||
"Failed to clean up generated file after action execution | {e:#}"
|
||||
);
|
||||
};
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
folders.push(entry.path());
|
||||
}
|
||||
}
|
||||
|
||||
if folders.len() == 1 {
|
||||
// unwrap ok, folders definitely is not empty
|
||||
let folder = folders.pop().unwrap();
|
||||
delete_file(folder, file).await
|
||||
} else {
|
||||
// Check folders with file.clone
|
||||
for folder in folders {
|
||||
if delete_file(folder, file.clone()).await {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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, ())
|
||||
|
||||
@@ -11,7 +11,9 @@ repository.workspace = true
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
# default = ["blocking"] # use to dev client blocking mode
|
||||
mongo = ["dep:mongo_indexed"]
|
||||
blocking = ["reqwest/blocking"]
|
||||
|
||||
[dependencies]
|
||||
# mogh
|
||||
|
||||
@@ -1,4 +1,35 @@
|
||||
# Komodo
|
||||
*A system to build and deploy software across many servers*
|
||||
|
||||
Docs: [https://docs.rs/komodo_client/latest/komodo_client](https://docs.rs/komodo_client/latest/komodo_client)
|
||||
Full Docs: [https://docs.rs/komodo_client/latest/komodo_client](https://docs.rs/komodo_client/latest/komodo_client).
|
||||
|
||||
This is a client library for the Komodo Core API.
|
||||
It contains:
|
||||
- Definitions for the application [api](https://docs.rs/komodo_client/latest/komodo_client/api/index.html)
|
||||
and [entities](https://docs.rs/komodo_client/latest/komodo_client/entities/index.html).
|
||||
- A [client](https://docs.rs/komodo_client/latest/komodo_client/struct.KomodoClient.html)
|
||||
to interact with the Komodo Core API.
|
||||
- Information on configuring Komodo
|
||||
[Core](https://docs.rs/komodo_client/latest/komodo_client/entities/config/core/index.html) and
|
||||
[Periphery](https://docs.rs/komodo_client/latest/komodo_client/entities/config/periphery/index.html).
|
||||
|
||||
## Client Configuration
|
||||
|
||||
The client includes a convenenience method to parse the Komodo API url and credentials from the environment:
|
||||
- `KOMODO_ADDRESS`
|
||||
- `KOMODO_API_KEY`
|
||||
- `KOMODO_API_SECRET`
|
||||
|
||||
## Client Example
|
||||
```rust
|
||||
dotenvy::dotenv().ok();
|
||||
|
||||
let client = KomodoClient::new_from_env()?;
|
||||
|
||||
// Get all the deployments
|
||||
let deployments = client.read(ListDeployments::default()).await?;
|
||||
|
||||
println!("{deployments:#?}");
|
||||
|
||||
let update = client.execute(RunBuild { build: "test-build".to_string() }).await?:
|
||||
```
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,87 +1,89 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typeshare::typeshare;
|
||||
|
||||
use super::{
|
||||
action::PartialActionConfig, alerter::PartialAlerterConfig,
|
||||
build::PartialBuildConfig, builder::PartialBuilderConfig,
|
||||
deployment::PartialDeploymentConfig, permission::PermissionLevel,
|
||||
procedure::PartialProcedureConfig, repo::PartialRepoConfig,
|
||||
server::PartialServerConfig,
|
||||
action::_PartialActionConfig, alerter::_PartialAlerterConfig,
|
||||
build::_PartialBuildConfig, builder::_PartialBuilderConfig,
|
||||
deployment::_PartialDeploymentConfig, permission::PermissionLevel,
|
||||
procedure::_PartialProcedureConfig, repo::_PartialRepoConfig,
|
||||
server::_PartialServerConfig,
|
||||
server_template::PartialServerTemplateConfig,
|
||||
stack::PartialStackConfig, sync::PartialResourceSyncConfig,
|
||||
stack::_PartialStackConfig, sync::_PartialResourceSyncConfig,
|
||||
variable::Variable, ResourceTarget, ResourceTargetVariant,
|
||||
};
|
||||
|
||||
/// Specifies resources to sync on Komodo
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct ResourcesToml {
|
||||
#[serde(
|
||||
default,
|
||||
rename = "server",
|
||||
alias = "server",
|
||||
skip_serializing_if = "Vec::is_empty"
|
||||
)]
|
||||
pub servers: Vec<ResourceToml<PartialServerConfig>>,
|
||||
pub servers: Vec<ResourceToml<_PartialServerConfig>>,
|
||||
|
||||
#[serde(
|
||||
default,
|
||||
rename = "deployment",
|
||||
alias = "deployment",
|
||||
skip_serializing_if = "Vec::is_empty"
|
||||
)]
|
||||
pub deployments: Vec<ResourceToml<PartialDeploymentConfig>>,
|
||||
pub deployments: Vec<ResourceToml<_PartialDeploymentConfig>>,
|
||||
|
||||
#[serde(
|
||||
default,
|
||||
rename = "stack",
|
||||
alias = "stack",
|
||||
skip_serializing_if = "Vec::is_empty"
|
||||
)]
|
||||
pub stacks: Vec<ResourceToml<PartialStackConfig>>,
|
||||
pub stacks: Vec<ResourceToml<_PartialStackConfig>>,
|
||||
|
||||
#[serde(
|
||||
default,
|
||||
rename = "build",
|
||||
alias = "build",
|
||||
skip_serializing_if = "Vec::is_empty"
|
||||
)]
|
||||
pub builds: Vec<ResourceToml<PartialBuildConfig>>,
|
||||
pub builds: Vec<ResourceToml<_PartialBuildConfig>>,
|
||||
|
||||
#[serde(
|
||||
default,
|
||||
rename = "repo",
|
||||
alias = "repo",
|
||||
skip_serializing_if = "Vec::is_empty"
|
||||
)]
|
||||
pub repos: Vec<ResourceToml<PartialRepoConfig>>,
|
||||
pub repos: Vec<ResourceToml<_PartialRepoConfig>>,
|
||||
|
||||
#[serde(
|
||||
default,
|
||||
rename = "procedure",
|
||||
alias = "procedure",
|
||||
skip_serializing_if = "Vec::is_empty"
|
||||
)]
|
||||
pub procedures: Vec<ResourceToml<PartialProcedureConfig>>,
|
||||
pub procedures: Vec<ResourceToml<_PartialProcedureConfig>>,
|
||||
|
||||
#[serde(
|
||||
default,
|
||||
rename = "action",
|
||||
alias = "action",
|
||||
skip_serializing_if = "Vec::is_empty"
|
||||
)]
|
||||
pub actions: Vec<ResourceToml<PartialActionConfig>>,
|
||||
pub actions: Vec<ResourceToml<_PartialActionConfig>>,
|
||||
|
||||
#[serde(
|
||||
default,
|
||||
rename = "alerter",
|
||||
alias = "alerter",
|
||||
skip_serializing_if = "Vec::is_empty"
|
||||
)]
|
||||
pub alerters: Vec<ResourceToml<PartialAlerterConfig>>,
|
||||
pub alerters: Vec<ResourceToml<_PartialAlerterConfig>>,
|
||||
|
||||
#[serde(
|
||||
default,
|
||||
rename = "builder",
|
||||
alias = "builder",
|
||||
skip_serializing_if = "Vec::is_empty"
|
||||
)]
|
||||
pub builders: Vec<ResourceToml<PartialBuilderConfig>>,
|
||||
pub builders: Vec<ResourceToml<_PartialBuilderConfig>>,
|
||||
|
||||
#[serde(
|
||||
default,
|
||||
rename = "server_template",
|
||||
alias = "server_template",
|
||||
skip_serializing_if = "Vec::is_empty"
|
||||
)]
|
||||
pub server_templates:
|
||||
@@ -89,26 +91,27 @@ pub struct ResourcesToml {
|
||||
|
||||
#[serde(
|
||||
default,
|
||||
rename = "resource_sync",
|
||||
alias = "resource_sync",
|
||||
skip_serializing_if = "Vec::is_empty"
|
||||
)]
|
||||
pub resource_syncs: Vec<ResourceToml<PartialResourceSyncConfig>>,
|
||||
pub resource_syncs: Vec<ResourceToml<_PartialResourceSyncConfig>>,
|
||||
|
||||
#[serde(
|
||||
default,
|
||||
rename = "user_group",
|
||||
alias = "user_group",
|
||||
skip_serializing_if = "Vec::is_empty"
|
||||
)]
|
||||
pub user_groups: Vec<UserGroupToml>,
|
||||
|
||||
#[serde(
|
||||
default,
|
||||
rename = "variable",
|
||||
alias = "variable",
|
||||
skip_serializing_if = "Vec::is_empty"
|
||||
)]
|
||||
pub variables: Vec<Variable>,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ResourceToml<PartialConfig: Default> {
|
||||
/// The resource name. Required
|
||||
@@ -146,6 +149,7 @@ fn is_false(b: &bool) -> bool {
|
||||
!b
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UserGroupToml {
|
||||
/// User group name
|
||||
@@ -164,6 +168,7 @@ pub struct UserGroupToml {
|
||||
pub permissions: Vec<PermissionToml>,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct PermissionToml {
|
||||
/// Id can be:
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
//! It contains:
|
||||
//! - Definitions for the application [api] and [entities].
|
||||
//! - A [client][KomodoClient] to interact with the Komodo Core API.
|
||||
//! - Information on configuring Komodo [core][entities::config::core] and [periphery][entities::config::periphery].
|
||||
//! - Information on configuring Komodo [Core][entities::config::core] and [Periphery][entities::config::periphery].
|
||||
//!
|
||||
//! ## Client Configuration
|
||||
//!
|
||||
//! The client includes a convenenience method to parse the Komodo API url and credentials from the environment:
|
||||
//! - KOMODO_ADDRESS
|
||||
//! - KOMODO_API_KEY
|
||||
//! - KOMODO_API_SECRET
|
||||
//! - `KOMODO_ADDRESS`
|
||||
//! - `KOMODO_API_KEY`
|
||||
//! - `KOMODO_API_SECRET`
|
||||
//!
|
||||
//! ## Client Example
|
||||
//! ```
|
||||
@@ -28,64 +28,146 @@
|
||||
//! let update = client.execute(RunBuild { build: "test-build".to_string() }).await?:
|
||||
//! ```
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use anyhow::Context;
|
||||
use api::read::GetVersion;
|
||||
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;
|
||||
|
||||
/// &'static KomodoClient initialized from environment.
|
||||
pub fn komodo_client() -> &'static KomodoClient {
|
||||
static KOMODO_CLIENT: OnceLock<KomodoClient> = OnceLock::new();
|
||||
KOMODO_CLIENT.get_or_init(|| {
|
||||
KomodoClient::new_from_env()
|
||||
.context("Missing KOMODO_ADDRESS, KOMODO_API_KEY, KOMODO_API_SECRET from env")
|
||||
.unwrap()
|
||||
})
|
||||
}
|
||||
|
||||
/// Default environment variables for the [KomodoClient].
|
||||
#[derive(Deserialize)]
|
||||
struct KomodoEnv {
|
||||
/// KOMODO_ADDRESS
|
||||
komodo_address: String,
|
||||
/// KOMODO_API_KEY
|
||||
komodo_api_key: String,
|
||||
/// KOMODO_API_SECRET
|
||||
komodo_api_secret: String,
|
||||
}
|
||||
|
||||
/// Client to interface with [Komodo](https://komo.do/docs/api#rust-client)
|
||||
#[derive(Clone)]
|
||||
pub struct KomodoClient {
|
||||
#[cfg(not(feature = "blocking"))]
|
||||
reqwest: reqwest::Client,
|
||||
#[cfg(feature = "blocking")]
|
||||
reqwest: reqwest::blocking::Client,
|
||||
address: String,
|
||||
key: String,
|
||||
secret: String,
|
||||
}
|
||||
|
||||
impl KomodoClient {
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn new(
|
||||
/// Initializes KomodoClient, including a health check.
|
||||
pub fn new(
|
||||
address: impl Into<String>,
|
||||
key: impl Into<String>,
|
||||
secret: impl Into<String>,
|
||||
) -> anyhow::Result<KomodoClient> {
|
||||
let client = KomodoClient {
|
||||
) -> KomodoClient {
|
||||
KomodoClient {
|
||||
reqwest: Default::default(),
|
||||
address: address.into(),
|
||||
key: key.into(),
|
||||
secret: secret.into(),
|
||||
};
|
||||
client.read(GetVersion {}).await?;
|
||||
Ok(client)
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn new_from_env() -> anyhow::Result<KomodoClient> {
|
||||
/// Initializes KomodoClient from environment: [KomodoEnv]
|
||||
pub fn new_from_env() -> anyhow::Result<KomodoClient> {
|
||||
let KomodoEnv {
|
||||
komodo_address,
|
||||
komodo_api_key,
|
||||
komodo_api_secret,
|
||||
} = envy::from_env()
|
||||
.context("failed to parse environment for komodo client")?;
|
||||
KomodoClient::new(
|
||||
Ok(KomodoClient::new(
|
||||
komodo_address,
|
||||
komodo_api_key,
|
||||
komodo_api_secret,
|
||||
)
|
||||
.await
|
||||
))
|
||||
}
|
||||
|
||||
/// Add a healthcheck in the initialization pipeline:
|
||||
///
|
||||
/// ```rust
|
||||
/// let komodo = KomodoClient::new_from_env()?
|
||||
/// .with_healthcheck().await?;
|
||||
/// ```
|
||||
#[cfg(not(feature = "blocking"))]
|
||||
pub async fn with_healthcheck(self) -> anyhow::Result<Self> {
|
||||
self.health_check().await?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Add a healthcheck in the initialization pipeline:
|
||||
///
|
||||
/// ```rust
|
||||
/// let komodo = KomodoClient::new_from_env()?
|
||||
/// .with_healthcheck().await?;
|
||||
/// ```
|
||||
#[cfg(feature = "blocking")]
|
||||
pub fn with_healthcheck(self) -> anyhow::Result<Self> {
|
||||
self.health_check()?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Get the Core version.
|
||||
#[cfg(not(feature = "blocking"))]
|
||||
pub async fn core_version(&self) -> anyhow::Result<String> {
|
||||
self.read(GetVersion {}).await.map(|r| r.version)
|
||||
}
|
||||
|
||||
/// Get the Core version.
|
||||
#[cfg(feature = "blocking")]
|
||||
pub fn core_version(&self) -> anyhow::Result<String> {
|
||||
self.read(GetVersion {}).map(|r| r.version)
|
||||
}
|
||||
|
||||
/// Send a health check.
|
||||
#[cfg(not(feature = "blocking"))]
|
||||
pub async fn health_check(&self) -> anyhow::Result<()> {
|
||||
self.read(GetVersion {}).await.map(|_| ())
|
||||
}
|
||||
|
||||
/// Send a health check.
|
||||
#[cfg(feature = "blocking")]
|
||||
pub fn health_check(&self) -> anyhow::Result<()> {
|
||||
self.read(GetVersion {}).map(|_| ())
|
||||
}
|
||||
|
||||
/// Use a custom reqwest client.
|
||||
#[cfg(not(feature = "blocking"))]
|
||||
pub fn set_reqwest(mut self, reqwest: reqwest::Client) -> Self {
|
||||
self.reqwest = reqwest;
|
||||
self
|
||||
}
|
||||
|
||||
/// Use a custom reqwest client.
|
||||
#[cfg(feature = "blocking")]
|
||||
pub fn set_reqwest(
|
||||
mut self,
|
||||
reqwest: reqwest::blocking::Client,
|
||||
) -> Self {
|
||||
self.reqwest = reqwest;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::{
|
||||
};
|
||||
|
||||
impl KomodoClient {
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[cfg(not(feature = "blocking"))]
|
||||
pub async fn auth<T: KomodoAuthRequest>(
|
||||
&self,
|
||||
request: T,
|
||||
@@ -30,7 +30,21 @@ impl KomodoClient {
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[cfg(feature = "blocking")]
|
||||
pub fn auth<T: KomodoAuthRequest>(
|
||||
&self,
|
||||
request: T,
|
||||
) -> anyhow::Result<T::Response> {
|
||||
self.post(
|
||||
"/auth",
|
||||
json!({
|
||||
"type": T::req_type(),
|
||||
"params": request
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "blocking"))]
|
||||
pub async fn user<T: KomodoUserRequest>(
|
||||
&self,
|
||||
request: T,
|
||||
@@ -46,7 +60,21 @@ impl KomodoClient {
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[cfg(feature = "blocking")]
|
||||
pub fn user<T: KomodoUserRequest>(
|
||||
&self,
|
||||
request: T,
|
||||
) -> anyhow::Result<T::Response> {
|
||||
self.post(
|
||||
"/auth",
|
||||
json!({
|
||||
"type": T::req_type(),
|
||||
"params": request
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "blocking"))]
|
||||
pub async fn read<T: KomodoReadRequest>(
|
||||
&self,
|
||||
request: T,
|
||||
@@ -62,7 +90,21 @@ impl KomodoClient {
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[cfg(feature = "blocking")]
|
||||
pub fn read<T: KomodoReadRequest>(
|
||||
&self,
|
||||
request: T,
|
||||
) -> anyhow::Result<T::Response> {
|
||||
self.post(
|
||||
"/read",
|
||||
json!({
|
||||
"type": T::req_type(),
|
||||
"params": request
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "blocking"))]
|
||||
pub async fn write<T: KomodoWriteRequest>(
|
||||
&self,
|
||||
request: T,
|
||||
@@ -78,7 +120,21 @@ impl KomodoClient {
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[cfg(feature = "blocking")]
|
||||
pub fn write<T: KomodoWriteRequest>(
|
||||
&self,
|
||||
request: T,
|
||||
) -> anyhow::Result<T::Response> {
|
||||
self.post(
|
||||
"/write",
|
||||
json!({
|
||||
"type": T::req_type(),
|
||||
"params": request
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "blocking"))]
|
||||
pub async fn execute<T: KomodoExecuteRequest>(
|
||||
&self,
|
||||
request: T,
|
||||
@@ -94,7 +150,21 @@ impl KomodoClient {
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[cfg(feature = "blocking")]
|
||||
pub fn execute<T: KomodoExecuteRequest>(
|
||||
&self,
|
||||
request: T,
|
||||
) -> anyhow::Result<T::Response> {
|
||||
self.post(
|
||||
"/execute",
|
||||
json!({
|
||||
"type": T::req_type(),
|
||||
"params": request
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "blocking"))]
|
||||
async fn post<
|
||||
B: Serialize + std::fmt::Debug,
|
||||
R: DeserializeOwned,
|
||||
@@ -108,29 +178,48 @@ impl KomodoClient {
|
||||
.post(format!("{}{endpoint}", self.address))
|
||||
.header("x-api-key", &self.key)
|
||||
.header("x-api-secret", &self.secret)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("content-type", "application/json")
|
||||
.json(&body);
|
||||
let res =
|
||||
req.send().await.context("failed to reach Komodo API")?;
|
||||
tracing::debug!("got response");
|
||||
let status = res.status();
|
||||
if status == StatusCode::OK {
|
||||
tracing::debug!("response is OK");
|
||||
match res.json().await {
|
||||
Ok(res) => Ok(res),
|
||||
Err(e) => Err(anyhow!("{status} | {e:#?}")),
|
||||
Err(e) => Err(anyhow!("{e:#?}").context(status)),
|
||||
}
|
||||
} else {
|
||||
tracing::debug!("response is non-OK");
|
||||
match res.text().await {
|
||||
Ok(res) => Err(
|
||||
deserialize_error(res)
|
||||
.context(format!("request failed with status {status}")),
|
||||
),
|
||||
Err(e) => Err(
|
||||
anyhow!("{e:?}")
|
||||
.context(format!("request failed with status {status}")),
|
||||
),
|
||||
Ok(res) => Err(deserialize_error(res).context(status)),
|
||||
Err(e) => Err(anyhow!("{e:?}").context(status)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "blocking")]
|
||||
fn post<B: Serialize + std::fmt::Debug, R: DeserializeOwned>(
|
||||
&self,
|
||||
endpoint: &str,
|
||||
body: B,
|
||||
) -> anyhow::Result<R> {
|
||||
let req = self
|
||||
.reqwest
|
||||
.post(format!("{}{endpoint}", self.address))
|
||||
.header("x-api-key", &self.key)
|
||||
.header("x-api-secret", &self.secret)
|
||||
.header("content-type", "application/json")
|
||||
.json(&body);
|
||||
let res = req.send().context("failed to reach Komodo API")?;
|
||||
let status = res.status();
|
||||
if status == StatusCode::OK {
|
||||
match res.json() {
|
||||
Ok(res) => Ok(res),
|
||||
Err(e) => Err(anyhow!("{e:#?}").context(status)),
|
||||
}
|
||||
} else {
|
||||
match res.text() {
|
||||
Ok(res) => Err(deserialize_error(res).context(status)),
|
||||
Err(e) => Err(anyhow!("{e:?}").context(status)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use thiserror::Error;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio_tungstenite::{connect_async, tungstenite::Message};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::{info, info_span, warn, Instrument};
|
||||
use tracing::{debug, info, info_span, warn, Instrument};
|
||||
use typeshare::typeshare;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -92,7 +92,7 @@ impl KomodoClient {
|
||||
);
|
||||
|
||||
async {
|
||||
info!("Entering inner (connection) loop | outer uuid {outer_uuid} | master uuid {master_uuid}");
|
||||
debug!("Entering inner (connection) loop | outer uuid {outer_uuid} | master uuid {master_uuid}");
|
||||
let mut retry = 0;
|
||||
loop {
|
||||
// INNER LOOP (SHORT RECONNECT)
|
||||
@@ -112,7 +112,7 @@ impl KomodoClient {
|
||||
);
|
||||
|
||||
async {
|
||||
info!("Connecting to websocket | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}");
|
||||
debug!("Connecting to websocket | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}");
|
||||
|
||||
let mut ws =
|
||||
match connect_async(&address).await.with_context(|| {
|
||||
@@ -131,7 +131,7 @@ impl KomodoClient {
|
||||
}
|
||||
};
|
||||
|
||||
info!("Connected to websocket | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}");
|
||||
debug!("Connected to websocket | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}");
|
||||
|
||||
// ==================
|
||||
// SEND LOGIN MSG
|
||||
@@ -200,7 +200,7 @@ impl KomodoClient {
|
||||
|
||||
let _ = tx.send(UpdateWsMessage::Reconnected);
|
||||
|
||||
info!("logged into websocket | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}");
|
||||
info!("Logged into websocket | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}");
|
||||
|
||||
// If we get to this point (connected / logged in) reset the short retry counter
|
||||
retry = 0;
|
||||
@@ -217,13 +217,13 @@ impl KomodoClient {
|
||||
Ok(Some(Message::Text(msg))) => {
|
||||
match serde_json::from_str::<UpdateListItem>(&msg) {
|
||||
Ok(msg) => {
|
||||
tracing::debug!(
|
||||
debug!(
|
||||
"got recognized message: {msg:?} | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}"
|
||||
);
|
||||
let _ = tx.send(UpdateWsMessage::Update(msg));
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::warn!(
|
||||
warn!(
|
||||
"got unrecognized message: {msg:?} | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}"
|
||||
);
|
||||
let _ = tx.send(UpdateWsMessage::Error(
|
||||
@@ -235,7 +235,7 @@ impl KomodoClient {
|
||||
Ok(Some(Message::Close(_))) => {
|
||||
let _ = tx.send(UpdateWsMessage::Disconnected);
|
||||
let _ = ws.close(None).await;
|
||||
tracing::warn!(
|
||||
warn!(
|
||||
"breaking inner loop | got close message | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}"
|
||||
);
|
||||
break;
|
||||
@@ -246,7 +246,7 @@ impl KomodoClient {
|
||||
));
|
||||
let _ = tx.send(UpdateWsMessage::Disconnected);
|
||||
let _ = ws.close(None).await;
|
||||
tracing::warn!(
|
||||
warn!(
|
||||
"breaking inner loop | got error message | {e:#} | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}"
|
||||
);
|
||||
break;
|
||||
|
||||
@@ -23,15 +23,11 @@ const komodo = KomodoClient("https://demo.komo.do", {
|
||||
},
|
||||
});
|
||||
|
||||
const stacks: Types.StackListItem[] = await komodo.read({
|
||||
type: "ListStacks",
|
||||
params: {},
|
||||
});
|
||||
// Inferred as Types.StackListItem[]
|
||||
const stacks = await komodo.read("ListStacks", {});
|
||||
|
||||
const stack: Types.Stack = await komodo.read({
|
||||
type: "GetStack",
|
||||
params: {
|
||||
stack: stacks[0].name,
|
||||
}
|
||||
// Inferred as Types.Stack
|
||||
const stack = await komodo.read("GetStack", {
|
||||
stack: stacks[0].name,
|
||||
});
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "komodo_client",
|
||||
"version": "1.16.0",
|
||||
"version": "1.16.4",
|
||||
"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 {
|
||||
@@ -5929,6 +5936,23 @@ export interface PauseStack {
|
||||
service?: string;
|
||||
}
|
||||
|
||||
export interface PermissionToml {
|
||||
/**
|
||||
* Id can be:
|
||||
* - resource name. `id = "abcd-build"`
|
||||
* - regex matching resource names. `id = "\^(.+)-build-([0-9]+)$\"`
|
||||
*/
|
||||
target: ResourceTarget;
|
||||
/**
|
||||
* The permission level:
|
||||
* - None
|
||||
* - Read
|
||||
* - Execute
|
||||
* - Write
|
||||
*/
|
||||
level: PermissionLevel;
|
||||
}
|
||||
|
||||
export enum PortTypeEnum {
|
||||
EMPTY = "",
|
||||
TCP = "tcp",
|
||||
@@ -6212,6 +6236,60 @@ export interface RenameUserGroup {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ResourceToml<PartialConfig> {
|
||||
/** The resource name. Required */
|
||||
name: string;
|
||||
/** The resource description. Optional. */
|
||||
description?: string;
|
||||
/** Tag ids or names. Optional */
|
||||
tags?: string[];
|
||||
/**
|
||||
* Optional. Only relevant for deployments / stacks.
|
||||
*
|
||||
* Will ensure deployment / stack is running with the latest configuration.
|
||||
* Deploy actions to achieve this will be included in the sync.
|
||||
* Default is false.
|
||||
*/
|
||||
deploy?: boolean;
|
||||
/**
|
||||
* Optional. Only relevant for deployments / stacks using the 'deploy' sync feature.
|
||||
*
|
||||
* Specify other deployments / stacks by name as dependencies.
|
||||
* The sync will ensure the deployment / stack will only be deployed 'after' its dependencies.
|
||||
*/
|
||||
after?: string[];
|
||||
/** Resource specific configuration. */
|
||||
config?: PartialConfig;
|
||||
}
|
||||
|
||||
export interface UserGroupToml {
|
||||
/** User group name */
|
||||
name: string;
|
||||
/** Users in the group */
|
||||
users?: string[];
|
||||
/** Give the user group elevated permissions on all resources of a certain type */
|
||||
all?: Record<ResourceTarget["type"], PermissionLevel>;
|
||||
/** Permissions given to the group */
|
||||
permissions?: PermissionToml[];
|
||||
}
|
||||
|
||||
/** Specifies resources to sync on Komodo */
|
||||
export interface ResourcesToml {
|
||||
servers?: ResourceToml<_PartialServerConfig>[];
|
||||
deployments?: ResourceToml<_PartialDeploymentConfig>[];
|
||||
stacks?: ResourceToml<_PartialStackConfig>[];
|
||||
builds?: ResourceToml<_PartialBuildConfig>[];
|
||||
repos?: ResourceToml<_PartialRepoConfig>[];
|
||||
procedures?: ResourceToml<_PartialProcedureConfig>[];
|
||||
actions?: ResourceToml<_PartialActionConfig>[];
|
||||
alerters?: ResourceToml<_PartialAlerterConfig>[];
|
||||
builders?: ResourceToml<_PartialBuilderConfig>[];
|
||||
server_templates?: ResourceToml<PartialServerTemplateConfig>[];
|
||||
resource_syncs?: ResourceToml<_PartialResourceSyncConfig>[];
|
||||
user_groups?: UserGroupToml[];
|
||||
variables?: Variable[];
|
||||
}
|
||||
|
||||
/** Restarts all containers on the target server. Response: [Update] */
|
||||
export interface RestartAllContainers {
|
||||
/** Name or id */
|
||||
|
||||
@@ -1,6 +1,50 @@
|
||||
# API
|
||||
# API and Clients
|
||||
|
||||
Komodo Core exposes an http API to read data, write configuration, and execute actions. The API documentation is generated from the code and is [available here](https://docs.rs/komodo_client/latest/komodo_client/api/index.html).
|
||||
Komodo Core exposes an RPC-like HTTP API to read data, write configuration, and execute actions.
|
||||
There are typesafe clients available in
|
||||
[**Rust**](/docs/api#rust-client) and [**Typescript**](/docs/api#typescript-client).
|
||||
|
||||
You can also install the [Komodo CLI](https://crates.io/crates/komodo_cli) to execute actions like RunBuild or DeployStack from the command line.
|
||||
This can be coupled with scripts in Komodo Repos to achieve unlimited automation.
|
||||
The full API documentation is [**available here**](https://docs.rs/komodo_client/latest/komodo_client/api/index.html).
|
||||
|
||||
## Rust Client
|
||||
|
||||
The Rust client is published to crates.io at [komodo_client](https://crates.io/crates/komodo_client).
|
||||
|
||||
```rust
|
||||
let komodo = KomodoClient::new("https://demo.komo.do", "your_key", "your_secret")
|
||||
.with_healthcheck()
|
||||
.await?;
|
||||
|
||||
let stacks = komodo.read(ListStacks::default()).await?;
|
||||
|
||||
let update = komodo
|
||||
.execute(DeployStack {
|
||||
stack: stacks[0].name.clone(),
|
||||
stop_time: None
|
||||
})
|
||||
.await?;
|
||||
```
|
||||
|
||||
## Typescript Client
|
||||
|
||||
The Typescript client is published to NPM at [komodo_client](https://www.npmjs.com/package/komodo_client).
|
||||
|
||||
```ts
|
||||
import { KomodoClient, Types } from "komodo_client";
|
||||
|
||||
const komodo = KomodoClient("https://demo.komo.do", {
|
||||
type: "api-key",
|
||||
params: {
|
||||
api_key: "your_key",
|
||||
secret: "your secret",
|
||||
},
|
||||
});
|
||||
|
||||
// Inferred as Types.StackListItem[]
|
||||
const stacks = await komodo.read("ListStacks", {});
|
||||
|
||||
// Inferred as Types.Update
|
||||
const update = await komodo.execute("DeployStack", {
|
||||
stack: stacks[0].name,
|
||||
});
|
||||
```
|
||||
|
||||
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.
|
||||
@@ -1,29 +1,57 @@
|
||||
# Permissioning Resources
|
||||
# Permissioning
|
||||
|
||||
All Komodo resources (servers, builds, deployment) have independant permission tables to allow for users to have granular access to these resources. By default, users do not see any resources until they are given at least read permissions.
|
||||
|
||||
## Permission Levels
|
||||
|
||||
There are 4 levels of permissions a user can have on a resource:
|
||||
|
||||
1. **None**. This is the lowest permission level, and means the user will not have any access to this resource. They will not see it in the GUI, and it will not show up if the user queries the core API directly. All attempts to view or update the resource will be blocked.
|
||||
|
||||
2. **Read**. This is the first permission level that grants any access. It will enable the user to see the resource in the GUI, read the configuration, and see any logs. Any attempts to update configuration or trigger any action will be blocked.
|
||||
|
||||
3. **Execute**. This level will allow the user to execute actions on the resource, like send a build command or trigger a redeploy. The user will still be blocked from updating configuration on the resource.
|
||||
|
||||
4. **Write**. The user has full access to the resource, they can execute any actions, update the configuration, and delete the resource.
|
||||
Komodo has a granular, layer-based permissioning system to provide non-admin users access only to intended Resources.
|
||||
|
||||
## User Groups
|
||||
|
||||
In addition to assigning permissions to users directly, admins can create User Groups and **assign permissions to them**, as if they were a user.
|
||||
Users can then be **added to multiple User Groups** and they **inherit the group's permissions**.
|
||||
While Komodo can assign permissions to specific users directly, it is recommended to instead **create User Groups and assign permissions to them**, as if they were a user.
|
||||
|
||||
Users can then be **added to multiple User Groups** and they **inherit the group's permissions**, similar to linux permissions.
|
||||
|
||||
For permissioning at scale, users can define [**User Groups in Resource Syncs**](/docs/sync-resources#user-group).
|
||||
|
||||
## Permission Levels
|
||||
|
||||
There are 4 permission levels a user / group can be given on a Resource:
|
||||
|
||||
1. **None**. The user will not have any access to the resource. The user **will not see it in the GUI, and it will not show up if the user queries the Komodo API directly**. All attempts to view or update the resource will be blocked. This is the default for non-admins, unless using `KOMODO_TRANSPARENT_MODE=true`.
|
||||
|
||||
2. **Read**. This is the first permission level that grants any access. It will enable the user to **see the resource in the GUI, read the configuration, and see any logs**. Any attempts to update configuration or trigger any action **will be blocked**. Using `KOMODO_TRANSPARENT_MODE=true` will make this level the base level on all resources, for all users.
|
||||
|
||||
3. **Execute**. This level will allow the user to execute actions on the resource, **like send a build command** or **trigger a redeploy**. The user will still be blocked from updating configuration on the resource.
|
||||
|
||||
4. **Write**. The user has full access to the resource, **they can execute any actions, update the configuration, and delete the resource**.
|
||||
|
||||
## Global permissions
|
||||
|
||||
Users or User Groups can be given a base permission level on all Resources of a particular type, such as Stack.
|
||||
In TOML form, this looks like:
|
||||
|
||||
```toml
|
||||
[[user_group]]
|
||||
name = "groupo"
|
||||
users = ["mbecker20", "karamvirsingh98"]
|
||||
all.Build = "Execute" # <- Group members can run all builds (but not update config),
|
||||
all.Stack = "Read" # <- And see all Stacks / logs (not deploy / update).
|
||||
```
|
||||
|
||||
A user / group can still be given a greater permission level on select resources:
|
||||
|
||||
```toml
|
||||
permissions = [
|
||||
{ target.type = "Stack", target.id = "my-stack", level = "Execute" },
|
||||
# Use regex to match multiple resources, for example give john execute on all of their Stacks
|
||||
{ target.type = "Stack", target.id = "\\^john-(.+)$\\", level = "Execute" },
|
||||
]
|
||||
```
|
||||
|
||||
## Administration
|
||||
|
||||
Users can be given admin priviledges by accessing the Komodo MongoDB and setting ```admin: true``` on the intended user document. These users have unrestricted access to all Komodo resources, like servers, builds, and deployments. Additionally, only these users can update other (non-admin) user's permissions on resources, an action not available to regular users even with **Update** level permissions.
|
||||
Users can be given Admin priviledges by a `Super Admin` (only the first user is given this status, set with `super_admin: true` on a User document in database). Super admins will see the "Make Admin" button when on a User page `/users/${user_id}`.
|
||||
|
||||
Komodo admins are responsible for managing user accounts as well. When a user logs into Komodo for the first time, they will not immediately be granted access. An admin must first **enable** the user, which can be done from the 'manage users' page (found in the user dropdown menu in the topbar). Users can also be **disabled** by an admin at any time, which blocks all their access to the GUI and API.
|
||||
These users have unrestricted access to all Komodo Resources. Additionally, these users can update other (non-admin) user's permissions on resources.
|
||||
|
||||
Komodo admins are responsible for managing user accounts as well. When a user logs into Komodo for the first time, they will not immediately be granted access (this can changed with `KOMODO_ENABLE_NEW_USERS=true`). An admin must first **enable** the user, which can be done from the `Users` tab on `Settings` page. Users can also be **disabled** by an admin at any time, which blocks all their access to the GUI and API.
|
||||
|
||||
Users also have some configurable global permissions, these are:
|
||||
|
||||
|
||||
@@ -12,54 +12,62 @@ All resources which depend on git repos / docker registries are able to use thes
|
||||
|
||||
## Server
|
||||
|
||||
-- Configure the connection to periphery agents.<br></br>
|
||||
-- Set alerting thresholds.<br></br>
|
||||
-- Can be attached to **Deployments**, **Stacks**, **Repos**, and **Builders**.
|
||||
- Configure the connection to periphery agents.
|
||||
- Set alerting thresholds.
|
||||
- Can be attached to by **Deployments**, **Stacks**, **Repos**, and **Builders**.
|
||||
|
||||
## Deployment
|
||||
|
||||
-- Deploy a docker container on the attached Server.<br></br>
|
||||
-- Manage services at the container level, perform orchestration using **Procedures** and **ResourceSyncs**.
|
||||
- Deploy a docker container on the attached Server.
|
||||
- Manage services at the container level, perform orchestration using **Procedures** and **ResourceSyncs**.
|
||||
|
||||
## Stack
|
||||
|
||||
-- Deploy with docker compose.<br></br>
|
||||
-- Provide the compose file in UI, or move the files to a git repo and use a webhook for auto redeploy on push.<br></br>
|
||||
-- Supports composing multiple compose files using `docker compose -f ... -f ...`.<br></br>
|
||||
-- Pass environment variables usable within the compose file. Interpolate in app-wide variables / secrets.
|
||||
- Deploy with docker compose.
|
||||
- Provide the compose file in UI, or move the files to a git repo and use a webhook for auto redeploy on push.
|
||||
- Supports composing multiple compose files using `docker compose -f ... -f ...`.
|
||||
- Pass environment variables usable within the compose file. Interpolate in app-wide variables / secrets.
|
||||
|
||||
## Repo
|
||||
|
||||
-- Put scripts in git repos, and run them on a Server, or using a Builder.<br></br>
|
||||
-- Can build binaries, perform automation, really whatever you can think of.
|
||||
- Put scripts in git repos, and run them on a Server, or using a Builder.
|
||||
- Can build binaries, perform automation, really whatever you can think of.
|
||||
|
||||
## Build
|
||||
|
||||
-- Build application source into docker images, and push them to the configured registry.<br></br>
|
||||
-- The source can be any git repo containing a Dockerfile.
|
||||
- Build application source into docker images, and push them to the configured registry.
|
||||
- The source can be any git repo containing a Dockerfile.
|
||||
|
||||
## Builder
|
||||
|
||||
-- Either points to a connected server, or holds configuration to launch a single-use AWS instance to build the image.<br></br>
|
||||
-- Can be attached to **Builds** and **Repos**.
|
||||
- Either points to a connected server, or holds configuration to launch a single-use AWS instance to build the image.
|
||||
- Can be attached to **Builds** and **Repos**.
|
||||
|
||||
## Procedure
|
||||
|
||||
-- Compose many actions on other resource type, like `RunBuild` or `DeployStack`, and run it on button push (or with a webhook).<br></br>
|
||||
-- Can run one or more actions in parallel "stages", and compose a series of parallel stages to run sequentially.
|
||||
- Compose many actions on other resource type, like `RunBuild` or `DeployStack`, and run it on button push (or with a webhook).
|
||||
- Can run one or more actions in parallel "stages", and compose a series of parallel stages to run sequentially.
|
||||
|
||||
## Action
|
||||
|
||||
- Write scripts calling the Komodo API in Typescript
|
||||
- Use a pre-initialized Komodo client within the script, no api keys necessary.
|
||||
- Type aware in UI editor. Get suggestions and see in depth docs as you type.
|
||||
- The Typescript client is also [published on NPM](https://www.npmjs.com/package/komodo_client).
|
||||
|
||||
## ResourceSync
|
||||
|
||||
-- Orchestrate all your configuration declaratively by defining it in `toml` files, which are checked into a git repo.<br></br>
|
||||
-- Can deploy **Deployments** and **Stacks** if changes are suggested.<br></br>
|
||||
-- Specify deploy ordering with `after` array. (like docker compose `depends_on` but can span across servers.).
|
||||
- Orchestrate all your configuration declaratively by defining it in `toml` files, which are checked into a git repo.
|
||||
- Can deploy **Deployments** and **Stacks** if changes are suggested.
|
||||
- Specify deploy ordering with `after` array. (like docker compose `depends_on` but can span across servers.).
|
||||
|
||||
## Alerter
|
||||
|
||||
-- Route alerts to various endpoints.<br></br>
|
||||
-- Can configure rules on each Alerter, such as resource whitelist, blacklist, or alert type filter.
|
||||
- Route alerts to various endpoints.
|
||||
- Can configure rules on each Alerter, such as resource whitelist, blacklist, or alert type filter.
|
||||
|
||||
## ServerTemplate
|
||||
|
||||
-- Easily expand your cloud network by storing cloud server lauch templates on various providers.<br></br>
|
||||
-- Auto connect the server to Komodo on launch, using `User Data` launch scripts.
|
||||
- Easily expand your cloud network by storing cloud server lauch templates on various providers.
|
||||
- Auto connect the server to Komodo on launch, using `User Data` launch scripts.
|
||||
- Currently supports **AWS EC2** and **Hetzner**
|
||||
|
||||
45
docsite/docs/variables.md
Normal file
45
docsite/docs/variables.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Variables and Secrets
|
||||
|
||||
A variable / secret in Komodo is just a key-value pair.
|
||||
|
||||
```
|
||||
KEY_1 = "value_1"
|
||||
```
|
||||
|
||||
You can interpolate the value into any Environment (and most other user configurable inputs, such as Repo `On Clone` and `On Pull`, or Stack `Extra Args`) using double brackets around the key to trigger interpolation:
|
||||
|
||||
```toml
|
||||
# Before interpolation
|
||||
SOME_ENV_VAR = [[KEY_1]] # <- wrap the key in double brackets '[[]]'
|
||||
|
||||
# After iterpolation:
|
||||
SOME_ENV_VAR = value_1
|
||||
```
|
||||
|
||||
## Defining Variables and Secrets
|
||||
|
||||
- **In the UI**, you can go to `Settings` page, `Variables` tab. Here, you can create some Variables to store in the Komodo database.
|
||||
- There is a "secret" option you can check, this will **prevent the value from exposure in any updates / logs**, as well as prevent access to the value to any **non-admin** Komodo users.
|
||||
- Variables can also be managed in ResourceSyncs (see [example](/docs/sync-resources#deployments)) but should only be done for non-secret variables, to avoid committing sensitive data. You should manage secrets using one of the following options.
|
||||
|
||||
- **Mount a config file to Core**: https://komo.do/docs/setup/advanced#mount-a-config-file
|
||||
- In the Komodo Core config file, you can configure `secrets` using a block like:
|
||||
```toml
|
||||
# in core.config.toml
|
||||
[secrets]
|
||||
KEY_1 = "value_1"
|
||||
KEY_2 = "value_2"
|
||||
```
|
||||
- `KEY_1` and `KEY_2` will be available for interpolation on all your resources, as if they were Variables set up in the UI.
|
||||
- They keys are queryable and show up on the variable page (so you know they are available for use),
|
||||
but **the values are not exposed by API for ANY user**.
|
||||
|
||||
- **Mount a config file to Periphery agent**:
|
||||
|
||||
- In the Komodo Periphery config file, you can also configure `secrets` using the same syntax as the Core config file.
|
||||
- The variable **WILL NOT be available globally to all Komodo resources**, it will only be available to the resources on the associated Server resource on which that single Periphery agent is running.
|
||||
- This effectively distributes your secret locations, can be good or bad depending on your security requirements. It does avoid the need to send the secret over network from Core to Periphery, Periphery based secrets are never exposed to the network.
|
||||
|
||||
- **Use a dedicated secret management tool** such as Hashicorp Vault, alongside Komodo
|
||||
- Ultimately Komodo variable / secret features **may not fill enterprise level secret management requirements**, organizations of this level should use still a dedicated secret management solution. At this point Komodo is not intended as an enterprise level secret management solution.
|
||||
- These solutions do require application level integrations, your applications should only receive credentials to access the secret management API. **Your applications will pull the actual secret values from the dedicated secret management tool, they stay out of Komodo entirely**.
|
||||
@@ -1,15 +1,37 @@
|
||||
# Configuring Webhooks
|
||||
|
||||
Multiple Komodo resources can take advantage of webhooks from your git provider. Komodo supports incoming webhooks using the Github standard, which is also supported by other providers like Gitea.
|
||||
Multiple Komodo resources can take advantage of webhooks from your git provider. Komodo supports incoming webhooks using either the Github or Gitlab webhook authentication type, which is also supported by other providers like Gitea.
|
||||
|
||||
:::note
|
||||
On Gitea, the default "Gitea" webhook type works with the Github standard 👍
|
||||
On Gitea, the default "Gitea" webhook type works with the Github authentication type 👍
|
||||
:::
|
||||
|
||||
## Copy the Resource Payload URL
|
||||
## Copy the Webhook URL
|
||||
|
||||
Find the resource in UI, like a `Build`, `Repo`, or `Stack`.
|
||||
Scroll down to the bottom of Configuration area, and copy the webhook for the action you want.
|
||||
Go to the `Config` section, find "Webhooks", and copy the webhook for the action you want.
|
||||
|
||||
The webhook URL is constructed as follows:
|
||||
|
||||
```shell
|
||||
https://${HOST}/listener/${AUTH_TYPE}/${RESOURCE_TYPE}/${ID_OR_NAME}/${EXECUTION}
|
||||
```
|
||||
- **`HOST`**: Your Komodo endpoint to recieve webhooks.
|
||||
- If your Komodo sits in a private network,
|
||||
you will need a public proxy setup to forward `/listener` requests to Komodo.
|
||||
- **`AUTH_TYPE`**:
|
||||
- options: `github` | `gitlab`
|
||||
- `github`: Validates the signature attached with `X-Hub-Signature-256`. [reference](https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries)
|
||||
- `gitlab`: Checks that the secret attached to `X-Gitlab-Token` is valid. [reference](https://docs.gitlab.com/ee/user/project/integrations/webhooks.html#create-a-webhook)
|
||||
- **`RESOURCE_TYPE`**:
|
||||
- options: `build` | `repo` | `stack` | `sync` | `procedure` | `action`
|
||||
- **`ID_OR_NAME`**:
|
||||
- Reference the specific resource by id or name. If the name may change, it is better to use id.
|
||||
- **`EXECUTION`**:
|
||||
- Which executions are available depends on the `RESOURCE_TYPE`. Builds only have the `/build` action.
|
||||
Repos can select between `/pull`, `/clone`, or `/build`. Stacks have `/deploy` and `/refresh`, and Resource Syncs have `/sync` and `/refresh`.
|
||||
- For **Procedures and Actions**, this will be the **branch to listen to for pushes**, or `__ALL__` to trigger
|
||||
on pushes to any branch.
|
||||
|
||||
## Create the webhook on the Git Provider
|
||||
|
||||
@@ -32,17 +54,4 @@ etc. only cares about a specific branch of the repo.
|
||||
|
||||
Because of this, the webhook will trigger the action **only on pushes to the branch configured on the resource**.
|
||||
|
||||
For example, if I make a build, I may point the build to the `release` branch of a particular repo. If I set up a webhook, and push to the `main` branch, the action will *not trigger*. It will only trigger when the push is to the `release` branch.
|
||||
|
||||
## Procedure webhooks
|
||||
|
||||
Not all actions support webhooks directly, however for those that don't, they can still be triggered via webhook by using a Procedure. Just create a Procedure and configure it to run the action you are looking for, and create a webhook pointing to the Procedure.
|
||||
|
||||
Since Procedures don't specificy a particular branch it should listen for pushes on, this information
|
||||
must be put in the webhook payload url. Procedures use webhook payload urls of the form:
|
||||
|
||||
```
|
||||
<KOMODO_HOST>/listener/github/procedure/<PROCEDURE_ID>/<LISTEN_BRANCH>
|
||||
```
|
||||
|
||||
If the `<LISTEN_BRANCH>` is not provided, it will default to listening on the `main` branch.
|
||||
For example, if I make a build, I may point the build to the `release` branch of a particular repo. If I set up a webhook, and push to the `main` branch, the action will *not trigger*. It will only trigger when the push is to the `release` branch.
|
||||
@@ -58,11 +58,13 @@ const sidebars: SidebarsConfig = {
|
||||
],
|
||||
},
|
||||
"docker-compose",
|
||||
"variables",
|
||||
"permissioning",
|
||||
"sync-resources",
|
||||
"webhooks",
|
||||
"permissioning",
|
||||
"version-upgrades",
|
||||
"api",
|
||||
"development"
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -17,7 +17,8 @@ async fn app() -> anyhow::Result<()> {
|
||||
|
||||
info!("v {}", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
let komodo = KomodoClient::new_from_env().await?;
|
||||
let komodo =
|
||||
KomodoClient::new_from_env()?.with_healthcheck().await?;
|
||||
|
||||
let (mut rx, _) = komodo.subscribe_to_updates(1000, 5)?;
|
||||
|
||||
|
||||
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
|
||||
74
frontend/public/client/types.d.ts
vendored
74
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 */
|
||||
@@ -5521,6 +5528,22 @@ export interface PauseStack {
|
||||
/** Optionally specify a specific service to pause */
|
||||
service?: string;
|
||||
}
|
||||
export interface PermissionToml {
|
||||
/**
|
||||
* Id can be:
|
||||
* - resource name. `id = "abcd-build"`
|
||||
* - regex matching resource names. `id = "\^(.+)-build-([0-9]+)$\"`
|
||||
*/
|
||||
target: ResourceTarget;
|
||||
/**
|
||||
* The permission level:
|
||||
* - None
|
||||
* - Read
|
||||
* - Execute
|
||||
* - Write
|
||||
*/
|
||||
level: PermissionLevel;
|
||||
}
|
||||
export declare enum PortTypeEnum {
|
||||
EMPTY = "",
|
||||
TCP = "tcp",
|
||||
@@ -5775,6 +5798,57 @@ export interface RenameUserGroup {
|
||||
/** The new name for the UserGroup */
|
||||
name: string;
|
||||
}
|
||||
export interface ResourceToml<PartialConfig> {
|
||||
/** The resource name. Required */
|
||||
name: string;
|
||||
/** The resource description. Optional. */
|
||||
description?: string;
|
||||
/** Tag ids or names. Optional */
|
||||
tags?: string[];
|
||||
/**
|
||||
* Optional. Only relevant for deployments / stacks.
|
||||
*
|
||||
* Will ensure deployment / stack is running with the latest configuration.
|
||||
* Deploy actions to achieve this will be included in the sync.
|
||||
* Default is false.
|
||||
*/
|
||||
deploy?: boolean;
|
||||
/**
|
||||
* Optional. Only relevant for deployments / stacks using the 'deploy' sync feature.
|
||||
*
|
||||
* Specify other deployments / stacks by name as dependencies.
|
||||
* The sync will ensure the deployment / stack will only be deployed 'after' its dependencies.
|
||||
*/
|
||||
after?: string[];
|
||||
/** Resource specific configuration. */
|
||||
config?: PartialConfig;
|
||||
}
|
||||
export interface UserGroupToml {
|
||||
/** User group name */
|
||||
name: string;
|
||||
/** Users in the group */
|
||||
users?: string[];
|
||||
/** Give the user group elevated permissions on all resources of a certain type */
|
||||
all?: Record<ResourceTarget["type"], PermissionLevel>;
|
||||
/** Permissions given to the group */
|
||||
permissions?: PermissionToml[];
|
||||
}
|
||||
/** Specifies resources to sync on Komodo */
|
||||
export interface ResourcesToml {
|
||||
servers?: ResourceToml<_PartialServerConfig>[];
|
||||
deployments?: ResourceToml<_PartialDeploymentConfig>[];
|
||||
stacks?: ResourceToml<_PartialStackConfig>[];
|
||||
builds?: ResourceToml<_PartialBuildConfig>[];
|
||||
repos?: ResourceToml<_PartialRepoConfig>[];
|
||||
procedures?: ResourceToml<_PartialProcedureConfig>[];
|
||||
actions?: ResourceToml<_PartialActionConfig>[];
|
||||
alerters?: ResourceToml<_PartialAlerterConfig>[];
|
||||
builders?: ResourceToml<_PartialBuilderConfig>[];
|
||||
server_templates?: ResourceToml<PartialServerTemplateConfig>[];
|
||||
resource_syncs?: ResourceToml<_PartialResourceSyncConfig>[];
|
||||
user_groups?: UserGroupToml[];
|
||||
variables?: Variable[];
|
||||
}
|
||||
/** Restarts all containers on the target server. Response: [Update] */
|
||||
export interface RestartAllContainers {
|
||||
/** Name or id */
|
||||
|
||||
6653
frontend/public/deno.d.ts
vendored
Normal file
6653
frontend/public/deno.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
919
frontend/public/index.d.ts
vendored
919
frontend/public/index.d.ts
vendored
@@ -1,8 +1,923 @@
|
||||
import { KomodoClient, Types as KomodoTypes } from "./client/lib.js";
|
||||
import "./deno.d.ts";
|
||||
|
||||
declare global {
|
||||
// =================
|
||||
// 🔴 Docker Compose
|
||||
// =================
|
||||
|
||||
/**
|
||||
* Docker Compose configuration interface
|
||||
*/
|
||||
export interface DockerCompose {
|
||||
/** Version of the Compose file format */
|
||||
version?: string;
|
||||
/** Defines services within the Docker Compose file */
|
||||
services: Record<string, DockerComposeService>;
|
||||
/** Defines volumes in the Docker Compose file */
|
||||
volumes?: Record<string, DockerComposeVolume>;
|
||||
/** Defines networks in the Docker Compose file */
|
||||
networks?: Record<string, DockerComposeNetwork>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes a service within Docker Compose
|
||||
*/
|
||||
export interface DockerComposeService {
|
||||
/** Docker image to use */
|
||||
image?: string;
|
||||
/** Build configuration for the service */
|
||||
build?: DockerComposeServiceBuild;
|
||||
/** Ports to map, supporting single strings or mappings */
|
||||
ports?: (string | DockerComposeServicePortMapping)[];
|
||||
/** Environment variables to set within the container */
|
||||
environment?: Record<string, string>;
|
||||
/** Volumes to mount */
|
||||
volumes?: (string | DockerComposeServiceVolumeMount)[];
|
||||
/** Networks to attach the service to */
|
||||
networks?: string[];
|
||||
/** Dependencies of the service */
|
||||
depends_on?: string[];
|
||||
/** Command to override the default CMD */
|
||||
command?: string | string[];
|
||||
/** Entrypoint to override the default ENTRYPOINT */
|
||||
entrypoint?: string | string[];
|
||||
/** Container name */
|
||||
container_name?: string;
|
||||
/** Healthcheck configuration for the service */
|
||||
healthcheck?: DockerComposeServiceHealthcheck;
|
||||
/** Logging options for the service */
|
||||
logging?: DockerComposeServiceLogging;
|
||||
/** Deployment settings for the service */
|
||||
deploy?: DockerComposeServiceDeploy;
|
||||
/** Restart policy */
|
||||
restart?: string;
|
||||
/** Security options */
|
||||
security_opt?: string[];
|
||||
/** Ulimits configuration */
|
||||
ulimits?: Record<string, DockerComposeServiceUlimit>;
|
||||
/** Secrets to be used by the service */
|
||||
secrets?: string[];
|
||||
/** Configuration items */
|
||||
configs?: string[];
|
||||
/** Labels to apply to the service */
|
||||
labels?: Record<string, string>;
|
||||
/** Number of CPU units assigned */
|
||||
cpus?: string | number;
|
||||
/** Memory limit */
|
||||
mem_limit?: string;
|
||||
/** CPU shares for container allocation */
|
||||
cpu_shares?: number;
|
||||
/** Extra hosts for the service */
|
||||
extra_hosts?: string[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for Docker build
|
||||
*/
|
||||
export interface DockerComposeServiceBuild {
|
||||
/** Build context path */
|
||||
context: string;
|
||||
/** Dockerfile path within the context */
|
||||
dockerfile?: string;
|
||||
/** Build arguments to pass */
|
||||
args?: Record<string, string>;
|
||||
/** Sources for cache imports */
|
||||
cache_from?: string[];
|
||||
/** Labels for the build */
|
||||
labels?: Record<string, string>;
|
||||
/** Network mode for build process */
|
||||
network?: string;
|
||||
/** Target build stage */
|
||||
target?: string;
|
||||
/** Shared memory size */
|
||||
shm_size?: string;
|
||||
/** Secrets for the build process */
|
||||
secrets?: string[];
|
||||
/** Extra hosts for build process */
|
||||
extra_hosts?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Port mapping configuration
|
||||
*/
|
||||
export interface DockerComposeServicePortMapping {
|
||||
/** Target port inside the container */
|
||||
target: number;
|
||||
/** Published port on the host */
|
||||
published?: number;
|
||||
/** Protocol used for the port (tcp/udp) */
|
||||
protocol?: "tcp" | "udp";
|
||||
/** Mode for port publishing */
|
||||
mode?: "host" | "ingress";
|
||||
}
|
||||
|
||||
/**
|
||||
* Volume mount configuration
|
||||
*/
|
||||
export interface DockerComposeServiceVolumeMount {
|
||||
/** Type of volume mount */
|
||||
type: "volume" | "bind" | "tmpfs";
|
||||
/** Source path or name */
|
||||
source: string;
|
||||
/** Target path within the container */
|
||||
target: string;
|
||||
/** Whether the volume is read-only */
|
||||
read_only?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Healthcheck configuration for a service
|
||||
*/
|
||||
export interface DockerComposeServiceHealthcheck {
|
||||
/** Command to check health */
|
||||
test: string | string[];
|
||||
/** Interval between checks */
|
||||
interval?: string;
|
||||
/** Timeout for each check */
|
||||
timeout?: string;
|
||||
/** Maximum number of retries */
|
||||
retries?: number;
|
||||
/** Initial delay before checks start */
|
||||
start_period?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logging configuration for a service
|
||||
*/
|
||||
export interface DockerComposeServiceLogging {
|
||||
/** Logging driver */
|
||||
driver: string;
|
||||
/** Options for the logging driver */
|
||||
options?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deployment configuration for a service
|
||||
*/
|
||||
export interface DockerComposeServiceDeploy {
|
||||
/** Number of replicas */
|
||||
replicas?: number;
|
||||
/** Update configuration */
|
||||
update_config?: DockerComposeServiceDeploy;
|
||||
/** Restart policy */
|
||||
restart_policy?: DockerComposeServiceDeployRestartPolicy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update configuration during deployment
|
||||
*/
|
||||
export interface DockerComposeServiceDeployUpdateConfig {
|
||||
/** Number of containers updated in parallel */
|
||||
parallelism?: number;
|
||||
/** Delay between updates */
|
||||
delay?: string;
|
||||
/** Action on failure */
|
||||
failure_action?: string;
|
||||
/** Order of updates */
|
||||
order?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart policy configuration
|
||||
*/
|
||||
export interface DockerComposeServiceDeployRestartPolicy {
|
||||
/** Condition for restart */
|
||||
condition: "none" | "on-failure" | "any";
|
||||
/** Delay before restarting */
|
||||
delay?: string;
|
||||
/** Maximum number of restart attempts */
|
||||
max_attempts?: number;
|
||||
/** Time window for restart attempts */
|
||||
window?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ulimit configuration
|
||||
*/
|
||||
export interface DockerComposeServiceUlimit {
|
||||
/** Soft limit */
|
||||
soft: number;
|
||||
/** Hard limit */
|
||||
hard: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Volume configuration in Docker Compose
|
||||
*/
|
||||
export interface DockerComposeVolume {
|
||||
/** Volume driver to use */
|
||||
driver?: string;
|
||||
/** Driver options */
|
||||
driver_opts?: Record<string, string>;
|
||||
/** External volume identifier */
|
||||
external?: boolean | string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Network configuration in Docker Compose
|
||||
*/
|
||||
export interface DockerComposeNetwork {
|
||||
/** Network driver */
|
||||
driver?: string;
|
||||
/** Indicates if network is external */
|
||||
external?: boolean;
|
||||
}
|
||||
|
||||
// =====================
|
||||
// 🔴 YAML De/serializer
|
||||
// =====================
|
||||
|
||||
// https://jsr.io/@std/yaml
|
||||
|
||||
export type YamlSchemaType =
|
||||
| "failsafe"
|
||||
| "json"
|
||||
| "core"
|
||||
| "default"
|
||||
| "extended";
|
||||
|
||||
export type YamlStyleVariant =
|
||||
| "lowercase"
|
||||
| "uppercase"
|
||||
| "camelcase"
|
||||
| "decimal"
|
||||
| "binary"
|
||||
| "octal"
|
||||
| "hexadecimal";
|
||||
|
||||
/** Options for `YAML.stringify` */
|
||||
export type YamlStringifyOptions = {
|
||||
/**
|
||||
* Indentation width to use (in spaces).
|
||||
*
|
||||
* @default {2}
|
||||
*/
|
||||
indent?: number;
|
||||
/**
|
||||
* When true, adds an indentation level to array elements.
|
||||
*
|
||||
* @default {true}
|
||||
*/
|
||||
arrayIndent?: boolean;
|
||||
/**
|
||||
* Do not throw on invalid types (like function in the safe schema) and skip
|
||||
* pairs and single values with such types.
|
||||
*
|
||||
* @default {false}
|
||||
*/
|
||||
skipInvalid?: boolean;
|
||||
/**
|
||||
* Specifies level of nesting, when to switch from block to flow style for
|
||||
* collections. `-1` means block style everywhere.
|
||||
*
|
||||
* @default {-1}
|
||||
*/
|
||||
flowLevel?: number;
|
||||
/** Each tag may have own set of styles. - "tag" => "style" map. */
|
||||
styles?: Record<string, YamlStyleVariant>;
|
||||
/**
|
||||
* Name of the schema to use.
|
||||
*
|
||||
* @default {"default"}
|
||||
*/
|
||||
schema?: YamlSchemaType;
|
||||
/**
|
||||
* If true, sort keys when dumping YAML in ascending, ASCII character order.
|
||||
* If a function, use the function to sort the keys.
|
||||
* If a function is specified, the function must return a negative value
|
||||
* if first argument is less than second argument, zero if they're equal
|
||||
* and a positive value otherwise.
|
||||
*
|
||||
* @default {false}
|
||||
*/
|
||||
sortKeys?: boolean | ((a: string, b: string) => number);
|
||||
/**
|
||||
* Set max line width.
|
||||
*
|
||||
* @default {80}
|
||||
*/
|
||||
lineWidth?: number;
|
||||
/**
|
||||
* If false, don't convert duplicate objects into references.
|
||||
*
|
||||
* @default {true}
|
||||
*/
|
||||
useAnchors?: boolean;
|
||||
/**
|
||||
* If false don't try to be compatible with older yaml versions.
|
||||
* Currently: don't quote "yes", "no" and so on,
|
||||
* as required for YAML 1.1.
|
||||
*
|
||||
* @default {true}
|
||||
*/
|
||||
compatMode?: boolean;
|
||||
/**
|
||||
* If true flow sequences will be condensed, omitting the
|
||||
* space between `key: value` or `a, b`. Eg. `'[a,b]'` or `{a:{b:c}}`.
|
||||
* Can be useful when using yaml for pretty URL query params
|
||||
* as spaces are %-encoded.
|
||||
*
|
||||
* @default {false}
|
||||
*/
|
||||
condenseFlow?: boolean;
|
||||
};
|
||||
|
||||
/** Options for `YAML.parse` */
|
||||
export interface YamlParseOptions {
|
||||
/**
|
||||
* Name of the schema to use.
|
||||
*
|
||||
* @default {"default"}
|
||||
*/
|
||||
schema?: YamlSchemaType;
|
||||
/**
|
||||
* If `true`, duplicate keys will overwrite previous values. Otherwise,
|
||||
* duplicate keys will throw a {@linkcode SyntaxError}.
|
||||
*
|
||||
* @default {false}
|
||||
*/
|
||||
allowDuplicateKeys?: boolean;
|
||||
/**
|
||||
* If defined, a function to call on warning messages taking an
|
||||
* {@linkcode Error} as its only argument.
|
||||
*/
|
||||
onWarning?(error: Error): void;
|
||||
}
|
||||
|
||||
// ===============
|
||||
// 🔴 Cargo TOML 🦀
|
||||
// ===============
|
||||
|
||||
/**
|
||||
* Represents the structure of a Cargo.toml manifest file.
|
||||
*/
|
||||
export interface CargoToml {
|
||||
/**
|
||||
* Information about the main package in the Cargo project.
|
||||
*/
|
||||
package?: CargoTomlPackage;
|
||||
|
||||
/**
|
||||
* Dependencies required by the project, organized into normal, development, and build dependencies.
|
||||
*/
|
||||
dependencies?: CargoTomlDependencies;
|
||||
|
||||
/**
|
||||
* Development dependencies required by the project.
|
||||
*/
|
||||
devDependencies?: CargoTomlDependencies;
|
||||
|
||||
/**
|
||||
* Build dependencies required by the project.
|
||||
*/
|
||||
buildDependencies?: CargoTomlDependencies;
|
||||
|
||||
/**
|
||||
* Features available in the package, each as an array of dependency names or other features.
|
||||
*/
|
||||
features?: Record<string, string[]>;
|
||||
|
||||
/**
|
||||
* Build profiles available in the package, allowing for profile-specific configurations.
|
||||
*/
|
||||
profile?: CargoTomlProfiles;
|
||||
|
||||
/**
|
||||
* Path to the custom build script for the package, if applicable.
|
||||
*/
|
||||
build?: string;
|
||||
|
||||
/**
|
||||
* Workspace configuration for multi-package Cargo projects.
|
||||
*/
|
||||
workspace?: CargoTomlWorkspace;
|
||||
|
||||
/**
|
||||
* Additional metadata ignored by Cargo but potentially used by other tools.
|
||||
*/
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata for the main package in the Cargo project.
|
||||
*/
|
||||
export interface CargoTomlPackage {
|
||||
/**
|
||||
* The name of the package, used by Cargo and for crate publishing.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The version of the package, following Semantic Versioning.
|
||||
*/
|
||||
version: string;
|
||||
|
||||
/**
|
||||
* List of author names or emails.
|
||||
*/
|
||||
authors?: string[];
|
||||
|
||||
/**
|
||||
* The Rust edition for this package.
|
||||
*/
|
||||
edition?: "2015" | "2018" | "2021";
|
||||
|
||||
/**
|
||||
* Short description of the package.
|
||||
*/
|
||||
description?: string;
|
||||
|
||||
/**
|
||||
* The license for the package, specified as a SPDX identifier.
|
||||
*/
|
||||
license?: string;
|
||||
|
||||
/**
|
||||
* Path to a custom license file for the package.
|
||||
*/
|
||||
licenseFile?: string;
|
||||
|
||||
/**
|
||||
* URL to the package documentation.
|
||||
*/
|
||||
documentation?: string;
|
||||
|
||||
/**
|
||||
* URL to the package homepage.
|
||||
*/
|
||||
homepage?: string;
|
||||
|
||||
/**
|
||||
* URL to the package repository.
|
||||
*/
|
||||
repository?: string;
|
||||
|
||||
/**
|
||||
* Path to the README file for the package.
|
||||
*/
|
||||
readme?: string;
|
||||
|
||||
/**
|
||||
* List of keywords for the package, used for search optimization.
|
||||
*/
|
||||
keywords?: string[];
|
||||
|
||||
/**
|
||||
* List of categories that the package belongs to.
|
||||
*/
|
||||
categories?: string[];
|
||||
|
||||
/**
|
||||
* Workspace that this package belongs to, if any.
|
||||
*/
|
||||
workspace?: string;
|
||||
|
||||
/**
|
||||
* Path to a build script for the package.
|
||||
*/
|
||||
build?: string;
|
||||
|
||||
/**
|
||||
* Name of a native library to link with, if applicable.
|
||||
*/
|
||||
links?: string;
|
||||
|
||||
/**
|
||||
* List of paths to exclude from the package.
|
||||
*/
|
||||
exclude?: string[];
|
||||
|
||||
/**
|
||||
* List of paths to include in the package.
|
||||
*/
|
||||
include?: string[];
|
||||
|
||||
/**
|
||||
* Indicates whether the package should be published to crates.io.
|
||||
*/
|
||||
publish?: boolean;
|
||||
|
||||
/**
|
||||
* Arbitrary metadata that is ignored by Cargo but can be used by other tools.
|
||||
*/
|
||||
metadata?: Record<string, any>;
|
||||
|
||||
/**
|
||||
* Auto-enable binaries for the package.
|
||||
*/
|
||||
autobins?: boolean;
|
||||
|
||||
/**
|
||||
* Auto-enable examples for the package.
|
||||
*/
|
||||
autoexamples?: boolean;
|
||||
|
||||
/**
|
||||
* Auto-enable tests for the package.
|
||||
*/
|
||||
autotests?: boolean;
|
||||
|
||||
/**
|
||||
* Auto-enable benchmarks for the package.
|
||||
*/
|
||||
autobenches?: boolean;
|
||||
|
||||
/**
|
||||
* Specifies the version of dependency resolution to use.
|
||||
*/
|
||||
resolver?: "1" | "2";
|
||||
}
|
||||
|
||||
/**
|
||||
* A map of dependencies in the Cargo manifest, with each dependency represented by its name.
|
||||
*/
|
||||
export type CargoTomlDependencies = Record<string, CargoTomlDependency>;
|
||||
|
||||
/**
|
||||
* Information about a specific dependency in the Cargo manifest.
|
||||
*/
|
||||
export type CargoTomlDependency =
|
||||
| string
|
||||
| {
|
||||
/**
|
||||
* Version requirement for the dependency.
|
||||
*/
|
||||
version?: string;
|
||||
|
||||
/**
|
||||
* Path to a local dependency.
|
||||
*/
|
||||
path?: string;
|
||||
|
||||
/**
|
||||
* Name of the registry to use for this dependency.
|
||||
*/
|
||||
registry?: string;
|
||||
|
||||
/**
|
||||
* URL to a Git repository for this dependency.
|
||||
*/
|
||||
git?: string;
|
||||
|
||||
/**
|
||||
* Branch to use for a Git dependency.
|
||||
*/
|
||||
branch?: string;
|
||||
|
||||
/**
|
||||
* Tag to use for a Git dependency.
|
||||
*/
|
||||
tag?: string;
|
||||
|
||||
/**
|
||||
* Specific revision to use for a Git dependency.
|
||||
*/
|
||||
rev?: string;
|
||||
|
||||
/**
|
||||
* Marks this dependency as optional.
|
||||
*/
|
||||
optional?: boolean;
|
||||
|
||||
/**
|
||||
* Enables default features for this dependency.
|
||||
*/
|
||||
defaultFeatures?: boolean;
|
||||
|
||||
/**
|
||||
* List of features to enable for this dependency.
|
||||
*/
|
||||
features?: string[];
|
||||
|
||||
/**
|
||||
* Renames the dependency package name.
|
||||
*/
|
||||
package?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines available profiles for building the package.
|
||||
*/
|
||||
export interface CargoTomlProfiles {
|
||||
/**
|
||||
* Development profile configuration.
|
||||
*/
|
||||
dev?: CargoTomlProfile;
|
||||
|
||||
/**
|
||||
* Release profile configuration.
|
||||
*/
|
||||
release?: CargoTomlProfile;
|
||||
|
||||
/**
|
||||
* Test profile configuration.
|
||||
*/
|
||||
test?: CargoTomlProfile;
|
||||
|
||||
/**
|
||||
* Benchmark profile configuration.
|
||||
*/
|
||||
bench?: CargoTomlProfile;
|
||||
|
||||
/**
|
||||
* Documentation profile configuration.
|
||||
*/
|
||||
doc?: CargoTomlProfile;
|
||||
|
||||
/**
|
||||
* Additional custom profiles.
|
||||
*/
|
||||
[profileName: string]: CargoTomlProfile | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for an individual build profile.
|
||||
*/
|
||||
export interface CargoTomlProfile {
|
||||
/**
|
||||
* Profile that this profile inherits from.
|
||||
*/
|
||||
inherits?: string;
|
||||
|
||||
/**
|
||||
* Optimization level for the profile.
|
||||
*/
|
||||
optLevel?: "0" | "1" | "2" | "3" | "s" | "z";
|
||||
|
||||
/**
|
||||
* Enables debug information, either as a boolean or a level.
|
||||
*/
|
||||
debug?: boolean | number;
|
||||
|
||||
/**
|
||||
* Controls how debug information is split.
|
||||
*/
|
||||
splitDebugInfo?: "unpacked" | "packed" | "off";
|
||||
|
||||
/**
|
||||
* Enables or disables debug assertions.
|
||||
*/
|
||||
debugAssertions?: boolean;
|
||||
|
||||
/**
|
||||
* Enables or disables overflow checks.
|
||||
*/
|
||||
overflowChecks?: boolean;
|
||||
|
||||
/**
|
||||
* Enables or disables unit testing for the profile.
|
||||
*/
|
||||
test?: boolean;
|
||||
|
||||
/**
|
||||
* Link-time optimization settings for the profile.
|
||||
*/
|
||||
lto?: boolean | "thin" | "fat";
|
||||
|
||||
/**
|
||||
* Panic strategy for the profile.
|
||||
*/
|
||||
panic?: "unwind" | "abort";
|
||||
|
||||
/**
|
||||
* Enables or disables incremental compilation.
|
||||
*/
|
||||
incremental?: boolean;
|
||||
|
||||
/**
|
||||
* Number of code generation units for parallelism.
|
||||
*/
|
||||
codegenUnits?: number;
|
||||
|
||||
/**
|
||||
* Enables or disables the use of runtime paths.
|
||||
*/
|
||||
rpath?: boolean;
|
||||
|
||||
/**
|
||||
* Specifies stripping options for the binary.
|
||||
*/
|
||||
strip?: boolean | "debuginfo" | "symbols";
|
||||
|
||||
/**
|
||||
* Additional custom profile fields.
|
||||
*/
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines workspace-specific settings for a Cargo project.
|
||||
*/
|
||||
export interface CargoTomlWorkspace {
|
||||
/**
|
||||
* Members of the workspace.
|
||||
*/
|
||||
members?: string[];
|
||||
|
||||
/**
|
||||
* Paths to exclude from the workspace.
|
||||
*/
|
||||
exclude?: string[];
|
||||
|
||||
/**
|
||||
* Members to include by default when building the workspace.
|
||||
*/
|
||||
defaultMembers?: string[];
|
||||
|
||||
/**
|
||||
* Common Information about the packages in the Cargo workspace.
|
||||
*/
|
||||
package?: CargoTomlPackage;
|
||||
|
||||
/**
|
||||
* Additional custom workspace fields.
|
||||
*/
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// =====================
|
||||
// 🔴 TOML De/serializer
|
||||
// =====================
|
||||
|
||||
// https://jsr.io/@std/toml
|
||||
|
||||
export interface TomlStringifyOptions {
|
||||
/**
|
||||
* Define if the keys should be aligned or not.
|
||||
*
|
||||
* @default {false}
|
||||
*/
|
||||
keyAlignment?: boolean;
|
||||
}
|
||||
|
||||
/** Pre initialized Komodo client */
|
||||
var komodo: ReturnType<typeof KomodoClient>;
|
||||
/** YAML parsing utilities */
|
||||
var YAML: {
|
||||
/**
|
||||
* Converts a JavaScript object or value to a YAML document string.
|
||||
*
|
||||
* @example Usage
|
||||
* ```ts
|
||||
* const data = { id: 1, name: "Alice" };
|
||||
*
|
||||
* const yaml = YAML.stringify(data);
|
||||
*
|
||||
* assertEquals(yaml, "id: 1\nname: Alice\n");
|
||||
* ```
|
||||
*
|
||||
* @throws {TypeError} If `data` contains invalid types.
|
||||
* @param data The data to serialize.
|
||||
* @param options The options for serialization.
|
||||
* @returns A YAML string.
|
||||
*/
|
||||
stringify: (data: unknown, options?: YamlStringifyOptions) => string;
|
||||
/**
|
||||
* Parse and return a YAML string as a parsed YAML document object.
|
||||
*
|
||||
* Note: This does not support functions. Untrusted data is safe to parse.
|
||||
*
|
||||
* @example Usage
|
||||
* ```ts
|
||||
* const data = YAML.parse(`
|
||||
* id: 1
|
||||
* name: Alice
|
||||
* `);
|
||||
*
|
||||
* assertEquals(data, { id: 1, name: "Alice" });
|
||||
* ```
|
||||
*
|
||||
* @throws {SyntaxError} Throws error on invalid YAML.
|
||||
* @param content YAML string to parse.
|
||||
* @param options Parsing options.
|
||||
* @returns Parsed document.
|
||||
*/
|
||||
parse: (content: string, options?: YamlParseOptions) => unknown;
|
||||
/**
|
||||
* Same as `YAML.parse`, but understands multi-document YAML sources, and
|
||||
* returns multiple parsed YAML document objects.
|
||||
*
|
||||
* @example Usage
|
||||
* ```ts
|
||||
* const data = YAML.parseAll(`
|
||||
* ---
|
||||
* id: 1
|
||||
* name: Alice
|
||||
* ---
|
||||
* id: 2
|
||||
* name: Bob
|
||||
* ---
|
||||
* id: 3
|
||||
* name: Eve
|
||||
* `);
|
||||
*
|
||||
* assertEquals(data, [ { id: 1, name: "Alice" }, { id: 2, name: "Bob" }, { id: 3, name: "Eve" }]);
|
||||
* ```
|
||||
*
|
||||
* @param content YAML string to parse.
|
||||
* @param options Parsing options.
|
||||
* @returns Array of parsed documents.
|
||||
*/
|
||||
parseAll: (content: string, options?: YamlParseOptions) => unknown;
|
||||
/**
|
||||
* Parse and return a YAML string as a Docker Compose file.
|
||||
*
|
||||
* @example Usage
|
||||
* ```ts
|
||||
* const stack = await komodo.read("GetStack", { stack: "test-stack" });
|
||||
* const contents = stack?.config?.file_contents;
|
||||
*
|
||||
* const parsed: DockerCompose = YAML.parseDockerCompose(contents)
|
||||
* ```
|
||||
*
|
||||
* @throws {SyntaxError} Throws error on invalid YAML.
|
||||
* @param content Docker compose file string.
|
||||
* @param options Parsing options.
|
||||
* @returns Parsed document.
|
||||
*/
|
||||
parseDockerCompose: (
|
||||
content: string,
|
||||
options?: YamlParseOptions
|
||||
) => DockerCompose;
|
||||
};
|
||||
/** TOML parsing utilities */
|
||||
var TOML: {
|
||||
/**
|
||||
* Converts an object to a [TOML string](https://toml.io).
|
||||
*
|
||||
* @example Usage
|
||||
* ```ts
|
||||
* const obj = {
|
||||
* title: "TOML Example",
|
||||
* owner: {
|
||||
* name: "Bob",
|
||||
* bio: "Bob is a cool guy",
|
||||
* }
|
||||
* };
|
||||
*
|
||||
* const tomlString = TOML.stringify(obj);
|
||||
*
|
||||
* assertEquals(tomlString, `title = "TOML Example"\n\n[owner]\nname = "Bob"\nbio = "Bob is a cool guy"\n`);
|
||||
* ```
|
||||
* @param obj Source object
|
||||
* @param options Options for stringifying.
|
||||
* @returns TOML string
|
||||
*/
|
||||
stringify: (
|
||||
obj: Record<string, unknown>,
|
||||
options?: TomlStringifyOptions
|
||||
) => string;
|
||||
/**
|
||||
* Parses a [TOML string](https://toml.io) into an object.
|
||||
*
|
||||
* @example Usage
|
||||
* ```ts
|
||||
* const tomlString = `title = "TOML Example"
|
||||
* [owner]
|
||||
* name = "Alice"
|
||||
* bio = "Alice is a programmer."`;
|
||||
*
|
||||
* const obj = TOML.parse(tomlString);
|
||||
*
|
||||
* assertEquals(obj, { title: "TOML Example", owner: { name: "Alice", bio: "Alice is a programmer." } });
|
||||
* ```
|
||||
* @param tomlString TOML string to be parsed.
|
||||
* @returns The parsed JS object.
|
||||
*/
|
||||
parse: (tomlString: string) => Record<string, unknown>;
|
||||
/**
|
||||
* Parses Komodo resource.toml contents to an object
|
||||
* for easier handling.
|
||||
*
|
||||
* @example Usage
|
||||
* ```ts
|
||||
* const sync = await komodo.read("GetResourceSync", { sync: "test-sync" })
|
||||
* const contents = sync?.config?.file_contents;
|
||||
*
|
||||
* const resources: Types.ResourcesToml = TOML.parseResourceToml(contents);
|
||||
* ```
|
||||
*
|
||||
* @param resourceToml The resource file contents.
|
||||
* @returns Komodo resource.toml contents as JSON
|
||||
*/
|
||||
parseResourceToml: (resourceToml: string) => Types.ResourcesToml;
|
||||
/**
|
||||
* Parses Cargo.toml contents to an object
|
||||
* for easier handling.
|
||||
*
|
||||
* @example Usage
|
||||
* ```ts
|
||||
* const contents = Deno.readTextFile("/path/to/Cargo.toml");
|
||||
* const cargoToml: CargoToml = TOML.parseCargoToml(contents);
|
||||
* ```
|
||||
*
|
||||
* @param cargoToml The Cargo.toml contents.
|
||||
* @returns Cargo.toml contents as JSON
|
||||
*/
|
||||
parseCargoToml: (cargoToml: string) => CargoToml;
|
||||
};
|
||||
/** All Komodo Types */
|
||||
export import Types = KomodoTypes;
|
||||
}
|
||||
|
||||
export {}
|
||||
@@ -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) => (
|
||||
@@ -222,7 +199,7 @@ export const Config = <T,>({
|
||||
key={section}
|
||||
className="relative pb-12 border-b last:pb-0 last:border-b-0 "
|
||||
>
|
||||
<div className="xl:hidden sticky top-16 h-24 flex items-center justify-between bg-background z-10">
|
||||
<div className="xl:hidden sticky top-16 h-16 flex items-center justify-between bg-background z-10">
|
||||
{section && <p className="uppercase text-2xl">{section}</p>}
|
||||
<Select
|
||||
onValueChange={(value) => (window.location.hash = value)}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -15,16 +15,22 @@ export async function init_monaco() {
|
||||
)
|
||||
)
|
||||
);
|
||||
await Promise.all(promises);
|
||||
|
||||
fetch(`/index.d.ts`)
|
||||
.then((res) => res.text())
|
||||
.then((dts) =>
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(
|
||||
dts,
|
||||
`file:///index.d.ts`
|
||||
promises.push(
|
||||
Promise.all(
|
||||
["index.d.ts", "deno.d.ts"].map((file) =>
|
||||
fetch(`/${file}`)
|
||||
.then((res) => res.text())
|
||||
.then((dts) =>
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(
|
||||
dts,
|
||||
`file:///${file}`
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
)
|
||||
);
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
|
||||
module: monaco.languages.typescript.ModuleKind.ESNext,
|
||||
|
||||
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