Added webhook support

This commit is contained in:
2025-12-29 02:06:23 +00:00
parent fff7312d7d
commit d185d2a4d0
4 changed files with 265 additions and 27 deletions

169
Cargo.lock generated
View File

@@ -45,6 +45,21 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.20" version = "0.6.20"
@@ -333,6 +348,12 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "bumpalo"
version = "3.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.10.1" version = "1.10.1"
@@ -361,8 +382,13 @@ version = "0.4.41"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
dependencies = [ dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits", "num-traits",
"serde", "serde",
"wasm-bindgen",
"windows-link 0.1.3",
] ]
[[package]] [[package]]
@@ -943,6 +969,30 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "iana-time-zone"
version = "0.1.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "icu_collections" name = "icu_collections"
version = "2.0.0" version = "2.0.0"
@@ -1098,6 +1148,16 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "js-sys"
version = "0.3.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]] [[package]]
name = "json-patch" name = "json-patch"
version = "2.0.0" version = "2.0.0"
@@ -1800,11 +1860,12 @@ dependencies = [
[[package]] [[package]]
name = "secret-reader" name = "secret-reader"
version = "0.1.1" version = "0.2.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"askama", "askama",
"axum", "axum",
"chrono",
"clap", "clap",
"k8s-openapi", "k8s-openapi",
"kube", "kube",
@@ -2424,12 +2485,116 @@ dependencies = [
"wit-bindgen", "wit-bindgen",
] ]
[[package]]
name = "wasm-bindgen"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn 2.0.106",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
dependencies = [
"unicode-ident",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link 0.2.1",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]] [[package]]
name = "windows-link" name = "windows-link"
version = "0.1.3" version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link 0.2.1",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.52.0" version = "0.52.0"
@@ -2479,7 +2644,7 @@ version = "0.53.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
dependencies = [ dependencies = [
"windows-link", "windows-link 0.1.3",
"windows_aarch64_gnullvm 0.53.0", "windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0", "windows_aarch64_msvc 0.53.0",
"windows_i686_gnu 0.53.0", "windows_i686_gnu 0.53.0",

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "secret-reader" name = "secret-reader"
version = "0.1.1" version = "0.2.0"
edition = "2021" edition = "2024"
[dependencies] [dependencies]
axum = "0.7" axum = "0.7"
@@ -16,3 +16,4 @@ tracing = "0.1"
tracing-subscriber = "0.3" tracing-subscriber = "0.3"
anyhow = "1.0" anyhow = "1.0"
totp-rs = { version = "5.6", features = ["otpauth"] } totp-rs = { version = "5.6", features = ["otpauth"] }
chrono = "0.4"

View File

@@ -1,17 +1,19 @@
use anyhow::Result; use anyhow::Result;
use askama::Template; use askama::Template;
use axum::{ use axum::{
extract::{Query, State}, extract::{Json, Query, State},
http::StatusCode, http::StatusCode,
response::{Html, IntoResponse, Response}, response::{Html, IntoResponse, Response},
routing::get, routing::{get, post},
Router, Router,
}; };
use chrono;
use clap::Parser; use clap::Parser;
use k8s_openapi::api::core::v1::Secret; use k8s_openapi::api::core::v1::Secret;
use kube::{Api, Client}; use kube::{Api, Client};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::sync::Arc; use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use totp_rs::TOTP; use totp_rs::TOTP;
use tracing::{error, info}; use tracing::{error, info};
use tracing_subscriber; use tracing_subscriber;
@@ -27,6 +29,9 @@ struct Args {
#[arg(short, long, default_value = "default")] #[arg(short, long, default_value = "default")]
namespace: String, namespace: String,
#[arg(short = 'w', long, help = "Enable webhook endpoint at /webhook")]
webhook: bool,
} }
#[derive(Clone)] #[derive(Clone)]
@@ -34,12 +39,24 @@ struct AppState {
client: Client, client: Client,
secret_names: Vec<String>, secret_names: Vec<String>,
namespace: String, namespace: String,
webhook_secrets: Arc<RwLock<HashMap<String, WebhookSecret>>>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
struct WebhookSecret {
name: String,
fields: HashMap<String, String>,
#[serde(skip_deserializing)]
#[serde(default)]
received_at: String,
} }
#[derive(Serialize)] #[derive(Serialize)]
struct SecretData { struct SecretData {
name: String, name: String,
data: Vec<(String, String)>, data: Vec<(String, String)>,
#[serde(skip_serializing_if = "Option::is_none")]
received_at: Option<String>,
} }
#[derive(Template)] #[derive(Template)]
@@ -80,6 +97,7 @@ async fn read_secrets(state: &AppState) -> Result<Vec<SecretData>> {
result.push(SecretData { result.push(SecretData {
name: secret_name.clone(), name: secret_name.clone(),
data: data_pairs, data: data_pairs,
received_at: None,
}); });
} }
Err(e) => { Err(e) => {
@@ -87,6 +105,7 @@ async fn read_secrets(state: &AppState) -> Result<Vec<SecretData>> {
result.push(SecretData { result.push(SecretData {
name: secret_name.clone(), name: secret_name.clone(),
data: vec![("error".to_string(), format!("Failed to read: {}", e))], data: vec![("error".to_string(), format!("Failed to read: {}", e))],
received_at: None,
}); });
} }
} }
@@ -98,10 +117,40 @@ async fn read_secrets(state: &AppState) -> Result<Vec<SecretData>> {
async fn index_handler(State(state): State<Arc<AppState>>) -> impl IntoResponse { async fn index_handler(State(state): State<Arc<AppState>>) -> impl IntoResponse {
info!("Handling request, fetching secrets: {:?}", state.secret_names); info!("Handling request, fetching secrets: {:?}", state.secret_names);
match read_secrets(&state).await { let mut all_secrets = match read_secrets(&state).await {
Ok(secrets) => { Ok(secrets) => secrets,
Err(e) => {
error!("Failed to read secrets: {}", e);
let template = IndexTemplate { let template = IndexTemplate {
secrets, secrets: vec![],
error: Some(format!("Failed to read secrets: {}", e)),
};
return match template.render() {
Ok(html) => Html(html).into_response(),
Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Failed to read secrets").into_response(),
};
}
};
// Add webhook secrets
if let Ok(webhook_secrets) = state.webhook_secrets.read() {
for (_, webhook_secret) in webhook_secrets.iter() {
let mut data_pairs: Vec<(String, String)> = webhook_secret.fields.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
data_pairs.sort_by(|a, b| a.0.cmp(&b.0));
all_secrets.push(SecretData {
name: webhook_secret.name.clone(),
data: data_pairs,
received_at: Some(webhook_secret.received_at.clone()),
});
}
}
let template = IndexTemplate {
secrets: all_secrets,
error: None, error: None,
}; };
@@ -113,25 +162,32 @@ async fn index_handler(State(state): State<Arc<AppState>>) -> impl IntoResponse
} }
} }
} }
Err(e) => {
error!("Failed to read secrets: {}", e);
let template = IndexTemplate {
secrets: vec![],
error: Some(format!("Failed to read secrets: {}", e)),
};
match template.render() {
Ok(html) => Html(html).into_response(),
Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Failed to read secrets").into_response(),
}
}
}
}
async fn health_handler() -> impl IntoResponse { async fn health_handler() -> impl IntoResponse {
"OK" "OK"
} }
async fn webhook_handler(
State(state): State<Arc<AppState>>,
Json(payload): Json<WebhookSecret>,
) -> impl IntoResponse {
info!("Received webhook for secret: {}", payload.name);
let mut webhook_secret = payload;
webhook_secret.received_at = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string();
match state.webhook_secrets.write() {
Ok(mut secrets) => {
secrets.insert(webhook_secret.name.clone(), webhook_secret);
(StatusCode::OK, "Webhook received")
}
Err(e) => {
error!("Failed to write webhook secret: {}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "Failed to store webhook")
}
}
}
fn generate_totp_code(otpauth_url: &str) -> Option<String> { fn generate_totp_code(otpauth_url: &str) -> Option<String> {
// Try to parse the otpauth URL directly using totp-rs // Try to parse the otpauth URL directly using totp-rs
match TOTP::from_url(otpauth_url) { match TOTP::from_url(otpauth_url) {
@@ -220,13 +276,20 @@ async fn main() -> Result<()> {
client, client,
secret_names: args.secrets, secret_names: args.secrets,
namespace: args.namespace, namespace: args.namespace,
webhook_secrets: Arc::new(RwLock::new(HashMap::new())),
}); });
let app = Router::new() let mut app = Router::new()
.route("/", get(index_handler)) .route("/", get(index_handler))
.route("/health", get(health_handler)) .route("/health", get(health_handler))
.route("/secret", get(secret_handler)) .route("/secret", get(secret_handler));
.with_state(state);
if args.webhook {
info!("Webhook endpoint enabled at /webhook");
app = app.route("/webhook", post(webhook_handler));
}
let app = app.with_state(state.clone());
let addr = format!("0.0.0.0:{}", args.port); let addr = format!("0.0.0.0:{}", args.port);
info!("Server listening on {}", addr); info!("Server listening on {}", addr);

View File

@@ -90,6 +90,12 @@
color: #666; color: #666;
margin-left: 10px; margin-left: 10px;
} }
.received-at {
font-size: 11px;
color: #888;
margin-top: 5px;
font-style: italic;
}
.error { .error {
background: #ffdddd; background: #ffdddd;
color: #cc0000; color: #cc0000;
@@ -125,6 +131,9 @@
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
{% if let Some(received) = secret.received_at %}
<div class="received-at">Received: {{ received }}</div>
{% endif %}
</div> </div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}