Compare commits

..
23 Commits
Author SHA1 Message Date
mbecker20 38f3448790 add Procedures and Actions page 2024-10-28 00:57:42 -04:00
Maxwell BeckerandGitHub ec88a6fa5a 1.16.5 (#159)
* repo pull lock

* implement BatchRunAction

* other batch methods
2024-10-27 20:56:56 -07:00
mbecker20 3820cd0ca2 make Build Organization configurable with custom value 2024-10-27 14:42:06 -04:00
mbecker20 419aa87bbb update resource toml examples to latest standard 2024-10-26 16:22:12 -04:00
Maxwell BeckerandGitHub 7a9ad42203 1.16.4 (#151)
* rust client improvements and docs

* sync rust client

* version 1.16.4

* UI support YAML / TOML utils, typed Deno namespace

* add ResourcesToml to typeshare

* add YAML and TOML convenience

* make the types available globally

* preload container with @std/yaml and @std/toml, clean up genned files

* add deno setup to alpine dockerfile
2024-10-26 12:15:34 -07:00
mbecker20 3f1cfa9064 update docs. Add Variables / Secrets docs 2024-10-25 00:02:12 -04:00
Maxwell BeckerandGitHub d05c81864e 1.16.3 (#150)
* refactor listener api implementation for Gitlab integration

* version 1.16.3

* builder delete id link cleanup

* refactor and add "__ALL__" branch to avoid branch filtering

* frontend config the webhook url

* action webhook config

* clean up webhook url copy

* add __ALL__ branch switch for Actions / Procedures
2024-10-24 16:03:00 -07:00
mbecker20 f1a09f34ab tweak dev docs and runfile 2024-10-22 17:04:49 -04:00
mbecker20 23c6e6306d fix usage of runnables-cli in dev docs 2024-10-22 16:36:25 -04:00
mbecker20 800da90561 tweaks to dev docs 2024-10-22 15:25:33 -04:00
mbecker20 b24bf6ed89 fix docsite broken links 2024-10-22 15:17:10 -04:00
Matt FoxxandGitHub d66a781a13 docs: Add development docs (#136)
* docs: Add development POC

* docs: Flesh out full build/run steps

* feat: Add mergeable compose file to expose port and internal periphery url

* feat: Add .devcontainer and VSCode Tasks for developing Komodo

* Make cargo cache persistent in devcontainer

* Add deno to devcontainer

* Update tasks to include TS client copy to frontend before run

* Recommend extensions for used dependencies in vscode workspace

All extensions recommended are included in the devcontainer. This makes it easier for users not using devcontainer to get lang support.

* Update local `run` sequence for development docs
2024-10-22 12:09:26 -07:00
Maxwell BeckerandGitHub f9b2994d44 1.16.2 (#145)
* Env vars written using same quotes (single vs double) as the user passes

* fmt

* trim start matches '-'

* ts client version
2024-10-22 11:41:17 -07:00
mbecker20 c0d6d96b64 get username works for service users 2024-10-22 03:36:20 -04:00
mbecker20 34496b948a bump ts client to 1.16.1 2024-10-22 02:58:42 -04:00
mbecker20 90c6adf923 fix periphery installer force file recreation command 2024-10-22 02:55:39 -04:00
mbecker20 3b72dc65cc remove "Overviews" label from sidebar 2024-10-22 02:27:22 -04:00
mbecker20 05f38d02be bump version to 1.16.1 2024-10-22 02:21:16 -04:00
Maxwell BeckerandGitHub ea5506c202 1.16.1 (#143)
* ensure sync state cache is refreshed on sync create / copy

* clean up resources post_create

* show sidebar if element length > 1

* update `run_komodo_command` command

* rename all resources

* refresh repo cache after clone / pull

* improve rename repo log
2024-10-21 23:19:40 -07:00
mbecker20 64b0a5c9d2 delete unrelated caddy compose 2024-10-21 00:30:54 -04:00
mbecker20 93cc6a3a6e Add running Action to dashboard "Active" 2024-10-21 00:21:17 -04:00
mbecker20 7ae69cf33b ignore top level return linting 2024-10-20 05:10:28 -04:00
mbecker20 404e00cc64 move action info inline 2024-10-20 04:43:29 -04:00
174 changed files with 14188 additions and 2323 deletions
+33
View 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
View 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
View File
@@ -0,0 +1,3 @@
#!/bin/sh
cargo install typeshare-cli
+8
View File
@@ -0,0 +1,8 @@
{
"recommendations": [
"rust-lang.rust-analyzer",
"tamasfe.even-better-toml",
"vadimcn.vscode-lldb",
"denoland.vscode-deno"
]
}
+179
View 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": []
},
]
}
Generated
+49 -38
View File
@@ -41,7 +41,7 @@ dependencies = [
[[package]]
name = "alerter"
version = "1.16.0"
version = "1.16.5"
dependencies = [
"anyhow",
"axum",
@@ -120,9 +120,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.90"
version = "1.0.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37bf3594c4c988a53154954629820791dde498571819ae4ca50ca811e060cc95"
checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8"
[[package]]
name = "arc-swap"
@@ -201,9 +201,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "aws-config"
version = "1.5.8"
version = "1.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7198e6f03240fdceba36656d8be440297b6b82270325908c7381f37d826a74f6"
checksum = "2d6448cfb224dd6a9b9ac734f58622dd0d4751f3589f3b777345745f46b2eb14"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -268,9 +268,9 @@ dependencies = [
[[package]]
name = "aws-sdk-ec2"
version = "1.79.0"
version = "1.83.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95559916ae9d6ef69b104546098a4b4c57082db5d11571917916e9a69234c6ea"
checksum = "59ef9cdd731373735434b79a33fd1049525d78674c49b1e278d56544e388fe01"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -292,9 +292,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sso"
version = "1.46.0"
version = "1.47.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dc2faec3205d496c7e57eff685dd944203df7ce16a4116d0281c44021788a7b"
checksum = "a8776850becacbd3a82a4737a9375ddb5c6832a51379f24443a98e61513f852c"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -314,9 +314,9 @@ dependencies = [
[[package]]
name = "aws-sdk-ssooidc"
version = "1.47.0"
version = "1.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c93c241f52bc5e0476e259c953234dab7e2a35ee207ee202e86c0095ec4951dc"
checksum = "0007b5b8004547133319b6c4e87193eee2a0bcb3e4c18c75d09febe9dab7b383"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -336,9 +336,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sts"
version = "1.46.0"
version = "1.47.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b259429be94a3459fa1b00c5684faee118d74f9577cc50aebadc36e507c63b5f"
checksum = "9fffaa356e7f1c725908b75136d53207fa714e348f365671df14e95a60530ad3"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -432,9 +432,9 @@ dependencies = [
[[package]]
name = "aws-smithy-runtime"
version = "1.7.2"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a065c0fe6fdbdf9f11817eb68582b2ab4aff9e9c39e986ae48f7ec576c6322db"
checksum = "be28bd063fa91fd871d131fc8b68d7cd4c5fa0869bea68daca50dcb1cbd76be2"
dependencies = [
"aws-smithy-async",
"aws-smithy-http",
@@ -476,9 +476,9 @@ dependencies = [
[[package]]
name = "aws-smithy-types"
version = "1.2.7"
version = "1.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147100a7bea70fa20ef224a6bad700358305f5dc0f84649c53769761395b355b"
checksum = "07c9cdc179e6afbf5d391ab08c85eac817b51c87e1892a5edb5f7bbdc64314b4"
dependencies = [
"base64-simd",
"bytes",
@@ -943,7 +943,7 @@ dependencies = [
[[package]]
name = "command"
version = "1.16.0"
version = "1.16.5"
dependencies = [
"komodo_client",
"run_command",
@@ -1355,7 +1355,7 @@ dependencies = [
[[package]]
name = "environment_file"
version = "1.16.0"
version = "1.16.5"
dependencies = [
"thiserror",
]
@@ -1439,7 +1439,7 @@ dependencies = [
[[package]]
name = "formatting"
version = "1.16.0"
version = "1.16.5"
dependencies = [
"serror",
]
@@ -1571,7 +1571,7 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "git"
version = "1.16.0"
version = "1.16.5"
dependencies = [
"anyhow",
"command",
@@ -2191,7 +2191,7 @@ dependencies = [
[[package]]
name = "komodo_cli"
version = "1.16.0"
version = "1.16.5"
dependencies = [
"anyhow",
"clap",
@@ -2207,7 +2207,7 @@ dependencies = [
[[package]]
name = "komodo_client"
version = "1.16.0"
version = "1.16.5"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2238,7 +2238,7 @@ dependencies = [
[[package]]
name = "komodo_core"
version = "1.16.0"
version = "1.16.5"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2292,11 +2292,12 @@ dependencies = [
"typeshare",
"urlencoding",
"uuid",
"wildcard",
]
[[package]]
name = "komodo_periphery"
version = "1.16.0"
version = "1.16.5"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2383,7 +2384,7 @@ dependencies = [
[[package]]
name = "logger"
version = "1.16.0"
version = "1.16.5"
dependencies = [
"anyhow",
"komodo_client",
@@ -3089,7 +3090,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "periphery_client"
version = "1.16.0"
version = "1.16.5"
dependencies = [
"anyhow",
"komodo_client",
@@ -3307,9 +3308,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.11.0"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
@@ -3391,6 +3392,7 @@ dependencies = [
"base64 0.22.1",
"bytes",
"encoding_rs",
"futures-channel",
"futures-core",
"futures-util",
"h2 0.4.6",
@@ -3992,9 +3994,9 @@ dependencies = [
[[package]]
name = "serror"
version = "0.4.6"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8f432d878d404110352cfbaa031d8a6878a166cb7f50e00ab87d0508f8f68a0"
checksum = "715a997753611604c722411afbe11f83a89e00e39323dc9016db96a86cc04fc8"
dependencies = [
"anyhow",
"axum",
@@ -4326,18 +4328,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.64"
version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.64"
version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602"
dependencies = [
"proc-macro2",
"quote",
@@ -4790,9 +4792,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "typeshare"
version = "1.0.3"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04f17399b76c2e743d58eac0635d7686e9c00f48cd4776f00695d9882a7d3187"
checksum = "19be0f411120091e76e13e5a0186d8e2bcc3e7e244afdb70152197f1a8486ceb"
dependencies = [
"chrono",
"serde",
@@ -4863,7 +4865,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "update_logger"
version = "1.16.0"
version = "1.16.5"
dependencies = [
"anyhow",
"komodo_client",
@@ -5072,6 +5074,15 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311"
[[package]]
name = "wildcard"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36241ad0795516b55e3b60e55c7f979d4f324e4aaea4c70d56b548b9164ee4d2"
dependencies = [
"thiserror",
]
[[package]]
name = "winapi"
version = "0.3.9"
+9 -8
View File
@@ -9,7 +9,7 @@ members = [
]
[workspace.package]
version = "1.16.0"
version = "1.16.5"
edition = "2021"
authors = ["mbecker20 <becker.maxh@gmail.com>"]
license = "GPL-3.0-or-later"
@@ -32,7 +32,7 @@ git = { path = "lib/git" }
# MOGH
run_command = { version = "0.0.6", features = ["async_tokio"] }
serror = { version = "0.4.6", default-features = false }
serror = { version = "0.4.7", default-features = false }
slack = { version = "0.2.0", package = "slack_client_rs" }
derive_default_builder = "0.1.8"
derive_empty_traits = "0.1.0"
@@ -69,8 +69,8 @@ serde_yaml = "0.9.34"
toml = "0.8.19"
# ERROR
anyhow = "1.0.90"
thiserror = "1.0.64"
anyhow = "1.0.91"
thiserror = "1.0.65"
# LOGGING
opentelemetry_sdk = { version = "0.25.0", features = ["rt-tokio"] }
@@ -104,14 +104,15 @@ bollard = "0.17.1"
sysinfo = "0.32.0"
# CLOUD
aws-config = "1.5.8"
aws-sdk-ec2 = "1.77.0"
aws-config = "1.5.9"
aws-sdk-ec2 = "1.83.0"
# MISC
derive_builder = "0.20.2"
typeshare = "1.0.3"
typeshare = "1.0.4"
octorust = "0.7.0"
dashmap = "6.1.0"
wildcard = "0.2.0"
colored = "2.1.0"
regex = "1.11.0"
regex = "1.11.1"
bson = "2.13.0"
+276 -140
View File
@@ -1,13 +1,21 @@
use std::time::Duration;
use colored::Colorize;
use komodo_client::api::execute::Execution;
use komodo_client::{
api::execute::{BatchExecutionResponse, Execution},
entities::update::Update,
};
use crate::{
helpers::wait_for_enter,
state::{cli_args, komodo_client},
};
pub enum ExecutionResult {
Single(Update),
Batch(BatchExecutionResponse),
}
pub async fn run(execution: Execution) -> anyhow::Result<()> {
if matches!(execution, Execution::None(_)) {
println!("Got 'none' execution. Doing nothing...");
@@ -24,18 +32,30 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
Execution::RunAction(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchRunAction(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RunProcedure(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchRunProcedure(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RunBuild(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchRunBuild(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::CancelBuild(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::Deploy(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchDeploy(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StartDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
@@ -54,15 +74,27 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
Execution::DestroyDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchDestroyDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::CloneRepo(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchCloneRepo(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PullRepo(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchPullRepo(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BuildRepo(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchBuildRepo(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::CancelRepoBuild(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
@@ -138,9 +170,15 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
Execution::DeployStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchDeployStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::DeployStackIfChanged(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchDeployStackIfChanged(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StartStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
@@ -159,6 +197,9 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
Execution::DestroyStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchDestroyStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::Sleep(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
@@ -171,144 +212,234 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
info!("Running Execution...");
let res = match execution {
Execution::RunAction(request) => {
komodo_client().execute(request).await
}
Execution::RunProcedure(request) => {
komodo_client().execute(request).await
}
Execution::RunBuild(request) => {
komodo_client().execute(request).await
}
Execution::CancelBuild(request) => {
komodo_client().execute(request).await
}
Execution::Deploy(request) => {
komodo_client().execute(request).await
}
Execution::StartDeployment(request) => {
komodo_client().execute(request).await
}
Execution::RestartDeployment(request) => {
komodo_client().execute(request).await
}
Execution::PauseDeployment(request) => {
komodo_client().execute(request).await
}
Execution::UnpauseDeployment(request) => {
komodo_client().execute(request).await
}
Execution::StopDeployment(request) => {
komodo_client().execute(request).await
}
Execution::DestroyDeployment(request) => {
komodo_client().execute(request).await
}
Execution::CloneRepo(request) => {
komodo_client().execute(request).await
}
Execution::PullRepo(request) => {
komodo_client().execute(request).await
}
Execution::BuildRepo(request) => {
komodo_client().execute(request).await
}
Execution::CancelRepoBuild(request) => {
komodo_client().execute(request).await
}
Execution::StartContainer(request) => {
komodo_client().execute(request).await
}
Execution::RestartContainer(request) => {
komodo_client().execute(request).await
}
Execution::PauseContainer(request) => {
komodo_client().execute(request).await
}
Execution::UnpauseContainer(request) => {
komodo_client().execute(request).await
}
Execution::StopContainer(request) => {
komodo_client().execute(request).await
}
Execution::DestroyContainer(request) => {
komodo_client().execute(request).await
}
Execution::StartAllContainers(request) => {
komodo_client().execute(request).await
}
Execution::RestartAllContainers(request) => {
komodo_client().execute(request).await
}
Execution::PauseAllContainers(request) => {
komodo_client().execute(request).await
}
Execution::UnpauseAllContainers(request) => {
komodo_client().execute(request).await
}
Execution::StopAllContainers(request) => {
komodo_client().execute(request).await
}
Execution::PruneContainers(request) => {
komodo_client().execute(request).await
}
Execution::DeleteNetwork(request) => {
komodo_client().execute(request).await
}
Execution::PruneNetworks(request) => {
komodo_client().execute(request).await
}
Execution::DeleteImage(request) => {
komodo_client().execute(request).await
}
Execution::PruneImages(request) => {
komodo_client().execute(request).await
}
Execution::DeleteVolume(request) => {
komodo_client().execute(request).await
}
Execution::PruneVolumes(request) => {
komodo_client().execute(request).await
}
Execution::PruneDockerBuilders(request) => {
komodo_client().execute(request).await
}
Execution::PruneBuildx(request) => {
komodo_client().execute(request).await
}
Execution::PruneSystem(request) => {
komodo_client().execute(request).await
}
Execution::RunSync(request) => {
komodo_client().execute(request).await
}
Execution::CommitSync(request) => {
komodo_client().write(request).await
}
Execution::DeployStack(request) => {
komodo_client().execute(request).await
}
Execution::DeployStackIfChanged(request) => {
komodo_client().execute(request).await
}
Execution::StartStack(request) => {
komodo_client().execute(request).await
}
Execution::RestartStack(request) => {
komodo_client().execute(request).await
}
Execution::PauseStack(request) => {
komodo_client().execute(request).await
}
Execution::UnpauseStack(request) => {
komodo_client().execute(request).await
}
Execution::StopStack(request) => {
komodo_client().execute(request).await
}
Execution::DestroyStack(request) => {
komodo_client().execute(request).await
}
Execution::RunAction(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::BatchRunAction(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
Execution::RunProcedure(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::BatchRunProcedure(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
Execution::RunBuild(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::BatchRunBuild(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
Execution::CancelBuild(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::Deploy(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::BatchDeploy(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
Execution::StartDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::RestartDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::PauseDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::UnpauseDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::StopDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::DestroyDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::BatchDestroyDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
Execution::CloneRepo(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::BatchCloneRepo(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
Execution::PullRepo(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::BatchPullRepo(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
Execution::BuildRepo(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::BatchBuildRepo(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
Execution::CancelRepoBuild(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::StartContainer(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::RestartContainer(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::PauseContainer(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::UnpauseContainer(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::StopContainer(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::DestroyContainer(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::StartAllContainers(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::RestartAllContainers(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::PauseAllContainers(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::UnpauseAllContainers(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::StopAllContainers(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::PruneContainers(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::DeleteNetwork(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::PruneNetworks(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::DeleteImage(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::PruneImages(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::DeleteVolume(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::PruneVolumes(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::PruneDockerBuilders(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::PruneBuildx(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::PruneSystem(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::RunSync(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::CommitSync(request) => komodo_client()
.write(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::DeployStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::BatchDeployStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
Execution::DeployStackIfChanged(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::BatchDeployStackIfChanged(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
Execution::StartStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::RestartStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::PauseStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::UnpauseStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::StopStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::DestroyStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::BatchDestroyStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
Execution::Sleep(request) => {
let duration =
Duration::from_millis(request.duration_ms as u64);
@@ -320,7 +451,12 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
};
match res {
Ok(update) => println!("\n{}: {update:#?}", "SUCCESS".green()),
Ok(ExecutionResult::Single(update)) => {
println!("\n{}: {update:#?}", "SUCCESS".green())
}
Ok(ExecutionResult::Batch(update)) => {
println!("\n{}: {update:#?}", "SUCCESS".green())
}
Err(e) => println!("{}\n\n{e:#?}", "ERROR".red()),
}
+4 -2
View File
@@ -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")
})
}
+2 -1
View File
@@ -48,13 +48,14 @@ serde_json.workspace = true
serde_yaml.workspace = true
typeshare.workspace = true
octorust.workspace = true
wildcard.workspace = true
dashmap.workspace = true
tracing.workspace = true
reqwest.workspace = true
futures.workspace = true
nom_pem.workspace = true
anyhow.workspace = true
dotenvy.workspace = true
anyhow.workspace = true
bcrypt.workspace = true
base64.workspace = true
tokio.workspace = true
+6
View File
@@ -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
+6
View File
@@ -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
+129 -12
View File
@@ -1,10 +1,15 @@
use std::collections::HashSet;
use std::{
collections::HashSet,
path::{Path, PathBuf},
str::FromStr,
sync::OnceLock,
};
use anyhow::Context;
use command::run_komodo_command;
use komodo_client::{
api::{
execute::RunAction,
execute::{BatchExecutionResponse, BatchRunAction, RunAction},
user::{CreateApiKey, CreateApiKeyResponse, DeleteApiKey},
},
entities::{
@@ -20,6 +25,7 @@ use resolver_api::Resolve;
use tokio::fs;
use crate::{
api::execute::ExecuteRequest,
config::core_config,
helpers::{
interpolate::{
@@ -34,7 +40,26 @@ use crate::{
state::{action_states, db_client, State},
};
impl super::BatchExecute for BatchRunAction {
type Resource = Action;
fn single_request(action: String) -> ExecuteRequest {
ExecuteRequest::RunAction(RunAction { action })
}
}
impl Resolve<BatchRunAction, (User, Update)> for State {
#[instrument(name = "BatchRunAction", skip(self, user), fields(user_id = user.id))]
async fn resolve(
&self,
BatchRunAction { pattern }: BatchRunAction,
(user, _): (User, Update),
) -> anyhow::Result<BatchExecutionResponse> {
super::batch_execute::<BatchRunAction>(&pattern, &user).await
}
}
impl Resolve<RunAction, (User, Update)> for State {
#[instrument(name = "RunAction", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
RunAction { action }: RunAction,
@@ -81,23 +106,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 +131,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 +204,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 +240,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
}
})
}
+22 -1
View File
@@ -4,7 +4,10 @@ use anyhow::{anyhow, Context};
use formatting::format_serror;
use futures::future::join_all;
use komodo_client::{
api::execute::{CancelBuild, Deploy, RunBuild},
api::execute::{
BatchExecutionResponse, BatchRunBuild, CancelBuild, Deploy,
RunBuild,
},
entities::{
alert::{Alert, AlertData, SeverityLevel},
all_logs_success,
@@ -51,6 +54,24 @@ use crate::{
use super::ExecuteRequest;
impl super::BatchExecute for BatchRunBuild {
type Resource = Build;
fn single_request(build: String) -> ExecuteRequest {
ExecuteRequest::RunBuild(RunBuild { build })
}
}
impl Resolve<BatchRunBuild, (User, Update)> for State {
#[instrument(name = "BatchRunBuild", skip(self, user), fields(user_id = user.id))]
async fn resolve(
&self,
BatchRunBuild { pattern }: BatchRunBuild,
(user, _): (User, Update),
) -> anyhow::Result<BatchExecutionResponse> {
super::batch_execute::<BatchRunBuild>(&pattern, &user).await
}
}
impl Resolve<RunBuild, (User, Update)> for State {
#[instrument(name = "RunBuild", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
+24
View File
@@ -37,6 +37,30 @@ use crate::{
state::{action_states, State},
};
use super::ExecuteRequest;
impl super::BatchExecute for BatchDeploy {
type Resource = Deployment;
fn single_request(deployment: String) -> ExecuteRequest {
ExecuteRequest::Deploy(Deploy {
deployment,
stop_signal: None,
stop_time: None,
})
}
}
impl Resolve<BatchDeploy, (User, Update)> for State {
#[instrument(name = "BatchDeploy", skip(self, user), fields(user_id = user.id))]
async fn resolve(
&self,
BatchDeploy { pattern }: BatchDeploy,
(user, _): (User, Update),
) -> anyhow::Result<BatchExecutionResponse> {
super::batch_execute::<BatchDeploy>(&pattern, &user).await
}
}
async fn setup_deployment_execution(
deployment: &str,
user: &User,
+82 -2
View File
@@ -2,13 +2,16 @@ use std::time::Instant;
use anyhow::{anyhow, Context};
use axum::{middleware, routing::post, Extension, Router};
use axum_extra::{headers::ContentType, TypedHeader};
use derive_variants::{EnumVariants, ExtractVariant};
use formatting::format_serror;
use futures::future::join_all;
use komodo_client::{
api::execute::*,
entities::{
update::{Log, Update},
user::User,
Operation,
},
};
use mungos::by_id::find_one_by_id;
@@ -21,6 +24,7 @@ use uuid::Uuid;
use crate::{
auth::auth_request,
helpers::update::{init_execution_update, update_update},
resource::{list_full_for_user_using_pattern, KomodoResource},
state::{db_client, State},
};
@@ -68,6 +72,7 @@ pub enum ExecuteRequest {
// ==== DEPLOYMENT ====
Deploy(Deploy),
BatchDeploy(BatchDeploy),
StartDeployment(StartDeployment),
RestartDeployment(RestartDeployment),
PauseDeployment(PauseDeployment),
@@ -77,29 +82,38 @@ pub enum ExecuteRequest {
// ==== STACK ====
DeployStack(DeployStack),
BatchDeployStack(BatchDeployStack),
DeployStackIfChanged(DeployStackIfChanged),
BatchDeployStackIfChanged(BatchDeployStackIfChanged),
StartStack(StartStack),
RestartStack(RestartStack),
StopStack(StopStack),
PauseStack(PauseStack),
UnpauseStack(UnpauseStack),
DestroyStack(DestroyStack),
BatchDestroyStack(BatchDestroyStack),
// ==== BUILD ====
RunBuild(RunBuild),
BatchRunBuild(BatchRunBuild),
CancelBuild(CancelBuild),
// ==== REPO ====
CloneRepo(CloneRepo),
BatchCloneRepo(BatchCloneRepo),
PullRepo(PullRepo),
BatchPullRepo(BatchPullRepo),
BuildRepo(BuildRepo),
BatchBuildRepo(BatchBuildRepo),
CancelRepoBuild(CancelRepoBuild),
// ==== PROCEDURE ====
RunProcedure(RunProcedure),
BatchRunProcedure(BatchRunProcedure),
// ==== ACTION ====
RunAction(RunAction),
BatchRunAction(BatchRunAction),
// ==== SERVER TEMPLATE ====
LaunchServer(LaunchServer),
@@ -117,7 +131,25 @@ pub fn router() -> Router {
async fn handler(
Extension(user): Extension<User>,
Json(request): Json<ExecuteRequest>,
) -> serror::Result<Json<Update>> {
) -> serror::Result<(TypedHeader<ContentType>, String)> {
let res = match inner_handler(request, user).await? {
ExecutionResult::Single(update) => serde_json::to_string(&update)
.context("Failed to serialize Update")?,
ExecutionResult::Batch(res) => res,
};
Ok((TypedHeader(ContentType::json()), res))
}
enum ExecutionResult {
Single(Update),
/// The batch contents will be pre serialized here
Batch(String),
}
async fn inner_handler(
request: ExecuteRequest,
user: User,
) -> anyhow::Result<ExecutionResult> {
let req_id = Uuid::new_v4();
// need to validate no cancel is active before any update is created.
@@ -125,6 +157,17 @@ async fn handler(
let update = init_execution_update(&request, &user).await?;
// This will be the case for the Batch exections,
// they don't have their own updates.
// The batch calls also call "inner_handler" themselves,
// and in their case will spawn tasks, so that isn't necessary
// here either.
if update.operation == Operation::None {
return Ok(ExecutionResult::Batch(
task(req_id, request, user, update).await?,
));
}
let handle =
tokio::spawn(task(req_id, request, user, update.clone()));
@@ -160,7 +203,7 @@ async fn handler(
}
});
Ok(Json(update))
Ok(ExecutionResult::Single(update))
}
#[instrument(
@@ -200,3 +243,40 @@ async fn task(
res
}
trait BatchExecute {
type Resource: KomodoResource;
fn single_request(name: String) -> ExecuteRequest;
}
async fn batch_execute<E: BatchExecute>(
pattern: &str,
user: &User,
) -> anyhow::Result<BatchExecutionResponse> {
let resources = list_full_for_user_using_pattern::<E::Resource>(
&pattern,
Default::default(),
&user,
&[],
)
.await?;
let futures = resources.into_iter().map(|resource| {
let user = user.clone();
async move {
inner_handler(E::single_request(resource.name.clone()), user)
.await
.map(|r| {
let ExecutionResult::Single(update) = r else {
unreachable!()
};
update
})
.map_err(|e| BatchExecutionResponseItemErr {
name: resource.name,
error: e.into(),
})
.into()
}
});
Ok(join_all(futures).await)
}
+23 -1
View File
@@ -2,7 +2,9 @@ use std::pin::Pin;
use formatting::{bold, colored, format_serror, muted, Color};
use komodo_client::{
api::execute::RunProcedure,
api::execute::{
BatchExecutionResponse, BatchRunProcedure, RunProcedure,
},
entities::{
permission::PermissionLevel, procedure::Procedure,
update::Update, user::User,
@@ -18,6 +20,26 @@ use crate::{
state::{action_states, db_client, State},
};
use super::ExecuteRequest;
impl super::BatchExecute for BatchRunProcedure {
type Resource = Procedure;
fn single_request(procedure: String) -> ExecuteRequest {
ExecuteRequest::RunProcedure(RunProcedure { procedure })
}
}
impl Resolve<BatchRunProcedure, (User, Update)> for State {
#[instrument(name = "BatchRunProcedure", skip(self, user), fields(user_id = user.id))]
async fn resolve(
&self,
BatchRunProcedure { pattern }: BatchRunProcedure,
(user, _): (User, Update),
) -> anyhow::Result<BatchExecutionResponse> {
super::batch_execute::<BatchRunProcedure>(&pattern, &user).await
}
}
impl Resolve<RunProcedure, (User, Update)> for State {
#[instrument(name = "RunProcedure", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
+77 -1
View File
@@ -3,7 +3,7 @@ use std::{collections::HashSet, future::IntoFuture, time::Duration};
use anyhow::{anyhow, Context};
use formatting::format_serror;
use komodo_client::{
api::execute::*,
api::{execute::*, write::RefreshRepoCache},
entities::{
alert::{Alert, AlertData, SeverityLevel},
builder::{Builder, BuilderConfig},
@@ -47,6 +47,24 @@ use crate::{
use super::ExecuteRequest;
impl super::BatchExecute for BatchCloneRepo {
type Resource = Repo;
fn single_request(repo: String) -> ExecuteRequest {
ExecuteRequest::CloneRepo(CloneRepo { repo })
}
}
impl Resolve<BatchCloneRepo, (User, Update)> for State {
#[instrument(name = "BatchCloneRepo", skip(self, user), fields(user_id = user.id))]
async fn resolve(
&self,
BatchCloneRepo { pattern }: BatchCloneRepo,
(user, _): (User, Update),
) -> anyhow::Result<BatchExecutionResponse> {
super::batch_execute::<BatchCloneRepo>(&pattern, &user).await
}
}
impl Resolve<CloneRepo, (User, Update)> for State {
#[instrument(name = "CloneRepo", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
@@ -123,10 +141,39 @@ impl Resolve<CloneRepo, (User, Update)> for State {
update_last_pulled_time(&repo.name).await;
}
if let Err(e) = State
.resolve(RefreshRepoCache { repo: repo.id }, user)
.await
.context("Failed to refresh repo cache")
{
update.push_error_log(
"Refresh Repo cache",
format_serror(&e.into()),
);
};
handle_server_update_return(update).await
}
}
impl super::BatchExecute for BatchPullRepo {
type Resource = Repo;
fn single_request(repo: String) -> ExecuteRequest {
ExecuteRequest::CloneRepo(CloneRepo { repo })
}
}
impl Resolve<BatchPullRepo, (User, Update)> for State {
#[instrument(name = "BatchPullRepo", skip(self, user), fields(user_id = user.id))]
async fn resolve(
&self,
BatchPullRepo { pattern }: BatchPullRepo,
(user, _): (User, Update),
) -> anyhow::Result<BatchExecutionResponse> {
super::batch_execute::<BatchPullRepo>(&pattern, &user).await
}
}
impl Resolve<PullRepo, (User, Update)> for State {
#[instrument(name = "PullRepo", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
@@ -207,6 +254,17 @@ impl Resolve<PullRepo, (User, Update)> for State {
update_last_pulled_time(&repo.name).await;
}
if let Err(e) = State
.resolve(RefreshRepoCache { repo: repo.id }, user)
.await
.context("Failed to refresh repo cache")
{
update.push_error_log(
"Refresh Repo cache",
format_serror(&e.into()),
);
};
handle_server_update_return(update).await
}
}
@@ -249,6 +307,24 @@ async fn update_last_pulled_time(repo_name: &str) {
}
}
impl super::BatchExecute for BatchBuildRepo {
type Resource = Repo;
fn single_request(repo: String) -> ExecuteRequest {
ExecuteRequest::CloneRepo(CloneRepo { repo })
}
}
impl Resolve<BatchBuildRepo, (User, Update)> for State {
#[instrument(name = "BatchBuildRepo", skip(self, user), fields(user_id = user.id))]
async fn resolve(
&self,
BatchBuildRepo { pattern }: BatchBuildRepo,
(user, _): (User, Update),
) -> anyhow::Result<BatchExecutionResponse> {
super::batch_execute::<BatchBuildRepo>(&pattern, &user).await
}
}
impl Resolve<BuildRepo, (User, Update)> for State {
#[instrument(name = "BuildRepo", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
+67
View File
@@ -36,6 +36,29 @@ use crate::{
state::{action_states, db_client, State},
};
use super::ExecuteRequest;
impl super::BatchExecute for BatchDeployStack {
type Resource = Stack;
fn single_request(stack: String) -> ExecuteRequest {
ExecuteRequest::DeployStack(DeployStack {
stack,
stop_time: None,
})
}
}
impl Resolve<BatchDeployStack, (User, Update)> for State {
#[instrument(name = "BatchDeployStack", skip(self, user), fields(user_id = user.id))]
async fn resolve(
&self,
BatchDeployStack { pattern }: BatchDeployStack,
(user, _): (User, Update),
) -> anyhow::Result<BatchExecutionResponse> {
super::batch_execute::<BatchDeployStack>(&pattern, &user).await
}
}
impl Resolve<DeployStack, (User, Update)> for State {
#[instrument(name = "DeployStack", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
@@ -246,6 +269,28 @@ impl Resolve<DeployStack, (User, Update)> for State {
}
}
impl super::BatchExecute for BatchDeployStackIfChanged {
type Resource = Stack;
fn single_request(stack: String) -> ExecuteRequest {
ExecuteRequest::DeployStackIfChanged(DeployStackIfChanged {
stack,
stop_time: None,
})
}
}
impl Resolve<BatchDeployStackIfChanged, (User, Update)> for State {
#[instrument(name = "BatchDeployStackIfChanged", skip(self, user), fields(user_id = user.id))]
async fn resolve(
&self,
BatchDeployStackIfChanged { pattern }: BatchDeployStackIfChanged,
(user, _): (User, Update),
) -> anyhow::Result<BatchExecutionResponse> {
super::batch_execute::<BatchDeployStackIfChanged>(&pattern, &user)
.await
}
}
impl Resolve<DeployStackIfChanged, (User, Update)> for State {
async fn resolve(
&self,
@@ -418,6 +463,28 @@ impl Resolve<StopStack, (User, Update)> for State {
}
}
impl super::BatchExecute for BatchDestroyStack {
type Resource = Stack;
fn single_request(stack: String) -> ExecuteRequest {
ExecuteRequest::DestroyStack(DestroyStack {
stack,
remove_orphans: false,
stop_time: None,
})
}
}
impl Resolve<BatchDestroyStack, (User, Update)> for State {
#[instrument(name = "BatchDestroyStack", skip(self, user), fields(user_id = user.id))]
async fn resolve(
&self,
BatchDestroyStack { pattern }: BatchDestroyStack,
(user, _): (User, Update),
) -> anyhow::Result<BatchExecutionResponse> {
super::batch_execute::<BatchDestroyStack>(&pattern, &user).await
}
}
impl Resolve<DestroyStack, (User, Update)> for State {
#[instrument(name = "DestroyStack", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
+10 -9
View File
@@ -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)
+10 -9
View File
@@ -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)
+8 -1
View File
@@ -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")?
+13 -1
View File
@@ -1,7 +1,8 @@
use komodo_client::{
api::write::*,
entities::{
action::Action, permission::PermissionLevel, user::User,
action::Action, permission::PermissionLevel, update::Update,
user::User,
},
};
use resolver_api::Resolve;
@@ -47,6 +48,17 @@ impl Resolve<UpdateAction, User> for State {
}
}
impl Resolve<RenameAction, User> for State {
#[instrument(name = "RenameAction", skip(self, user))]
async fn resolve(
&self,
RenameAction { id, name }: RenameAction,
user: User,
) -> anyhow::Result<Update> {
resource::rename::<Action>(&id, &name, &user).await
}
}
impl Resolve<DeleteAction, User> for State {
#[instrument(name = "DeleteAction", skip(self, user))]
async fn resolve(
+14 -4
View File
@@ -1,9 +1,8 @@
use komodo_client::{
api::write::{
CopyAlerter, CreateAlerter, DeleteAlerter, UpdateAlerter,
},
api::write::*,
entities::{
alerter::Alerter, permission::PermissionLevel, user::User,
alerter::Alerter, permission::PermissionLevel, update::Update,
user::User,
},
};
use resolver_api::Resolve;
@@ -59,3 +58,14 @@ impl Resolve<UpdateAlerter, User> for State {
resource::update::<Alerter>(&id, config, &user).await
}
}
impl Resolve<RenameAlerter, User> for State {
#[instrument(name = "RenameAlerter", skip(self, user))]
async fn resolve(
&self,
RenameAlerter { id, name }: RenameAlerter,
user: User,
) -> anyhow::Result<Update> {
resource::rename::<Alerter>(&id, &name, &user).await
}
}
+12
View File
@@ -6,6 +6,7 @@ use komodo_client::{
build::{Build, BuildInfo, PartialBuildConfig},
config::core::CoreConfig,
permission::PermissionLevel,
update::Update,
user::User,
CloneArgs, NoData,
},
@@ -77,6 +78,17 @@ impl Resolve<UpdateBuild, User> for State {
}
}
impl Resolve<RenameBuild, User> for State {
#[instrument(name = "RenameBuild", skip(self, user))]
async fn resolve(
&self,
RenameBuild { id, name }: RenameBuild,
user: User,
) -> anyhow::Result<Update> {
resource::rename::<Build>(&id, &name, &user).await
}
}
impl Resolve<RefreshBuildCache, User> for State {
#[instrument(
name = "RefreshBuildCache",
+13 -1
View File
@@ -1,7 +1,8 @@
use komodo_client::{
api::write::*,
entities::{
builder::Builder, permission::PermissionLevel, user::User,
builder::Builder, permission::PermissionLevel, update::Update,
user::User,
},
};
use resolver_api::Resolve;
@@ -57,3 +58,14 @@ impl Resolve<UpdateBuilder, User> for State {
resource::update::<Builder>(&id, config, &user).await
}
}
impl Resolve<RenameBuilder, User> for State {
#[instrument(name = "RenameBuilder", skip(self, user))]
async fn resolve(
&self,
RenameBuilder { id, name }: RenameBuilder,
user: User,
) -> anyhow::Result<Update> {
resource::rename::<Builder>(&id, &name, &user).await
}
}
+6 -7
View File
@@ -108,7 +108,7 @@ impl Resolve<RenameDeployment, User> for State {
if container_state == DeploymentState::Unknown {
return Err(anyhow!(
"cannot rename deployment when container status is unknown"
"Cannot rename Deployment when container status is unknown"
));
}
@@ -124,7 +124,7 @@ impl Resolve<RenameDeployment, User> for State {
None,
)
.await
.context("failed to update deployment name on db")?;
.context("Failed to update Deployment name on db")?;
if container_state != DeploymentState::NotDeployed {
let server =
@@ -135,20 +135,19 @@ impl Resolve<RenameDeployment, User> for State {
new_name: name.clone(),
})
.await
.context("failed to rename container on server")?;
.context("Failed to rename container on server")?;
update.logs.push(log);
}
update.push_simple_log(
"rename deployment",
"Rename Deployment",
format!(
"renamed deployment from {} to {}",
"Renamed Deployment from {} to {}",
deployment.name, name
),
);
update.finalize();
add_update(update.clone()).await?;
update.id = add_update(update.clone()).await?;
Ok(update)
}
+8
View File
@@ -89,6 +89,7 @@ pub enum WriteRequest {
CopyBuild(CopyBuild),
DeleteBuild(DeleteBuild),
UpdateBuild(UpdateBuild),
RenameBuild(RenameBuild),
RefreshBuildCache(RefreshBuildCache),
CreateBuildWebhook(CreateBuildWebhook),
DeleteBuildWebhook(DeleteBuildWebhook),
@@ -98,18 +99,21 @@ pub enum WriteRequest {
CopyBuilder(CopyBuilder),
DeleteBuilder(DeleteBuilder),
UpdateBuilder(UpdateBuilder),
RenameBuilder(RenameBuilder),
// ==== SERVER TEMPLATE ====
CreateServerTemplate(CreateServerTemplate),
CopyServerTemplate(CopyServerTemplate),
DeleteServerTemplate(DeleteServerTemplate),
UpdateServerTemplate(UpdateServerTemplate),
RenameServerTemplate(RenameServerTemplate),
// ==== REPO ====
CreateRepo(CreateRepo),
CopyRepo(CopyRepo),
DeleteRepo(DeleteRepo),
UpdateRepo(UpdateRepo),
RenameRepo(RenameRepo),
RefreshRepoCache(RefreshRepoCache),
CreateRepoWebhook(CreateRepoWebhook),
DeleteRepoWebhook(DeleteRepoWebhook),
@@ -119,24 +123,28 @@ pub enum WriteRequest {
CopyAlerter(CopyAlerter),
DeleteAlerter(DeleteAlerter),
UpdateAlerter(UpdateAlerter),
RenameAlerter(RenameAlerter),
// ==== PROCEDURE ====
CreateProcedure(CreateProcedure),
CopyProcedure(CopyProcedure),
DeleteProcedure(DeleteProcedure),
UpdateProcedure(UpdateProcedure),
RenameProcedure(RenameProcedure),
// ==== ACTION ====
CreateAction(CreateAction),
CopyAction(CopyAction),
DeleteAction(DeleteAction),
UpdateAction(UpdateAction),
RenameAction(RenameAction),
// ==== SYNC ====
CreateResourceSync(CreateResourceSync),
CopyResourceSync(CopyResourceSync),
DeleteResourceSync(DeleteResourceSync),
UpdateResourceSync(UpdateResourceSync),
RenameResourceSync(RenameResourceSync),
WriteSyncFileContents(WriteSyncFileContents),
CommitSync(CommitSync),
RefreshResourceSyncPending(RefreshResourceSyncPending),
+13 -1
View File
@@ -1,7 +1,8 @@
use komodo_client::{
api::write::*,
entities::{
permission::PermissionLevel, procedure::Procedure, user::User,
permission::PermissionLevel, procedure::Procedure,
update::Update, user::User,
},
};
use resolver_api::Resolve;
@@ -48,6 +49,17 @@ impl Resolve<UpdateProcedure, User> for State {
}
}
impl Resolve<RenameProcedure, User> for State {
#[instrument(name = "RenameProcedure", skip(self, user))]
async fn resolve(
&self,
RenameProcedure { id, name }: RenameProcedure,
user: User,
) -> anyhow::Result<Update> {
resource::rename::<Procedure>(&id, &name, &user).await
}
}
impl Resolve<DeleteProcedure, User> for State {
#[instrument(name = "DeleteProcedure", skip(self, user))]
async fn resolve(
+88 -4
View File
@@ -1,27 +1,36 @@
use anyhow::{anyhow, Context};
use formatting::format_serror;
use git::GitRes;
use komodo_client::{
api::write::*,
entities::{
config::core::CoreConfig,
komodo_timestamp,
permission::PermissionLevel,
repo::{PartialRepoConfig, Repo, RepoInfo},
server::Server,
to_komodo_name,
update::{Log, Update},
user::User,
CloneArgs, NoData,
CloneArgs, NoData, Operation,
},
};
use mongo_indexed::doc;
use mungos::mongodb::bson::to_document;
use mungos::{by_id::update_one_by_id, mongodb::bson::to_document};
use octorust::types::{
ReposCreateWebhookRequest, ReposCreateWebhookRequestConfig,
};
use periphery_client::api;
use resolver_api::Resolve;
use crate::{
config::core_config,
helpers::git_token,
helpers::{
git_token, periphery_client,
update::{add_update, make_update},
},
resource,
state::{db_client, github_client, State},
state::{action_states, db_client, github_client, State},
};
impl Resolve<CreateRepo, User> for State {
@@ -75,6 +84,81 @@ impl Resolve<UpdateRepo, User> for State {
}
}
impl Resolve<RenameRepo, User> for State {
#[instrument(name = "RenameRepo", skip(self, user))]
async fn resolve(
&self,
RenameRepo { id, name }: RenameRepo,
user: User,
) -> anyhow::Result<Update> {
let repo = resource::get_check_permissions::<Repo>(
&id,
&user,
PermissionLevel::Write,
)
.await?;
if repo.config.server_id.is_empty()
|| !repo.config.path.is_empty()
{
return resource::rename::<Repo>(&repo.id, &name, &user).await;
}
// get the action state for the repo (or insert default).
let action_state =
action_states().repo.get_or_insert_default(&repo.id).await;
// Will check to ensure repo not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let _action_guard =
action_state.update(|state| state.renaming = true)?;
let name = to_komodo_name(&name);
let mut update = make_update(&repo, Operation::RenameRepo, &user);
update_one_by_id(
&db_client().repos,
&repo.id,
mungos::update::Update::Set(
doc! { "name": &name, "updated_at": komodo_timestamp() },
),
None,
)
.await
.context("Failed to update Repo name on db")?;
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),
new_name: name.clone(),
})
.await
.context("Failed to rename Repo directory on Server")
{
Ok(log) => log,
Err(e) => Log::error(
"Rename Repo directory failure",
format_serror(&e.into()),
),
};
update.logs.push(log);
update.push_simple_log(
"Rename Repo",
format!("Renamed Repo from {} to {}", repo.name, name),
);
update.finalize();
update.id = add_update(update.clone()).await?;
Ok(update)
}
}
impl Resolve<RefreshRepoCache, User> for State {
#[instrument(
name = "RefreshRepoCache",
+2 -23
View File
@@ -1,9 +1,7 @@
use anyhow::Context;
use formatting::format_serror;
use komodo_client::{
api::write::*,
entities::{
komodo_timestamp,
permission::PermissionLevel,
server::Server,
update::{Update, UpdateStatus},
@@ -11,7 +9,6 @@ use komodo_client::{
Operation,
},
};
use mungos::{by_id::update_one_by_id, mongodb::bson::doc};
use periphery_client::api;
use resolver_api::Resolve;
@@ -21,7 +18,7 @@ use crate::{
update::{add_update, make_update, update_update},
},
resource,
state::{db_client, State},
state::State,
};
impl Resolve<CreateServer, User> for State {
@@ -64,25 +61,7 @@ impl Resolve<RenameServer, User> for State {
RenameServer { id, name }: RenameServer,
user: User,
) -> anyhow::Result<Update> {
let server = resource::get_check_permissions::<Server>(
&id,
&user,
PermissionLevel::Write,
)
.await?;
let mut update =
make_update(&server, Operation::RenameServer, &user);
update_one_by_id(&db_client().servers, &id, mungos::update::Update::Set(doc! { "name": &name, "updated_at": komodo_timestamp() }), None)
.await
.context("failed to update server on db. this name may already be taken.")?;
update.push_simple_log(
"rename server",
format!("renamed server {id} from {} to {name}", server.name),
);
update.finalize();
update.id = add_update(update.clone()).await?;
Ok(update)
resource::rename::<Server>(&id, &name, &user).await
}
}
+13 -2
View File
@@ -1,11 +1,11 @@
use komodo_client::{
api::write::{
CopyServerTemplate, CreateServerTemplate, DeleteServerTemplate,
UpdateServerTemplate,
RenameServerTemplate, UpdateServerTemplate,
},
entities::{
permission::PermissionLevel, server_template::ServerTemplate,
user::User,
update::Update, user::User,
},
};
use resolver_api::Resolve;
@@ -63,3 +63,14 @@ impl Resolve<UpdateServerTemplate, User> for State {
resource::update::<ServerTemplate>(&id, config, &user).await
}
}
impl Resolve<RenameServerTemplate, User> for State {
#[instrument(name = "RenameServerTemplate", skip(self, user))]
async fn resolve(
&self,
RenameServerTemplate { id, name }: RenameServerTemplate,
user: User,
) -> anyhow::Result<Update> {
resource::rename::<ServerTemplate>(&id, &name, &user).await
}
}
+2 -35
View File
@@ -4,7 +4,6 @@ use komodo_client::{
api::write::*,
entities::{
config::core::CoreConfig,
komodo_timestamp,
permission::PermissionLevel,
server::ServerState,
stack::{PartialStackConfig, Stack, StackInfo},
@@ -13,10 +12,7 @@ use komodo_client::{
FileContents, NoData, Operation,
},
};
use mungos::{
by_id::update_one_by_id,
mongodb::bson::{doc, to_document},
};
use mungos::mongodb::bson::{doc, to_document};
use octorust::types::{
ReposCreateWebhookRequest, ReposCreateWebhookRequestConfig,
};
@@ -100,36 +96,7 @@ impl Resolve<RenameStack, User> for State {
RenameStack { id, name }: RenameStack,
user: User,
) -> anyhow::Result<Update> {
let stack = resource::get_check_permissions::<Stack>(
&id,
&user,
PermissionLevel::Write,
)
.await?;
let mut update =
make_update(&stack, Operation::RenameStack, &user);
update_one_by_id(
&db_client().stacks,
&stack.id,
mungos::update::Update::Set(
doc! { "name": &name, "updated_at": komodo_timestamp() },
),
None,
)
.await
.context("failed to update stack name on db")?;
update.push_simple_log(
"rename stack",
format!("renamed stack from {} to {}", stack.name, name),
);
update.finalize();
add_update(update.clone()).await?;
Ok(update)
resource::rename::<Stack>(&id, &name, &user).await
}
}
+33 -2
View File
@@ -5,9 +5,29 @@ use formatting::format_serror;
use komodo_client::{
api::{read::ExportAllResourcesToToml, write::*},
entities::{
self, action::Action, alert::{Alert, AlertData, SeverityLevel}, alerter::Alerter, all_logs_success, build::Build, builder::Builder, config::core::CoreConfig, deployment::Deployment, komodo_timestamp, permission::PermissionLevel, procedure::Procedure, repo::Repo, server::Server, server_template::ServerTemplate, stack::Stack, sync::{
self,
action::Action,
alert::{Alert, AlertData, SeverityLevel},
alerter::Alerter,
all_logs_success,
build::Build,
builder::Builder,
config::core::CoreConfig,
deployment::Deployment,
komodo_timestamp,
permission::PermissionLevel,
procedure::Procedure,
repo::Repo,
server::Server,
server_template::ServerTemplate,
stack::Stack,
sync::{
PartialResourceSyncConfig, ResourceSync, ResourceSyncInfo,
}, to_komodo_name, update::{Log, Update}, user::{sync_user, User}, CloneArgs, NoData, Operation, ResourceTarget
},
to_komodo_name,
update::{Log, Update},
user::{sync_user, User},
CloneArgs, NoData, Operation, ResourceTarget,
},
};
use mungos::{
@@ -87,6 +107,17 @@ impl Resolve<UpdateResourceSync, User> for State {
}
}
impl Resolve<RenameResourceSync, User> for State {
#[instrument(name = "RenameResourceSync", skip(self, user))]
async fn resolve(
&self,
RenameResourceSync { id, name }: RenameResourceSync,
user: User,
) -> anyhow::Result<Update> {
resource::rename::<ResourceSync>(&id, &name, &user).await
}
}
impl Resolve<WriteSyncFileContents, User> for State {
async fn resolve(
&self,
+1 -5
View File
@@ -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)
+274 -2
View File
@@ -4,9 +4,14 @@ use anyhow::{anyhow, Context};
use formatting::{bold, colored, format_serror, muted, Color};
use futures::future::join_all;
use komodo_client::{
api::execute::Execution,
api::execute::*,
entities::{
action::Action,
build::Build,
deployment::Deployment,
procedure::Procedure,
repo::Repo,
stack::Stack,
update::{Log, Update},
user::procedure_user,
},
@@ -17,6 +22,7 @@ use tokio::sync::Mutex;
use crate::{
api::execute::ExecuteRequest,
resource::{list_full_for_user_using_pattern, KomodoResource},
state::{db_client, State},
};
@@ -79,11 +85,94 @@ pub async fn execute_procedure(
#[allow(dependency_on_unit_never_type_fallback)]
#[instrument(skip(update))]
async fn execute_stage(
executions: Vec<Execution>,
_executions: Vec<Execution>,
parent_id: &str,
parent_name: &str,
update: &Mutex<Update>,
) -> anyhow::Result<()> {
let mut executions = Vec::with_capacity(_executions.capacity());
for execution in _executions {
match execution {
Execution::BatchRunAction(exec) => {
extend_batch_exection::<BatchRunAction>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchRunProcedure(exec) => {
extend_batch_exection::<BatchRunProcedure>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchRunBuild(exec) => {
extend_batch_exection::<BatchRunBuild>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchCloneRepo(exec) => {
extend_batch_exection::<BatchCloneRepo>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchPullRepo(exec) => {
extend_batch_exection::<BatchPullRepo>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchBuildRepo(exec) => {
extend_batch_exection::<BatchBuildRepo>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchDeploy(exec) => {
extend_batch_exection::<BatchDeploy>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchDestroyDeployment(exec) => {
extend_batch_exection::<BatchDestroyDeployment>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchDeployStack(exec) => {
extend_batch_exection::<BatchDeployStack>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchDeployStackIfChanged(exec) => {
extend_batch_exection::<BatchDeployStackIfChanged>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchDestroyStack(exec) => {
extend_batch_exection::<BatchDestroyStack>(
&exec.pattern,
&mut executions,
)
.await?;
}
execution => executions.push(execution),
}
}
let futures = executions.into_iter().map(|execution| async move {
let now = Instant::now();
add_line_to_update(
@@ -146,6 +235,12 @@ async fn execute_execution(
)
.await?
}
Execution::BatchRunProcedure(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchRunProcedure not implemented correctly"
));
}
Execution::RunAction(req) => {
let req = ExecuteRequest::RunAction(req);
let update = init_execution_update(&req, &user).await?;
@@ -162,6 +257,12 @@ async fn execute_execution(
)
.await?
}
Execution::BatchRunAction(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchRunAction not implemented correctly"
));
}
Execution::RunBuild(req) => {
let req = ExecuteRequest::RunBuild(req);
let update = init_execution_update(&req, &user).await?;
@@ -178,6 +279,12 @@ async fn execute_execution(
)
.await?
}
Execution::BatchRunBuild(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchRunBuild not implemented correctly"
));
}
Execution::CancelBuild(req) => {
let req = ExecuteRequest::CancelBuild(req);
let update = init_execution_update(&req, &user).await?;
@@ -210,6 +317,12 @@ async fn execute_execution(
)
.await?
}
Execution::BatchDeploy(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchDeploy not implemented correctly"
));
}
Execution::StartDeployment(req) => {
let req = ExecuteRequest::StartDeployment(req);
let update = init_execution_update(&req, &user).await?;
@@ -306,6 +419,12 @@ async fn execute_execution(
)
.await?
}
Execution::BatchDestroyDeployment(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchDestroyDeployment not implemented correctly"
));
}
Execution::CloneRepo(req) => {
let req = ExecuteRequest::CloneRepo(req);
let update = init_execution_update(&req, &user).await?;
@@ -322,6 +441,12 @@ async fn execute_execution(
)
.await?
}
Execution::BatchCloneRepo(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchCloneRepo not implemented correctly"
));
}
Execution::PullRepo(req) => {
let req = ExecuteRequest::PullRepo(req);
let update = init_execution_update(&req, &user).await?;
@@ -338,6 +463,12 @@ async fn execute_execution(
)
.await?
}
Execution::BatchPullRepo(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchPullRepo not implemented correctly"
));
}
Execution::BuildRepo(req) => {
let req = ExecuteRequest::BuildRepo(req);
let update = init_execution_update(&req, &user).await?;
@@ -354,6 +485,12 @@ async fn execute_execution(
)
.await?
}
Execution::BatchBuildRepo(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchBuildRepo not implemented correctly"
));
}
Execution::CancelRepoBuild(req) => {
let req = ExecuteRequest::CancelRepoBuild(req);
let update = init_execution_update(&req, &user).await?;
@@ -743,6 +880,12 @@ async fn execute_execution(
)
.await?
}
Execution::BatchDeployStack(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchDeployStack not implemented correctly"
));
}
Execution::DeployStackIfChanged(req) => {
let req = ExecuteRequest::DeployStackIfChanged(req);
let update = init_execution_update(&req, &user).await?;
@@ -759,6 +902,12 @@ async fn execute_execution(
)
.await?
}
Execution::BatchDeployStackIfChanged(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchDeployStackIfChanged not implemented correctly"
));
}
Execution::StartStack(req) => {
let req = ExecuteRequest::StartStack(req);
let update = init_execution_update(&req, &user).await?;
@@ -855,6 +1004,12 @@ async fn execute_execution(
)
.await?
}
Execution::BatchDestroyStack(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchDestroyStack not implemented correctly"
));
}
Execution::Sleep(req) => {
let duration = Duration::from_millis(req.duration_ms as u64);
tokio::time::sleep(duration).await;
@@ -912,3 +1067,120 @@ async fn add_line_to_update(update: &Mutex<Update>, line: &str) {
error!("Failed to update an update during procedure | {e:#}");
};
}
async fn extend_batch_exection<E: ExtendBatch>(
pattern: &str,
executions: &mut Vec<Execution>,
) -> anyhow::Result<()> {
let more = list_full_for_user_using_pattern::<E::Resource>(
pattern,
Default::default(),
procedure_user(),
&[],
)
.await?
.into_iter()
.map(|resource| E::single_execution(resource.name));
executions.extend(more);
Ok(())
}
trait ExtendBatch {
type Resource: KomodoResource;
fn single_execution(name: String) -> Execution;
}
impl ExtendBatch for BatchRunProcedure {
type Resource = Procedure;
fn single_execution(procedure: String) -> Execution {
Execution::RunProcedure(RunProcedure { procedure })
}
}
impl ExtendBatch for BatchRunAction {
type Resource = Action;
fn single_execution(action: String) -> Execution {
Execution::RunAction(RunAction { action })
}
}
impl ExtendBatch for BatchRunBuild {
type Resource = Build;
fn single_execution(build: String) -> Execution {
Execution::RunBuild(RunBuild { build })
}
}
impl ExtendBatch for BatchCloneRepo {
type Resource = Repo;
fn single_execution(repo: String) -> Execution {
Execution::CloneRepo(CloneRepo { repo })
}
}
impl ExtendBatch for BatchPullRepo {
type Resource = Repo;
fn single_execution(repo: String) -> Execution {
Execution::PullRepo(PullRepo { repo })
}
}
impl ExtendBatch for BatchBuildRepo {
type Resource = Repo;
fn single_execution(repo: String) -> Execution {
Execution::BuildRepo(BuildRepo { repo })
}
}
impl ExtendBatch for BatchDeploy {
type Resource = Deployment;
fn single_execution(deployment: String) -> Execution {
Execution::Deploy(Deploy {
deployment,
stop_signal: None,
stop_time: None,
})
}
}
impl ExtendBatch for BatchDestroyDeployment {
type Resource = Deployment;
fn single_execution(deployment: String) -> Execution {
Execution::DestroyDeployment(DestroyDeployment {
deployment,
signal: None,
time: None,
})
}
}
impl ExtendBatch for BatchDeployStack {
type Resource = Stack;
fn single_execution(stack: String) -> Execution {
Execution::DeployStack(DeployStack {
stack,
stop_time: None,
})
}
}
impl ExtendBatch for BatchDeployStackIfChanged {
type Resource = Stack;
fn single_execution(stack: String) -> Execution {
Execution::DeployStackIfChanged(DeployStackIfChanged {
stack,
stop_time: None,
})
}
}
impl ExtendBatch for BatchDestroyStack {
type Resource = Stack;
fn single_execution(stack: String) -> Execution {
Execution::DestroyStack(DestroyStack {
stack,
remove_orphans: false,
stop_time: None,
})
}
}
+30
View File
@@ -261,6 +261,9 @@ pub async fn init_execution_update(
resource::get::<Deployment>(&data.deployment).await?.id,
),
),
ExecuteRequest::BatchDeploy(_data) => {
return Ok(Default::default())
}
ExecuteRequest::StartDeployment(data) => (
Operation::StartDeployment,
ResourceTarget::Deployment(
@@ -305,6 +308,9 @@ pub async fn init_execution_update(
resource::get::<Build>(&data.build).await?.id,
),
),
ExecuteRequest::BatchRunBuild(_data) => {
return Ok(Default::default())
}
ExecuteRequest::CancelBuild(data) => (
Operation::CancelBuild,
ResourceTarget::Build(
@@ -319,18 +325,27 @@ pub async fn init_execution_update(
resource::get::<Repo>(&data.repo).await?.id,
),
),
ExecuteRequest::BatchCloneRepo(_data) => {
return Ok(Default::default())
}
ExecuteRequest::PullRepo(data) => (
Operation::PullRepo,
ResourceTarget::Repo(
resource::get::<Repo>(&data.repo).await?.id,
),
),
ExecuteRequest::BatchPullRepo(_data) => {
return Ok(Default::default())
}
ExecuteRequest::BuildRepo(data) => (
Operation::BuildRepo,
ResourceTarget::Repo(
resource::get::<Repo>(&data.repo).await?.id,
),
),
ExecuteRequest::BatchBuildRepo(_data) => {
return Ok(Default::default())
}
ExecuteRequest::CancelRepoBuild(data) => (
Operation::CancelRepoBuild,
ResourceTarget::Repo(
@@ -345,6 +360,9 @@ pub async fn init_execution_update(
resource::get::<Procedure>(&data.procedure).await?.id,
),
),
ExecuteRequest::BatchRunProcedure(_) => {
return Ok(Default::default())
}
// Action
ExecuteRequest::RunAction(data) => (
@@ -353,6 +371,9 @@ pub async fn init_execution_update(
resource::get::<Action>(&data.action).await?.id,
),
),
ExecuteRequest::BatchRunAction(_) => {
return Ok(Default::default())
}
// Server template
ExecuteRequest::LaunchServer(data) => (
@@ -379,12 +400,18 @@ pub async fn init_execution_update(
resource::get::<Stack>(&data.stack).await?.id,
),
),
ExecuteRequest::BatchDeployStack(_data) => {
return Ok(Default::default())
}
ExecuteRequest::DeployStackIfChanged(data) => (
Operation::DeployStack,
ResourceTarget::Stack(
resource::get::<Stack>(&data.stack).await?.id,
),
),
ExecuteRequest::BatchDeployStackIfChanged(_data) => {
return Ok(Default::default())
}
ExecuteRequest::StartStack(data) => (
if data.service.is_some() {
Operation::StartStackService
@@ -441,6 +468,9 @@ pub async fn init_execution_update(
resource::get::<Stack>(&data.stack).await?.id,
),
),
ExecuteRequest::BatchDestroyStack(_data) => {
return Ok(Default::default())
}
};
let mut update = make_update(target, operation, user);
update.in_progress();
-66
View File
@@ -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(())
}
-278
View File
@@ -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<()>>>;
-74
View File
@@ -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(())
}
-133
View File
@@ -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(())
}
-112
View File
@@ -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(())
}
-96
View File
@@ -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(())
}
@@ -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"))
}
}
}
@@ -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"))
}
}
}
@@ -0,0 +1,2 @@
pub mod github;
pub mod gitlab;
+48 -3
View File
@@ -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
View 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
View 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)
}
+11 -6
View File
@@ -31,8 +31,8 @@ impl super::KomodoResource for Action {
ResourceTargetVariant::Action
}
async fn coll(
) -> &'static Collection<Resource<Self::Config, Self::Info>> {
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().actions
}
@@ -105,11 +105,16 @@ impl super::KomodoResource for Action {
}
async fn post_update(
_updated: &Self,
_update: &mut Update,
updated: &Self,
update: &mut Update,
) -> anyhow::Result<()> {
refresh_action_state_cache().await;
Ok(())
Self::post_create(updated, update).await
}
// RENAME
fn rename_operation() -> Operation {
Operation::RenameAction
}
// DELETE
+8 -2
View File
@@ -25,8 +25,8 @@ impl super::KomodoResource for Alerter {
ResourceTargetVariant::Alerter
}
async fn coll(
) -> &'static Collection<Resource<Self::Config, Self::Info>> {
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().alerters
}
@@ -94,6 +94,12 @@ impl super::KomodoResource for Alerter {
Ok(())
}
// RENAME
fn rename_operation() -> Operation {
Operation::RenameAlerter
}
// DELETE
fn delete_operation() -> Operation {
+11 -6
View File
@@ -38,8 +38,8 @@ impl super::KomodoResource for Build {
ResourceTargetVariant::Build
}
async fn coll(
) -> &'static Collection<Resource<Self::Config, Self::Info>> {
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().builds
}
@@ -118,11 +118,16 @@ impl super::KomodoResource for Build {
}
async fn post_update(
_updated: &Self,
_update: &mut Update,
updated: &Self,
update: &mut Update,
) -> anyhow::Result<()> {
refresh_build_state_cache().await;
Ok(())
Self::post_create(updated, update).await
}
// RENAME
fn rename_operation() -> Operation {
Operation::RenameBuild
}
// DELETE
+18 -7
View File
@@ -31,8 +31,8 @@ impl super::KomodoResource for Builder {
ResourceTargetVariant::Builder
}
async fn coll(
) -> &'static Collection<Resource<Self::Config, Self::Info>> {
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().builders
}
@@ -118,6 +118,12 @@ impl super::KomodoResource for Builder {
Ok(())
}
// RENAME
fn rename_operation() -> Operation {
Operation::RenameBuilder
}
// DELETE
fn delete_operation() -> Operation {
@@ -128,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(())
}
+24 -14
View File
@@ -26,7 +26,6 @@ use crate::{
query::get_deployment_state,
},
monitor::update_cache_for_server,
resource,
state::{action_states, db_client, deployment_status_cache},
};
@@ -44,8 +43,8 @@ impl super::KomodoResource for Deployment {
ResourceTargetVariant::Deployment
}
async fn coll(
) -> &'static Collection<Resource<Self::Config, Self::Info>> {
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().deployments
}
@@ -132,11 +131,21 @@ impl super::KomodoResource for Deployment {
created: &Resource<Self::Config, Self::Info>,
_update: &mut Update,
) -> anyhow::Result<()> {
if !created.config.server_id.is_empty() {
let server =
resource::get::<Server>(&created.config.server_id).await?;
update_cache_for_server(&server).await;
if created.config.server_id.is_empty() {
return Ok(());
}
let Ok(server) = super::get::<Server>(&created.config.server_id)
.await
.inspect_err(|e| {
warn!(
"Failed to get Server for Deployment {} | {e:#}",
created.name
)
})
else {
return Ok(());
};
update_cache_for_server(&server).await;
Ok(())
}
@@ -156,14 +165,15 @@ impl super::KomodoResource for Deployment {
async fn post_update(
updated: &Self,
_update: &mut Update,
update: &mut Update,
) -> anyhow::Result<()> {
if !updated.config.server_id.is_empty() {
let server =
resource::get::<Server>(&updated.config.server_id).await?;
update_cache_for_server(&server).await;
}
Ok(())
Self::post_create(updated, update).await
}
// RENAME
fn rename_operation() -> Operation {
Operation::RenameDeployment
}
// DELETE
+218 -101
View File
@@ -18,6 +18,7 @@ use komodo_client::{
user::{system_user, User},
Operation, ResourceTarget, ResourceTargetVariant,
},
parsers::parse_string_list,
};
use mungos::{
by_id::{delete_one_by_id, update_one_by_id},
@@ -110,8 +111,7 @@ pub trait KomodoResource {
fn resource_type() -> ResourceTargetVariant;
async fn coll(
) -> &'static Collection<Resource<Self::Config, Self::Info>>;
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>;
async fn to_list_item(
resource: Resource<Self::Config, Self::Info>,
@@ -169,6 +169,12 @@ pub trait KomodoResource {
update: &mut Update,
) -> anyhow::Result<()>;
// =======
// RENAME
// =======
fn rename_operation() -> Operation;
// =======
// DELETE
// =======
@@ -199,7 +205,6 @@ pub async fn get<T: KomodoResource>(
id_or_name: &str,
) -> anyhow::Result<Resource<T::Config, T::Info>> {
T::coll()
.await
.find_one(id_or_name_filter(id_or_name))
.await
.context("failed to query db for resource")?
@@ -238,89 +243,6 @@ pub async fn get_check_permissions<T: KomodoResource>(
}
}
// ======
// LIST
// ======
/// Returns None if still no need to filter by resource id (eg transparent mode, group membership with all access).
#[instrument(level = "debug")]
pub async fn get_resource_object_ids_for_user<T: KomodoResource>(
user: &User,
) -> anyhow::Result<Option<Vec<ObjectId>>> {
get_resource_ids_for_user::<T>(user).await.map(|ids| {
ids.map(|ids| {
ids
.into_iter()
.flat_map(|id| ObjectId::from_str(&id))
.collect()
})
})
}
/// Returns None if still no need to filter by resource id (eg transparent mode, group membership with all access).
#[instrument(level = "debug")]
pub async fn get_resource_ids_for_user<T: KomodoResource>(
user: &User,
) -> anyhow::Result<Option<Vec<String>>> {
// Check admin or transparent mode
if user.admin || core_config().transparent_mode {
return Ok(None);
}
let resource_type = T::resource_type();
// Check user 'all' on variant
if let Some(level) = user.all.get(&resource_type).cloned() {
if level > PermissionLevel::None {
return Ok(None);
}
}
// Check user groups 'all' on variant
let groups = get_user_user_groups(&user.id).await?;
for group in &groups {
if let Some(level) = group.all.get(&resource_type).cloned() {
if level > PermissionLevel::None {
return Ok(None);
}
}
}
let (base, perms) = tokio::try_join!(
// Get any resources with non-none base permission,
find_collect(
T::coll().await,
doc! { "base_permission": { "$exists": true, "$ne": "None" } },
None,
)
.map(|res| res.with_context(|| format!(
"failed to query {resource_type} on db"
))),
// And any ids using the permissions table
find_collect(
&db_client().permissions,
doc! {
"$or": user_target_query(&user.id, &groups)?,
"resource_target.type": resource_type.as_ref(),
"level": { "$exists": true, "$ne": "None" }
},
None,
)
.map(|res| res.context("failed to query permissions on db"))
)?;
// Add specific ids
let ids = perms
.into_iter()
.map(|p| p.resource_target.extract_variant_id().1.to_string())
// Chain in the ones with non-None base permissions
.chain(base.into_iter().map(|res| res.id))
// collect into hashset first to remove any duplicates
.collect::<HashSet<_>>();
Ok(Some(ids.into_iter().collect()))
}
#[instrument(level = "debug")]
pub async fn get_user_permission_on_resource<T: KomodoResource>(
user: &User,
@@ -394,6 +316,89 @@ pub async fn get_user_permission_on_resource<T: KomodoResource>(
Ok(permission)
}
// ======
// LIST
// ======
/// Returns None if still no need to filter by resource id (eg transparent mode, group membership with all access).
#[instrument(level = "debug")]
pub async fn get_resource_object_ids_for_user<T: KomodoResource>(
user: &User,
) -> anyhow::Result<Option<Vec<ObjectId>>> {
get_resource_ids_for_user::<T>(user).await.map(|ids| {
ids.map(|ids| {
ids
.into_iter()
.flat_map(|id| ObjectId::from_str(&id))
.collect()
})
})
}
/// Returns None if still no need to filter by resource id (eg transparent mode, group membership with all access).
#[instrument(level = "debug")]
pub async fn get_resource_ids_for_user<T: KomodoResource>(
user: &User,
) -> anyhow::Result<Option<Vec<String>>> {
// Check admin or transparent mode
if user.admin || core_config().transparent_mode {
return Ok(None);
}
let resource_type = T::resource_type();
// Check user 'all' on variant
if let Some(level) = user.all.get(&resource_type).cloned() {
if level > PermissionLevel::None {
return Ok(None);
}
}
// Check user groups 'all' on variant
let groups = get_user_user_groups(&user.id).await?;
for group in &groups {
if let Some(level) = group.all.get(&resource_type).cloned() {
if level > PermissionLevel::None {
return Ok(None);
}
}
}
let (base, perms) = tokio::try_join!(
// Get any resources with non-none base permission,
find_collect(
T::coll(),
doc! { "base_permission": { "$exists": true, "$ne": "None" } },
None,
)
.map(|res| res.with_context(|| format!(
"failed to query {resource_type} on db"
))),
// And any ids using the permissions table
find_collect(
&db_client().permissions,
doc! {
"$or": user_target_query(&user.id, &groups)?,
"resource_target.type": resource_type.as_ref(),
"level": { "$exists": true, "$ne": "None" }
},
None,
)
.map(|res| res.context("failed to query permissions on db"))
)?;
// Add specific ids
let ids = perms
.into_iter()
.map(|p| p.resource_target.extract_variant_id().1.to_string())
// Chain in the ones with non-None base permissions
.chain(base.into_iter().map(|res| res.id))
// collect into hashset first to remove any duplicates
.collect::<HashSet<_>>();
Ok(Some(ids.into_iter().collect()))
}
#[instrument(level = "debug")]
pub async fn list_for_user<T: KomodoResource>(
mut query: ResourceQuery<T::QuerySpecifics>,
@@ -406,6 +411,23 @@ pub async fn list_for_user<T: KomodoResource>(
list_for_user_using_document::<T>(filters, user).await
}
#[instrument(level = "debug")]
pub async fn list_for_user_using_pattern<T: KomodoResource>(
pattern: &str,
query: ResourceQuery<T::QuerySpecifics>,
user: &User,
all_tags: &[Tag],
) -> anyhow::Result<Vec<T::ListItem>> {
let list = list_full_for_user_using_pattern::<T>(
pattern, query, user, all_tags,
)
.await?
.into_iter()
.map(|resource| T::to_list_item(resource));
Ok(join_all(list).await)
}
#[instrument(level = "debug")]
pub async fn list_for_user_using_document<T: KomodoResource>(
filters: Document,
user: &User,
@@ -417,6 +439,55 @@ pub async fn list_for_user_using_document<T: KomodoResource>(
Ok(join_all(list).await)
}
/// Lists full resource matching wildcard syntax,
/// or regex if wrapped with "\\"
///
/// ## Example
/// ```
/// let items = list_full_for_user_using_match_string::<Build>("foo-*", Default::default(), user, all_tags).await?;
/// let items = list_full_for_user_using_match_string::<Build>("\\^foo-.*$\\", Default::default(), user, all_tags).await?;
/// ```
#[instrument(level = "debug")]
pub async fn list_full_for_user_using_pattern<T: KomodoResource>(
pattern: &str,
query: ResourceQuery<T::QuerySpecifics>,
user: &User,
all_tags: &[Tag],
) -> anyhow::Result<Vec<Resource<T::Config, T::Info>>> {
let resources =
list_full_for_user::<T>(query, user, all_tags).await?;
let patterns = parse_string_list(pattern);
let mut names = HashSet::<String>::new();
for pattern in patterns {
if pattern.starts_with('\\') && pattern.ends_with('\\') {
let regex = regex::Regex::new(&pattern[1..(pattern.len() - 1)])
.context("Regex matching string invalid")?;
for resource in &resources {
if regex.is_match(&resource.name) {
names.insert(resource.name.clone());
}
}
} else {
let wildcard = wildcard::Wildcard::new(pattern.as_bytes())
.context("Wildcard matching string invalid")?;
for resource in &resources {
if wildcard.is_match(resource.name.as_bytes()) {
names.insert(resource.name.clone());
}
}
};
}
Ok(
resources
.into_iter()
.filter(|resource| names.contains(resource.name.as_str()))
.collect(),
)
}
#[instrument(level = "debug")]
pub async fn list_full_for_user<T: KomodoResource>(
mut query: ResourceQuery<T::QuerySpecifics>,
@@ -440,7 +511,7 @@ pub async fn list_full_for_user_using_document<T: KomodoResource>(
filters.insert("_id", doc! { "$in": ids });
}
find_collect(
T::coll().await,
T::coll(),
filters,
FindOptions::builder().sort(doc! { "name": 1 }).build(),
)
@@ -463,7 +534,7 @@ pub async fn get_id_to_resource_map<T: KomodoResource>(
id_to_tags: &HashMap<String, Tag>,
match_tags: &[String],
) -> anyhow::Result<IdResourceMap<T>> {
let res = find_collect(T::coll().await, None, None)
let res = find_collect(T::coll(), None, None)
.await
.with_context(|| {
format!("failed to pull {}s from mongo", T::resource_type())
@@ -555,7 +626,6 @@ pub async fn create<T: KomodoResource>(
};
let resource_id = T::coll()
.await
.insert_one(&resource)
.await
.with_context(|| {
@@ -644,14 +714,9 @@ pub async fn update<T: KomodoResource>(
let update_doc = flatten_document(doc! { "config": config_doc });
update_one_by_id(
T::coll().await,
&id,
doc! { "$set": update_doc },
None,
)
.await
.context("failed to update resource on database")?;
update_one_by_id(T::coll(), &id, doc! { "$set": update_doc }, None)
.await
.context("failed to update resource on database")?;
let mut update = make_update(
resource_target::<T>(id),
@@ -707,7 +772,6 @@ pub async fn update_description<T: KomodoResource>(
)
.await?;
T::coll()
.await
.update_one(
id_or_name_filter(id_or_name),
doc! { "$set": { "description": description } },
@@ -741,7 +805,6 @@ pub async fn update_tags<T: KomodoResource>(
.flatten()
.collect::<Vec<_>>();
T::coll()
.await
.update_one(
id_or_name_filter(id_or_name),
doc! { "$set": { "tags": tags } },
@@ -754,13 +817,67 @@ pub async fn remove_tag_from_all<T: KomodoResource>(
tag_id: &str,
) -> anyhow::Result<()> {
T::coll()
.await
.update_many(doc! {}, doc! { "$pull": { "tags": tag_id } })
.await
.context("failed to remove tag from resources")?;
Ok(())
}
// =======
// RENAME
// =======
pub async fn rename<T: KomodoResource>(
id_or_name: &str,
name: &str,
user: &User,
) -> anyhow::Result<Update> {
let resource = get_check_permissions::<T>(
id_or_name,
user,
PermissionLevel::Write,
)
.await?;
let mut update = make_update(
resource_target::<T>(resource.id.clone()),
T::rename_operation(),
user,
);
let name = to_komodo_name(name);
update_one_by_id(
T::coll(),
&resource.id,
mungos::update::Update::Set(
doc! { "name": &name, "updated_at": komodo_timestamp() },
),
None,
)
.await
.with_context(|| {
format!(
"Failed to update {ty} on db. This name may already be taken.",
ty = T::resource_type()
)
})?;
update.push_simple_log(
&format!("Rename {}", T::resource_type()),
format!(
"Renamed {ty} {id} from {prev_name} to {name}",
ty = T::resource_type(),
id = resource.id,
prev_name = resource.name
),
);
update.finalize();
update.id = add_update(update.clone()).await?;
Ok(update)
}
// =======
// DELETE
// =======
@@ -790,7 +907,7 @@ pub async fn delete<T: KomodoResource>(
delete_all_permissions_on_resource(target.clone()).await;
remove_from_recently_viewed(target.clone()).await;
delete_one_by_id(T::coll().await, &resource.id, None)
delete_one_by_id(T::coll(), &resource.id, None)
.await
.with_context(|| {
format!("failed to delete {} from database", T::resource_type())
+88 -6
View File
@@ -45,8 +45,8 @@ impl super::KomodoResource for Procedure {
ResourceTargetVariant::Procedure
}
async fn coll(
) -> &'static Collection<Resource<Self::Config, Self::Info>> {
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().procedures
}
@@ -115,11 +115,16 @@ impl super::KomodoResource for Procedure {
}
async fn post_update(
_updated: &Self,
_update: &mut Update,
updated: &Self,
update: &mut Update,
) -> anyhow::Result<()> {
refresh_procedure_state_cache().await;
Ok(())
Self::post_create(updated, update).await
}
// RENAME
fn rename_operation() -> Operation {
Operation::RenameProcedure
}
// DELETE
@@ -173,6 +178,13 @@ async fn validate_config(
}
params.procedure = procedure.id;
}
Execution::BatchRunProcedure(_params) => {
if !user.admin {
return Err(anyhow!(
"Non admin user cannot configure Batch executions"
));
}
}
Execution::RunAction(params) => {
let action = super::get_check_permissions::<Action>(
&params.action,
@@ -182,6 +194,13 @@ async fn validate_config(
.await?;
params.action = action.id;
}
Execution::BatchRunAction(_params) => {
if !user.admin {
return Err(anyhow!(
"Non admin user cannot configure Batch executions"
));
}
}
Execution::RunBuild(params) => {
let build = super::get_check_permissions::<Build>(
&params.build,
@@ -191,6 +210,13 @@ async fn validate_config(
.await?;
params.build = build.id;
}
Execution::BatchRunBuild(_params) => {
if !user.admin {
return Err(anyhow!(
"Non admin user cannot configure Batch executions"
));
}
}
Execution::CancelBuild(params) => {
let build = super::get_check_permissions::<Build>(
&params.build,
@@ -210,6 +236,13 @@ async fn validate_config(
.await?;
params.deployment = deployment.id;
}
Execution::BatchDeploy(_params) => {
if !user.admin {
return Err(anyhow!(
"Non admin user cannot configure Batch executions"
));
}
}
Execution::StartDeployment(params) => {
let deployment =
super::get_check_permissions::<Deployment>(
@@ -270,6 +303,13 @@ async fn validate_config(
.await?;
params.deployment = deployment.id;
}
Execution::BatchDestroyDeployment(_params) => {
if !user.admin {
return Err(anyhow!(
"Non admin user cannot configure Batch executions"
));
}
}
Execution::CloneRepo(params) => {
let repo = super::get_check_permissions::<Repo>(
&params.repo,
@@ -279,6 +319,13 @@ async fn validate_config(
.await?;
params.repo = repo.id;
}
Execution::BatchCloneRepo(_params) => {
if !user.admin {
return Err(anyhow!(
"Non admin user cannot configure Batch executions"
));
}
}
Execution::PullRepo(params) => {
let repo = super::get_check_permissions::<Repo>(
&params.repo,
@@ -288,6 +335,13 @@ async fn validate_config(
.await?;
params.repo = repo.id;
}
Execution::BatchPullRepo(_params) => {
if !user.admin {
return Err(anyhow!(
"Non admin user cannot configure Batch executions"
));
}
}
Execution::BuildRepo(params) => {
let repo = super::get_check_permissions::<Repo>(
&params.repo,
@@ -297,6 +351,13 @@ async fn validate_config(
.await?;
params.repo = repo.id;
}
Execution::BatchBuildRepo(_params) => {
if !user.admin {
return Err(anyhow!(
"Non admin user cannot configure Batch executions"
));
}
}
Execution::CancelRepoBuild(params) => {
let repo = super::get_check_permissions::<Repo>(
&params.repo,
@@ -523,6 +584,13 @@ async fn validate_config(
.await?;
params.stack = stack.id;
}
Execution::BatchDeployStack(_params) => {
if !user.admin {
return Err(anyhow!(
"Non admin user cannot configure Batch executions"
));
}
}
Execution::DeployStackIfChanged(params) => {
let stack = super::get_check_permissions::<Stack>(
&params.stack,
@@ -532,6 +600,13 @@ async fn validate_config(
.await?;
params.stack = stack.id;
}
Execution::BatchDeployStackIfChanged(_params) => {
if !user.admin {
return Err(anyhow!(
"Non admin user cannot configure Batch executions"
));
}
}
Execution::StartStack(params) => {
let stack = super::get_check_permissions::<Stack>(
&params.stack,
@@ -586,6 +661,13 @@ async fn validate_config(
.await?;
params.stack = stack.id;
}
Execution::BatchDestroyStack(_params) => {
if !user.admin {
return Err(anyhow!(
"Non admin user cannot configure Batch executions"
));
}
}
Execution::Sleep(_) => {}
}
}
+14 -3
View File
@@ -11,6 +11,7 @@ use komodo_client::entities::{
},
resource::Resource,
server::Server,
to_komodo_name,
update::Update,
user::User,
Operation, ResourceTargetVariant,
@@ -43,8 +44,8 @@ impl super::KomodoResource for Repo {
ResourceTargetVariant::Repo
}
async fn coll(
) -> &'static Collection<Resource<Self::Config, Self::Info>> {
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().repos
}
@@ -132,6 +133,12 @@ impl super::KomodoResource for Repo {
Ok(())
}
// RENAME
fn rename_operation() -> Operation {
Operation::RenameRepo
}
// DELETE
fn delete_operation() -> Operation {
@@ -158,7 +165,11 @@ impl super::KomodoResource for Repo {
match periphery
.request(DeleteRepo {
name: repo.name.clone(),
name: if repo.config.path.is_empty() {
to_komodo_name(&repo.name)
} else {
repo.config.path.clone()
},
})
.await
{
+8 -2
View File
@@ -30,8 +30,8 @@ impl super::KomodoResource for Server {
ResourceTargetVariant::Server
}
async fn coll(
) -> &'static Collection<Resource<Self::Config, Self::Info>> {
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().servers
}
@@ -115,6 +115,12 @@ impl super::KomodoResource for Server {
Ok(())
}
// RENAME
fn rename_operation() -> Operation {
Operation::RenameServer
}
// DELETE
fn delete_operation() -> Operation {
+8 -2
View File
@@ -29,8 +29,8 @@ impl super::KomodoResource for ServerTemplate {
ResourceTargetVariant::ServerTemplate
}
async fn coll(
) -> &'static Collection<Resource<Self::Config, Self::Info>> {
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().server_templates
}
@@ -117,6 +117,12 @@ impl super::KomodoResource for ServerTemplate {
Ok(())
}
// RENAME
fn rename_operation() -> Operation {
Operation::RenameServerTemplate
}
// DELETE
fn delete_operation() -> Operation {
+9 -3
View File
@@ -44,8 +44,8 @@ impl super::KomodoResource for Stack {
ResourceTargetVariant::Stack
}
async fn coll(
) -> &'static Collection<Resource<Self::Config, Self::Info>> {
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().stacks
}
@@ -172,7 +172,7 @@ impl super::KomodoResource for Stack {
.await
.inspect_err(|e| {
warn!(
"Failed to get server for stack {} | {e:#}",
"Failed to get Server for Stack {} | {e:#}",
created.name
)
})
@@ -204,6 +204,12 @@ impl super::KomodoResource for Stack {
Self::post_create(updated, update).await
}
// RENAME
fn rename_operation() -> Operation {
Operation::RenameStack
}
// DELETE
fn delete_operation() -> Operation {
+9 -2
View File
@@ -41,8 +41,8 @@ impl super::KomodoResource for ResourceSync {
ResourceTargetVariant::ResourceSync
}
async fn coll(
) -> &'static Collection<Resource<Self::Config, Self::Info>> {
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().resource_syncs
}
@@ -117,6 +117,7 @@ impl super::KomodoResource for ResourceSync {
format_serror(&e.context("The sync pending cache has failed to refresh. This is likely due to a misconfiguration of the sync").into())
);
};
refresh_resource_sync_state_cache().await;
Ok(())
}
@@ -141,6 +142,12 @@ impl super::KomodoResource for ResourceSync {
Self::post_create(updated, update).await
}
// RENAME
fn rename_operation() -> Operation {
Operation::RenameResourceSync
}
// DELETE
fn delete_operation() -> Operation {
+1 -1
View File
@@ -32,7 +32,7 @@ pub async fn get_updates_for_execution<
id_to_tags: &HashMap<String, Tag>,
match_tags: &[String],
) -> anyhow::Result<UpdatesResult<Resource::PartialConfig>> {
let map = find_collect(Resource::coll().await, None, None)
let map = find_collect(Resource::coll(), None, None)
.await
.context("failed to get resources from db")?
.into_iter()
+4 -5
View File
@@ -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,
)
+11
View File
@@ -360,6 +360,7 @@ impl ResourceSyncTrait for Procedure {
.map(|p| p.name.clone())
.unwrap_or_default();
}
Execution::BatchRunProcedure(_config) => {}
Execution::RunAction(config) => {
config.action = resources
.actions
@@ -367,6 +368,7 @@ impl ResourceSyncTrait for Procedure {
.map(|p| p.name.clone())
.unwrap_or_default();
}
Execution::BatchRunAction(_config) => {}
Execution::RunBuild(config) => {
config.build = resources
.builds
@@ -374,6 +376,7 @@ impl ResourceSyncTrait for Procedure {
.map(|b| b.name.clone())
.unwrap_or_default();
}
Execution::BatchRunBuild(_config) => {}
Execution::CancelBuild(config) => {
config.build = resources
.builds
@@ -388,6 +391,7 @@ impl ResourceSyncTrait for Procedure {
.map(|d| d.name.clone())
.unwrap_or_default();
}
Execution::BatchDeploy(_config) => {}
Execution::StartDeployment(config) => {
config.deployment = resources
.deployments
@@ -430,6 +434,7 @@ impl ResourceSyncTrait for Procedure {
.map(|d| d.name.clone())
.unwrap_or_default();
}
Execution::BatchDestroyDeployment(_config) => {}
Execution::CloneRepo(config) => {
config.repo = resources
.repos
@@ -437,6 +442,7 @@ impl ResourceSyncTrait for Procedure {
.map(|d| d.name.clone())
.unwrap_or_default();
}
Execution::BatchCloneRepo(_config) => {}
Execution::PullRepo(config) => {
config.repo = resources
.repos
@@ -444,6 +450,7 @@ impl ResourceSyncTrait for Procedure {
.map(|d| d.name.clone())
.unwrap_or_default();
}
Execution::BatchPullRepo(_config) => {}
Execution::BuildRepo(config) => {
config.repo = resources
.repos
@@ -451,6 +458,7 @@ impl ResourceSyncTrait for Procedure {
.map(|d| d.name.clone())
.unwrap_or_default();
}
Execution::BatchBuildRepo(_config) => {}
Execution::CancelRepoBuild(config) => {
config.repo = resources
.repos
@@ -626,6 +634,7 @@ impl ResourceSyncTrait for Procedure {
.map(|s| s.name.clone())
.unwrap_or_default();
}
Execution::BatchDeployStack(_config) => {}
Execution::DeployStackIfChanged(config) => {
config.stack = resources
.stacks
@@ -633,6 +642,7 @@ impl ResourceSyncTrait for Procedure {
.map(|s| s.name.clone())
.unwrap_or_default();
}
Execution::BatchDeployStackIfChanged(_config) => {}
Execution::StartStack(config) => {
config.stack = resources
.stacks
@@ -675,6 +685,7 @@ impl ResourceSyncTrait for Procedure {
.map(|s| s.name.clone())
.unwrap_or_default();
}
Execution::BatchDestroyStack(_config) => {}
Execution::Sleep(_) => {}
}
}
+11
View File
@@ -414,6 +414,7 @@ impl ToToml for Procedure {
.map(|r| &r.name)
.unwrap_or(&String::new()),
),
Execution::BatchRunProcedure(_exec) => {}
Execution::RunAction(exec) => exec.action.clone_from(
all
.actions
@@ -421,6 +422,7 @@ impl ToToml for Procedure {
.map(|r| &r.name)
.unwrap_or(&String::new()),
),
Execution::BatchRunAction(_exec) => {}
Execution::RunBuild(exec) => exec.build.clone_from(
all
.builds
@@ -428,6 +430,7 @@ impl ToToml for Procedure {
.map(|r| &r.name)
.unwrap_or(&String::new()),
),
Execution::BatchRunBuild(_exec) => {}
Execution::CancelBuild(exec) => exec.build.clone_from(
all
.builds
@@ -442,6 +445,7 @@ impl ToToml for Procedure {
.map(|r| &r.name)
.unwrap_or(&String::new()),
),
Execution::BatchDeploy(_exec) => {}
Execution::StartDeployment(exec) => {
exec.deployment.clone_from(
all
@@ -496,6 +500,7 @@ impl ToToml for Procedure {
.unwrap_or(&String::new()),
)
}
Execution::BatchDestroyDeployment(_exec) => {}
Execution::CloneRepo(exec) => exec.repo.clone_from(
all
.repos
@@ -503,6 +508,7 @@ impl ToToml for Procedure {
.map(|r| &r.name)
.unwrap_or(&String::new()),
),
Execution::BatchCloneRepo(_exec) => {}
Execution::PullRepo(exec) => exec.repo.clone_from(
all
.repos
@@ -510,6 +516,7 @@ impl ToToml for Procedure {
.map(|r| &r.name)
.unwrap_or(&String::new()),
),
Execution::BatchPullRepo(_exec) => {}
Execution::BuildRepo(exec) => exec.repo.clone_from(
all
.repos
@@ -517,6 +524,7 @@ impl ToToml for Procedure {
.map(|r| &r.name)
.unwrap_or(&String::new()),
),
Execution::BatchBuildRepo(_exec) => {}
Execution::CancelRepoBuild(exec) => exec.repo.clone_from(
all
.repos
@@ -710,6 +718,7 @@ impl ToToml for Procedure {
.map(|r| &r.name)
.unwrap_or(&String::new()),
),
Execution::BatchDeployStack(_exec) => {}
Execution::DeployStackIfChanged(exec) => {
exec.stack.clone_from(
all
@@ -719,6 +728,7 @@ impl ToToml for Procedure {
.unwrap_or(&String::new()),
)
}
Execution::BatchDeployStackIfChanged(_exec) => {}
Execution::StartStack(exec) => exec.stack.clone_from(
all
.stacks
@@ -761,6 +771,7 @@ impl ToToml for Procedure {
.map(|r| &r.name)
.unwrap_or(&String::new()),
),
Execution::BatchDestroyStack(_exec) => {}
Execution::Sleep(_) | Execution::None(_) => {}
}
}
+1 -1
View File
@@ -22,7 +22,7 @@ pub async fn push_updates_for_view<Resource: ResourceSyncTrait>(
match_tags: &[String],
diffs: &mut Vec<ResourceDiff>,
) -> anyhow::Result<()> {
let current_map = find_collect(Resource::coll().await, None, None)
let current_map = find_collect(Resource::coll(), None, None)
.await
.context("failed to get resources from db")?
.into_iter()
+22 -9
View File
@@ -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("")
}
+20 -8
View File
@@ -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("")
}
+36 -11
View File
@@ -1,13 +1,12 @@
use anyhow::{anyhow, Context};
use git::GitRes;
use komodo_client::entities::{
to_komodo_name, update::Log, CloneArgs, LatestCommit,
};
use komodo_client::entities::{update::Log, CloneArgs, LatestCommit};
use periphery_client::api::git::{
CloneRepo, DeleteRepo, GetLatestCommit, PullOrCloneRepo, PullRepo,
RepoActionResponse,
RenameRepo, RepoActionResponse,
};
use resolver_api::Resolve;
use tokio::fs;
use crate::{config::periphery_config, State};
@@ -207,6 +206,31 @@ impl Resolve<PullOrCloneRepo> for State {
//
impl Resolve<RenameRepo> for State {
#[instrument(name = "RenameRepo", skip(self))]
async fn resolve(
&self,
RenameRepo {
curr_name,
new_name,
}: RenameRepo,
_: (),
) -> anyhow::Result<Log> {
let renamed = fs::rename(
periphery_config().repo_dir.join(&curr_name),
periphery_config().repo_dir.join(&new_name),
)
.await;
let msg = match renamed {
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))
}
}
//
impl Resolve<DeleteRepo> for State {
#[instrument(name = "DeleteRepo", skip(self))]
async fn resolve(
@@ -214,14 +238,15 @@ impl Resolve<DeleteRepo> for State {
DeleteRepo { name }: DeleteRepo,
_: (),
) -> anyhow::Result<Log> {
let name = to_komodo_name(&name);
let deleted = std::fs::remove_dir_all(
periphery_config().repo_dir.join(&name),
);
// If using custom clone path, it will be passed by core instead of name.
// So the join will resolve to just the absolute path.
let deleted =
fs::remove_dir_all(periphery_config().repo_dir.join(&name))
.await;
let msg = match deleted {
Ok(_) => format!("deleted repo {name}"),
Err(_) => format!("no repo at {name} to delete"),
Ok(_) => format!("Deleted Repo {name}"),
Err(_) => format!("No Repo at {name} to delete"),
};
Ok(Log::simple("delete repo", msg))
Ok(Log::simple("Delete Repo on Host", msg))
}
}
+1
View File
@@ -66,6 +66,7 @@ pub enum PeripheryRequest {
// Repo (Write)
CloneRepo(CloneRepo),
PullRepo(PullRepo),
RenameRepo(RenameRepo),
DeleteRepo(DeleteRepo),
// Build
+5 -3
View File
@@ -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.
+14 -2
View File
@@ -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("")
}
+1 -1
View File
@@ -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, ())
+2
View File
@@ -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
+32 -1
View File
@@ -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?:
```
+29 -1
View File
@@ -6,7 +6,7 @@ use typeshare::typeshare;
use crate::entities::update::Update;
use super::KomodoExecuteRequest;
use super::{BatchExecutionResponse, KomodoExecuteRequest};
/// Runs the target Action. Response: [Update]
#[typeshare]
@@ -26,3 +26,31 @@ pub struct RunAction {
/// Id or name
pub action: String,
}
/// Runs multiple Actions in parallel that match pattern. Response: [BatchExecutionResult]
#[typeshare]
#[derive(
Debug,
Clone,
PartialEq,
Serialize,
Deserialize,
Request,
EmptyTraits,
Parser,
)]
#[empty_traits(KomodoExecuteRequest)]
#[response(BatchExecutionResponse)]
pub struct BatchRunAction {
/// Id or name or wildcard pattern or regex.
/// Supports multiline and comma delineated combinations of the above.
///
/// Example:
/// ```
/// # match all foo-* actions
/// foo-*
/// # add some more
/// extra-action-1, extra-action-2
/// ```
pub pattern: String,
}
+31 -1
View File
@@ -6,7 +6,7 @@ use typeshare::typeshare;
use crate::entities::update::Update;
use super::KomodoExecuteRequest;
use super::{BatchExecutionResponse, KomodoExecuteRequest};
//
@@ -36,6 +36,36 @@ pub struct RunBuild {
//
/// Runs multiple builds in parallel that match pattern. Response: [BatchExecutionResult].
#[typeshare]
#[derive(
Debug,
Clone,
PartialEq,
Serialize,
Deserialize,
Request,
EmptyTraits,
Parser,
)]
#[empty_traits(KomodoExecuteRequest)]
#[response(BatchExecutionResponse)]
pub struct BatchRunBuild {
/// Id or name or wildcard pattern or regex.
/// Supports multiline and comma delineated combinations of the above.
///
/// Example:
/// ```
/// # match all foo-* builds
/// foo-*
/// # add some more
/// extra-build-1, extra-build-2
/// ```
pub pattern: String,
}
//
/// Cancels the target build.
/// Only does anything if the build is `building` when called.
/// Response: [Update]
+61 -1
View File
@@ -6,7 +6,7 @@ use typeshare::typeshare;
use crate::entities::{update::Update, TerminationSignal};
use super::KomodoExecuteRequest;
use super::{BatchExecutionResponse, KomodoExecuteRequest};
/// Deploys the container for the target deployment. Response: [Update].
///
@@ -41,6 +41,36 @@ pub struct Deploy {
//
/// Deploys multiple Deployments in parallel that match pattern. Response: [BatchExecutionResult].
#[typeshare]
#[derive(
Serialize,
Deserialize,
Debug,
Clone,
PartialEq,
Request,
EmptyTraits,
Parser,
)]
#[empty_traits(KomodoExecuteRequest)]
#[response(BatchExecutionResponse)]
pub struct BatchDeploy {
/// Id or name or wildcard pattern or regex.
/// Supports multiline and comma delineated combinations of the above.
///
/// Example:
/// ```
/// # match all foo-* deployments
/// foo-*
/// # add some more
/// extra-deployment-1, extra-deployment-2
/// ```
pub pattern: String,
}
//
/// Starts the container for the target deployment. Response: [Update]
///
/// 1. Runs `docker start ${container_name}`.
@@ -187,3 +217,33 @@ pub struct DestroyDeployment {
/// Override the default termination max time.
pub time: Option<i32>,
}
//
/// Destroys multiple Deployments in parallel that match pattern. Response: [BatchExecutionResult].
#[typeshare]
#[derive(
Serialize,
Deserialize,
Debug,
Clone,
PartialEq,
Request,
EmptyTraits,
Parser,
)]
#[empty_traits(KomodoExecuteRequest)]
#[response(BatchExecutionResponse)]
pub struct BatchDestroyDeployment {
/// Id or name or wildcard pattern or regex.
/// Supports multiline and comma delineated combinations of the above.
///
/// Example:
/// ```
/// # match all foo-* deployments
/// foo-*
/// # add some more
/// extra-deployment-1, extra-deployment-2
/// ```
pub pattern: String,
}
+43 -1
View File
@@ -27,7 +27,7 @@ pub use sync::*;
use crate::{
api::write::CommitSync,
entities::{NoData, I64},
entities::{update::Update, NoData, _Serror, I64},
};
pub trait KomodoExecuteRequest: HasResponse {}
@@ -59,27 +59,35 @@ pub enum Execution {
// ACTION
RunAction(RunAction),
BatchRunAction(BatchRunAction),
// PROCEDURE
RunProcedure(RunProcedure),
BatchRunProcedure(BatchRunProcedure),
// BUILD
RunBuild(RunBuild),
BatchRunBuild(BatchRunBuild),
CancelBuild(CancelBuild),
// DEPLOYMENT
Deploy(Deploy),
BatchDeploy(BatchDeploy),
StartDeployment(StartDeployment),
RestartDeployment(RestartDeployment),
PauseDeployment(PauseDeployment),
UnpauseDeployment(UnpauseDeployment),
StopDeployment(StopDeployment),
DestroyDeployment(DestroyDeployment),
BatchDestroyDeployment(BatchDestroyDeployment),
// REPO
CloneRepo(CloneRepo),
BatchCloneRepo(BatchCloneRepo),
PullRepo(PullRepo),
BatchPullRepo(BatchPullRepo),
BuildRepo(BuildRepo),
BatchBuildRepo(BatchBuildRepo),
CancelRepoBuild(CancelRepoBuild),
// SERVER (Container)
@@ -113,13 +121,16 @@ pub enum Execution {
// STACK
DeployStack(DeployStack),
BatchDeployStack(BatchDeployStack),
DeployStackIfChanged(DeployStackIfChanged),
BatchDeployStackIfChanged(BatchDeployStackIfChanged),
StartStack(StartStack),
RestartStack(RestartStack),
PauseStack(PauseStack),
UnpauseStack(UnpauseStack),
StopStack(StopStack),
DestroyStack(DestroyStack),
BatchDestroyStack(BatchDestroyStack),
// SLEEP
Sleep(Sleep),
@@ -131,3 +142,34 @@ pub struct Sleep {
#[serde(default)]
pub duration_ms: I64,
}
#[typeshare]
pub type BatchExecutionResponse = Vec<BatchExecutionResponseItem>;
#[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "status", content = "data")]
pub enum BatchExecutionResponseItem {
Ok(Update),
Err(BatchExecutionResponseItemErr),
}
impl From<Result<Update, BatchExecutionResponseItemErr>>
for BatchExecutionResponseItem
{
fn from(
value: Result<Update, BatchExecutionResponseItemErr>,
) -> Self {
match value {
Ok(update) => Self::Ok(update),
Err(e) => Self::Err(e),
}
}
}
#[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BatchExecutionResponseItemErr {
pub name: String,
pub error: _Serror,
}
+29 -1
View File
@@ -6,7 +6,7 @@ use typeshare::typeshare;
use crate::entities::update::Update;
use super::KomodoExecuteRequest;
use super::{BatchExecutionResponse, KomodoExecuteRequest};
/// Runs the target Procedure. Response: [Update]
#[typeshare]
@@ -26,3 +26,31 @@ pub struct RunProcedure {
/// Id or name
pub procedure: String,
}
/// Runs multiple Procedures in parallel that match pattern. Response: [BatchExecutionResult].
#[typeshare]
#[derive(
Debug,
Clone,
PartialEq,
Serialize,
Deserialize,
Request,
EmptyTraits,
Parser,
)]
#[empty_traits(KomodoExecuteRequest)]
#[response(BatchExecutionResponse)]
pub struct BatchRunProcedure {
/// Id or name or wildcard pattern or regex.
/// Supports multiline and comma delineated combinations of the above.
///
/// Example:
/// ```
/// # match all foo-* procedures
/// foo-*
/// # add some more
/// extra-procedure-1, extra-procedure-2
/// ```
pub pattern: String,
}
+91 -1
View File
@@ -6,7 +6,7 @@ use typeshare::typeshare;
use crate::entities::update::Update;
use super::KomodoExecuteRequest;
use super::{BatchExecutionResponse, KomodoExecuteRequest};
//
@@ -39,6 +39,36 @@ pub struct CloneRepo {
//
/// Clones multiple Repos in parallel that match pattern. Response: [BatchExecutionResult].
#[typeshare]
#[derive(
Debug,
Clone,
PartialEq,
Serialize,
Deserialize,
Request,
EmptyTraits,
Parser,
)]
#[empty_traits(KomodoExecuteRequest)]
#[response(BatchExecutionResponse)]
pub struct BatchCloneRepo {
/// Id or name or wildcard pattern or regex.
/// Supports multiline and comma delineated combinations of the above.
///
/// Example:
/// ```
/// # match all foo-* repos
/// foo-*
/// # add some more
/// extra-repo-1, extra-repo-2
/// ```
pub pattern: String,
}
//
/// Pulls the target repo. Response: [Update].
///
/// Note. Repo must have server attached at `server_id`.
@@ -65,6 +95,36 @@ pub struct PullRepo {
//
/// Pulls multiple Repos in parallel that match pattern. Response: [BatchExecutionResult].
#[typeshare]
#[derive(
Debug,
Clone,
PartialEq,
Serialize,
Deserialize,
Request,
EmptyTraits,
Parser,
)]
#[empty_traits(KomodoExecuteRequest)]
#[response(BatchExecutionResponse)]
pub struct BatchPullRepo {
/// Id or name or wildcard pattern or regex.
/// Supports multiline and comma delineated combinations of the above.
///
/// Example:
/// ```
/// # match all foo-* repos
/// foo-*
/// # add some more
/// extra-repo-1, extra-repo-2
/// ```
pub pattern: String,
}
//
/// Builds the target repo, using the attached builder. Response: [Update].
///
/// Note. Repo must have builder attached at `builder_id`.
@@ -95,6 +155,36 @@ pub struct BuildRepo {
//
/// Builds multiple Repos in parallel that match pattern. Response: [BatchExecutionResult].
#[typeshare]
#[derive(
Debug,
Clone,
PartialEq,
Serialize,
Deserialize,
Request,
EmptyTraits,
Parser,
)]
#[empty_traits(KomodoExecuteRequest)]
#[response(BatchExecutionResponse)]
pub struct BatchBuildRepo {
/// Id or name or wildcard pattern or regex.
/// Supports multiline and comma delineated combinations of the above.
///
/// Example:
/// ```
/// # match all foo-* repos
/// foo-*
/// # add some more
/// extra-repo-1, extra-repo-2
/// ```
pub pattern: String,
}
//
/// Cancels the target repo build.
/// Only does anything if the repo build is `building` when called.
/// Response: [Update]
+93 -1
View File
@@ -6,7 +6,7 @@ use typeshare::typeshare;
use crate::entities::update::Update;
use super::KomodoExecuteRequest;
use super::{BatchExecutionResponse, KomodoExecuteRequest};
/// Deploys the target stack. `docker compose up`. Response: [Update]
#[typeshare]
@@ -30,6 +30,38 @@ pub struct DeployStack {
pub stop_time: Option<i32>,
}
//
/// Deploys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResult].
#[typeshare]
#[derive(
Serialize,
Deserialize,
Debug,
Clone,
PartialEq,
Request,
EmptyTraits,
Parser,
)]
#[empty_traits(KomodoExecuteRequest)]
#[response(BatchExecutionResponse)]
pub struct BatchDeployStack {
/// Id or name or wildcard pattern or regex.
/// Supports multiline and comma delineated combinations of the above.
///
/// Example:
/// ```
/// # match all foo-* stacks
/// foo-*
/// # add some more
/// extra-stack-1, extra-stack-2
/// ```
pub pattern: String,
}
//
/// Checks deployed contents vs latest contents,
/// and only if any changes found
/// will `docker compose up`. Response: [Update]
@@ -56,6 +88,36 @@ pub struct DeployStackIfChanged {
//
/// Deploys multiple Stacks if changed in parallel that match pattern. Response: [BatchExecutionResult].
#[typeshare]
#[derive(
Serialize,
Deserialize,
Debug,
Clone,
PartialEq,
Request,
EmptyTraits,
Parser,
)]
#[empty_traits(KomodoExecuteRequest)]
#[response(BatchExecutionResponse)]
pub struct BatchDeployStackIfChanged {
/// Id or name or wildcard pattern or regex.
/// Supports multiline and comma delineated combinations of the above.
///
/// Example:
/// ```
/// # match all foo-* stacks
/// foo-*
/// # add some more
/// extra-stack-1, extra-stack-2
/// ```
pub pattern: String,
}
//
/// Starts the target stack. `docker compose start`. Response: [Update]
#[typeshare]
#[derive(
@@ -198,3 +260,33 @@ pub struct DestroyStack {
/// Override the default termination max time.
pub stop_time: Option<i32>,
}
//
/// Destroys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResult].
#[typeshare]
#[derive(
Serialize,
Deserialize,
Debug,
Clone,
PartialEq,
Request,
EmptyTraits,
Parser,
)]
#[empty_traits(KomodoExecuteRequest)]
#[response(BatchExecutionResponse)]
pub struct BatchDestroyStack {
/// Id or name or wildcard pattern or regex.
/// Supports multiline and comma delineated combinations of the above.
///d
/// Example:
/// ```
/// # match all foo-* stacks
/// foo-*
/// # add some more
/// extra-stack-1, extra-stack-2
/// ```
pub pattern: String,
}
+8 -8
View File
@@ -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
+19
View File
@@ -5,6 +5,7 @@ use typeshare::typeshare;
use crate::entities::{
action::{Action, _PartialActionConfig},
update::Update,
NoData,
};
@@ -23,6 +24,7 @@ pub struct CreateAction {
/// The name given to newly created action.
pub name: String,
/// Optional partial config to initialize the action with.
#[serde(default)]
pub config: _PartialActionConfig,
}
@@ -81,6 +83,23 @@ pub struct UpdateAction {
pub config: _PartialActionConfig,
}
//
/// Rename the Action at id to the given name.
/// Response: [Update].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
)]
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
pub struct RenameAction {
/// The id or name of the Action to rename.
pub id: String,
/// The new name.
pub name: String,
}
/// Create a webhook on the github action attached to the Action resource.
/// passed in request. Response: [CreateActionWebhookResponse]
#[typeshare]
+22 -1
View File
@@ -3,7 +3,10 @@ use resolver_api::derive::Request;
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
use crate::entities::alerter::{Alerter, _PartialAlerterConfig};
use crate::entities::{
alerter::{Alerter, _PartialAlerterConfig},
update::Update,
};
use super::KomodoWriteRequest;
@@ -20,6 +23,7 @@ pub struct CreateAlerter {
/// The name given to newly created alerter.
pub name: String,
/// Optional partial config to initialize the alerter with.
#[serde(default)]
pub config: _PartialAlerterConfig,
}
@@ -75,3 +79,20 @@ pub struct UpdateAlerter {
/// The partial config update to apply.
pub config: _PartialAlerterConfig,
}
//
/// Rename the Alerter at id to the given name.
/// Response: [Update].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
)]
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
pub struct RenameAlerter {
/// The id or name of the Alerter to rename.
pub id: String,
/// The new name.
pub name: String,
}
+19
View File
@@ -5,6 +5,7 @@ use typeshare::typeshare;
use crate::entities::{
build::{Build, _PartialBuildConfig},
update::Update,
NoData,
};
@@ -23,6 +24,7 @@ pub struct CreateBuild {
/// The name given to newly created build.
pub name: String,
/// Optional partial config to initialize the build with.
#[serde(default)]
pub config: _PartialBuildConfig,
}
@@ -83,6 +85,23 @@ pub struct UpdateBuild {
//
/// Rename the Build at id to the given name.
/// Response: [Update].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
)]
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
pub struct RenameBuild {
/// The id or name of the Build to rename.
pub id: String,
/// The new name.
pub name: String,
}
//
/// Trigger a refresh of the cached latest hash and message.
#[typeshare]
#[derive(
+22 -1
View File
@@ -3,7 +3,10 @@ use resolver_api::derive::Request;
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
use crate::entities::builder::{Builder, PartialBuilderConfig};
use crate::entities::{
builder::{Builder, PartialBuilderConfig},
update::Update,
};
use super::KomodoWriteRequest;
@@ -20,6 +23,7 @@ pub struct CreateBuilder {
/// The name given to newly created builder.
pub name: String,
/// Optional partial config to initialize the builder with.
#[serde(default)]
pub config: PartialBuilderConfig,
}
@@ -77,3 +81,20 @@ pub struct UpdateBuilder {
/// The partial config update to apply.
pub config: PartialBuilderConfig,
}
//
/// Rename the Builder at id to the given name.
/// Response: [Update].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
)]
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
pub struct RenameBuilder {
/// The id or name of the Builder to rename.
pub id: String,
/// The new name.
pub name: String,
}
@@ -23,6 +23,7 @@ pub struct CreateDeployment {
/// The name given to newly created deployment.
pub name: String,
/// Optional partial config to initialize the deployment with.
#[serde(default)]
pub config: _PartialDeploymentConfig,
}
+21 -2
View File
@@ -3,8 +3,9 @@ use resolver_api::derive::Request;
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
use crate::entities::procedure::{
Procedure, _PartialProcedureConfig,
use crate::entities::{
procedure::{Procedure, _PartialProcedureConfig},
update::Update,
};
use super::KomodoWriteRequest;
@@ -22,6 +23,7 @@ pub struct CreateProcedure {
/// The name given to newly created build.
pub name: String,
/// Optional partial config to initialize the procedure with.
#[serde(default)]
pub config: _PartialProcedureConfig,
}
@@ -91,3 +93,20 @@ pub struct UpdateProcedure {
#[typeshare]
pub type UpdateProcedureResponse = Procedure;
//
/// Rename the Procedure at id to the given name.
/// Response: [Update].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
)]
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
pub struct RenameProcedure {
/// The id or name of the Procedure to rename.
pub id: String,
/// The new name.
pub name: String,
}
+19
View File
@@ -5,6 +5,7 @@ use typeshare::typeshare;
use crate::entities::{
repo::{Repo, _PartialRepoConfig},
update::Update,
NoData,
};
@@ -23,6 +24,7 @@ pub struct CreateRepo {
/// The name given to newly created repo.
pub name: String,
/// Optional partial config to initialize the repo with.
#[serde(default)]
pub config: _PartialRepoConfig,
}
@@ -86,6 +88,23 @@ pub struct UpdateRepo {
//
/// Rename the Repo at id to the given name.
/// Response: [Update].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
)]
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
pub struct RenameRepo {
/// The id or name of the Repo to rename.
pub id: String,
/// The new name.
pub name: String,
}
//
/// Trigger a refresh of the cached latest hash and message.
#[typeshare]
#[derive(
+9 -5
View File
@@ -23,6 +23,7 @@ pub struct CreateServer {
/// The name given to newly created server.
pub name: String,
/// Optional partial config to initialize the server with.
#[serde(default)]
pub config: _PartialServerConfig,
}
@@ -58,7 +59,7 @@ pub struct DeleteServer {
#[empty_traits(KomodoWriteRequest)]
#[response(Server)]
pub struct UpdateServer {
/// The id of the server to update.
/// The id or name of the server to update.
pub id: String,
/// The partial config update to apply.
pub config: _PartialServerConfig,
@@ -66,7 +67,8 @@ pub struct UpdateServer {
//
/// Rename the server at id to the given name. Response: [Update].
/// Rename an Server to the given name.
/// Response: [Update].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
@@ -74,7 +76,7 @@ pub struct UpdateServer {
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
pub struct RenameServer {
/// The id of the server to rename.
/// The id or name of the Server to rename.
pub id: String,
/// The new name.
pub name: String,
@@ -83,7 +85,9 @@ pub struct RenameServer {
//
/// Create a docker network on the server.
/// Respone: [Update]
/// Response: [Update]
///
/// `docker network create {name}`
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
@@ -91,7 +95,7 @@ pub struct RenameServer {
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
pub struct CreateNetwork {
/// Id or name
/// Server Id or name
pub server: String,
/// The name of the network to create.
pub name: String,
@@ -3,8 +3,9 @@ use resolver_api::derive::Request;
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
use crate::entities::server_template::{
PartialServerTemplateConfig, ServerTemplate,
use crate::entities::{
server_template::{PartialServerTemplateConfig, ServerTemplate},
update::Update,
};
use super::KomodoWriteRequest;
@@ -22,6 +23,7 @@ pub struct CreateServerTemplate {
/// The name given to newly created server template.
pub name: String,
/// Optional partial config to initialize the server template with.
#[serde(default)]
pub config: PartialServerTemplateConfig,
}
@@ -79,3 +81,20 @@ pub struct UpdateServerTemplate {
/// The partial config update to apply.
pub config: PartialServerTemplateConfig,
}
//
/// Rename the ServerTemplate at id to the given name.
/// Response: [Update].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
)]
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
pub struct RenameServerTemplate {
/// The id or name of the ServerTemplate to rename.
pub id: String,
/// The new name.
pub name: String,
}
+1
View File
@@ -24,6 +24,7 @@ pub struct CreateStack {
/// The name given to newly created stack.
pub name: String,
/// Optional partial config to initialize the stack with.
#[serde(default)]
pub config: _PartialStackConfig,
}
+18
View File
@@ -25,6 +25,7 @@ pub struct CreateResourceSync {
/// The name given to newly created sync.
pub name: String,
/// Optional partial config to initialize the sync with.
#[serde(default)]
pub config: _PartialResourceSyncConfig,
}
@@ -85,6 +86,23 @@ pub struct UpdateResourceSync {
//
/// Rename the ResourceSync at id to the given name.
/// Response: [Update].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
)]
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
pub struct RenameResourceSync {
/// The id or name of the ResourceSync to rename.
pub id: String,
/// The new name.
pub name: String,
}
//
/// Trigger a refresh of the computed diff logs for view. Response: [ResourceSync]
#[typeshare]
#[derive(
+1 -1
View File
@@ -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]
+18
View File
@@ -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(),
}
}
}
+10 -2
View File
@@ -168,7 +168,7 @@ pub fn get_image_name(
pub fn to_komodo_name(name: &str) -> String {
name
.to_lowercase()
.replace([' ', '.'], "_")
.replace([' ', '.', ',', '\n'], "_")
.trim()
.to_string()
}
@@ -695,6 +695,7 @@ pub enum Operation {
// deployment
CreateDeployment,
UpdateDeployment,
RenameDeployment,
DeleteDeployment,
Deploy,
StartDeployment,
@@ -703,11 +704,11 @@ pub enum Operation {
UnpauseDeployment,
StopDeployment,
DestroyDeployment,
RenameDeployment,
// build
CreateBuild,
UpdateBuild,
RenameBuild,
DeleteBuild,
RunBuild,
CancelBuild,
@@ -715,6 +716,7 @@ pub enum Operation {
// repo
CreateRepo,
UpdateRepo,
RenameRepo,
DeleteRepo,
CloneRepo,
PullRepo,
@@ -724,34 +726,40 @@ pub enum Operation {
// procedure
CreateProcedure,
UpdateProcedure,
RenameProcedure,
DeleteProcedure,
RunProcedure,
// action
CreateAction,
UpdateAction,
RenameAction,
DeleteAction,
RunAction,
// builder
CreateBuilder,
UpdateBuilder,
RenameBuilder,
DeleteBuilder,
// alerter
CreateAlerter,
UpdateAlerter,
RenameAlerter,
DeleteAlerter,
// server template
CreateServerTemplate,
UpdateServerTemplate,
RenameServerTemplate,
DeleteServerTemplate,
LaunchServer,
// sync
CreateResourceSync,
UpdateResourceSync,
RenameResourceSync,
DeleteResourceSync,
WriteSyncContents,
CommitSync,
+5 -3
View File
@@ -277,12 +277,14 @@ impl Default for RepoConfig {
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)]
pub struct RepoActionState {
/// Whether repo currently cloning
/// Whether Repo currently cloning on the attached Server
pub cloning: bool,
/// Whether repo currently pulling
/// Whether Repo currently pulling on the attached Server
pub pulling: bool,
/// Whether repo currently building, using the attached builder.
/// Whether Repo currently building using the attached Builder.
pub building: bool,
/// Whether Repo currently renaming.
pub renaming: bool,
}
#[typeshare]
+34 -29
View File
@@ -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:
+99 -17
View File
@@ -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
}
}

Some files were not shown because too many files have changed in this diff Show More