Compare commits
2 Commits
b91f63884a
...
1406a21785
Author | SHA1 | Date |
---|---|---|
Tom Alexander | 1406a21785 | |
Tom Alexander | 1612278bed |
|
@ -339,6 +339,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer",
|
"block-buffer",
|
||||||
"crypto-common",
|
"crypto-common",
|
||||||
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -567,6 +568,15 @@ version = "0.3.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hmac"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "home"
|
name = "home"
|
||||||
version = "0.5.9"
|
version = "0.5.9"
|
||||||
|
@ -1890,10 +1900,15 @@ name = "webhook_bridge"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
|
"base64 0.22.1",
|
||||||
|
"hmac",
|
||||||
|
"http-body-util",
|
||||||
"k8s-openapi",
|
"k8s-openapi",
|
||||||
"kube",
|
"kube",
|
||||||
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|
|
@ -20,12 +20,17 @@ include = [
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# default form, http1, json, matched-path, original-uri, query, tokio, tower-log, tracing
|
# default form, http1, json, matched-path, original-uri, query, tokio, tower-log, tracing
|
||||||
axum = { version = "0.7.5", default-features = false, features = ["tokio", "http1", "http2", "json"] }
|
axum = { version = "0.7.5", default-features = false, features = ["tokio", "http1", "http2", "json"] }
|
||||||
|
base64 = "0.22.1"
|
||||||
|
hmac = "0.12.1"
|
||||||
|
http-body-util = "0.1.2"
|
||||||
k8s-openapi = { version = "0.22.0", default-features = false, features = ["v1_30"] }
|
k8s-openapi = { version = "0.22.0", default-features = false, features = ["v1_30"] }
|
||||||
# default client, config, rustls-tls
|
# default client, config, rustls-tls
|
||||||
kube = { version = "0.92.1", default-features = false, features = ["client", "config", "rustls-tls", "derive", "runtime"] }
|
kube = { version = "0.92.1", default-features = false, features = ["client", "config", "rustls-tls", "derive", "runtime"] }
|
||||||
|
schemars = "0.8.21"
|
||||||
serde = { version = "1.0.204", features = ["derive"] }
|
serde = { version = "1.0.204", features = ["derive"] }
|
||||||
# default std
|
# default std
|
||||||
serde_json = { version = "1.0.120", default-features = false, features = ["std"] }
|
serde_json = { version = "1.0.120", default-features = false, features = ["std"] }
|
||||||
|
sha2 = "0.10.8"
|
||||||
tokio = { version = "1.38.0", default-features = false, features = ["macros", "process", "rt-multi-thread", "signal"] }
|
tokio = { version = "1.38.0", default-features = false, features = ["macros", "process", "rt-multi-thread", "signal"] }
|
||||||
tower-http = { version = "0.5.2", default-features = false, features = ["trace", "timeout"] }
|
tower-http = { version = "0.5.2", default-features = false, features = ["trace", "timeout"] }
|
||||||
# default attributes, std, tracing-attributes
|
# default attributes, std, tracing-attributes
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
{
|
||||||
|
"apiVersion": "tekton.dev/v1beta1",
|
||||||
|
"kind": "PipelineRun",
|
||||||
|
"metadata": {
|
||||||
|
"name": "minimal-test",
|
||||||
|
"namespace": "lighthouse"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"pipelineSpec": {
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"name": "echo-variable",
|
||||||
|
"taskSpec": {
|
||||||
|
"metadata": {},
|
||||||
|
"stepTemplate": {
|
||||||
|
"image": "alpine:3.18",
|
||||||
|
"name": "",
|
||||||
|
"resources": {
|
||||||
|
"requests": {
|
||||||
|
"cpu": "10m",
|
||||||
|
"memory": "600Mi"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"image": "alpine:3.18",
|
||||||
|
"script": "#!/usr/bin/env sh\necho \"The variable: $(params.LOREM)\"\n"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "LOREM",
|
||||||
|
"value": "$(tasks.set-variable.results.ipsum)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "set-variable",
|
||||||
|
"taskSpec": {
|
||||||
|
"metadata": {},
|
||||||
|
"stepTemplate": {
|
||||||
|
"image": "alpine:3.18",
|
||||||
|
"name": "",
|
||||||
|
"resources": {
|
||||||
|
"requests": {
|
||||||
|
"cpu": "10m",
|
||||||
|
"memory": "600Mi"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"name": "ipsum"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"image": "alpine:3.18",
|
||||||
|
"script": "#!/usr/bin/env sh\necho -n \"dolar\" > \"$(results.ipsum.path)\"\n"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"timeout": "240h0m0s"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
apiVersion: tekton.dev/v1beta1
|
||||||
|
kind: PipelineRun
|
||||||
|
metadata:
|
||||||
|
name: minimal-test
|
||||||
|
namespace: lighthouse
|
||||||
|
spec:
|
||||||
|
pipelineSpec:
|
||||||
|
tasks:
|
||||||
|
- name: echo-variable
|
||||||
|
taskSpec:
|
||||||
|
metadata: {}
|
||||||
|
stepTemplate:
|
||||||
|
image: alpine:3.18
|
||||||
|
name: ""
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 10m
|
||||||
|
memory: 600Mi
|
||||||
|
steps:
|
||||||
|
- image: alpine:3.18
|
||||||
|
script: |
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
echo "The variable: $(params.LOREM)"
|
||||||
|
params:
|
||||||
|
- name: LOREM
|
||||||
|
value: $(tasks.set-variable.results.ipsum)
|
||||||
|
- name: set-variable
|
||||||
|
taskSpec:
|
||||||
|
metadata: {}
|
||||||
|
stepTemplate:
|
||||||
|
image: alpine:3.18
|
||||||
|
name: ""
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 10m
|
||||||
|
memory: 600Mi
|
||||||
|
results:
|
||||||
|
- name: ipsum
|
||||||
|
steps:
|
||||||
|
- image: alpine:3.18
|
||||||
|
script: |
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
echo -n "dolar" > "$(results.ipsum.path)"
|
||||||
|
timeout: 240h0m0s
|
|
@ -0,0 +1,24 @@
|
||||||
|
use kube::CustomResource;
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
/// A single execution of a Pipeline.
|
||||||
|
#[derive(CustomResource, Serialize, Deserialize, Clone, Debug, JsonSchema)]
|
||||||
|
#[kube(
|
||||||
|
group = "tekton.dev",
|
||||||
|
version = "v1beta1",
|
||||||
|
kind = "PipelineRun",
|
||||||
|
singular = "pipelinerun",
|
||||||
|
plural = "pipelineruns"
|
||||||
|
)]
|
||||||
|
#[kube(namespaced)]
|
||||||
|
pub struct PipelineRunSpec {
|
||||||
|
/// Contents of the Pipeline
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub pipelineSpec: Option<Value>,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub timeout: Option<String>,
|
||||||
|
}
|
23
src/main.rs
23
src/main.rs
|
@ -2,10 +2,13 @@
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
|
use axum::middleware;
|
||||||
use axum::routing::get;
|
use axum::routing::get;
|
||||||
use axum::routing::post;
|
use axum::routing::post;
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
|
use kube::api::PostParams;
|
||||||
|
use kube::Api;
|
||||||
use kube::Client;
|
use kube::Client;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tokio::signal;
|
use tokio::signal;
|
||||||
|
@ -14,11 +17,17 @@ use tower_http::trace::TraceLayer;
|
||||||
use tracing_subscriber::layer::SubscriberExt;
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
use tracing_subscriber::util::SubscriberInitExt;
|
use tracing_subscriber::util::SubscriberInitExt;
|
||||||
|
|
||||||
|
use self::crd_pipeline_run::PipelineRun;
|
||||||
use self::webhook::hook;
|
use self::webhook::hook;
|
||||||
|
use self::webhook::verify_signature;
|
||||||
|
use kube::CustomResourceExt;
|
||||||
|
|
||||||
|
mod crd_pipeline_run;
|
||||||
mod hook_push;
|
mod hook_push;
|
||||||
mod webhook;
|
mod webhook;
|
||||||
|
|
||||||
|
const EXAMPLE_PIPELINE_RUN: &'static str = include_str!("../example_pipeline_run.json");
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
tracing_subscriber::registry()
|
tracing_subscriber::registry()
|
||||||
|
@ -34,16 +43,26 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
.await
|
.await
|
||||||
.expect("Set KUBECONFIG to a valid kubernetes config.");
|
.expect("Set KUBECONFIG to a valid kubernetes config.");
|
||||||
|
|
||||||
|
let jobs: Api<PipelineRun> = Api::namespaced(kubernetes_client, "lighthouse");
|
||||||
|
// let jobs: Api<PipelineRun> = Api::default_namespaced(kubernetes_client);
|
||||||
|
tracing::info!("Using crd: {}", serde_json::to_string(&PipelineRun::crd())?);
|
||||||
|
|
||||||
|
let test_run: PipelineRun = serde_json::from_str(EXAMPLE_PIPELINE_RUN)?;
|
||||||
|
|
||||||
|
let pp = PostParams::default();
|
||||||
|
let created_run = jobs.create(&pp, &test_run).await?;
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/health", get(health))
|
|
||||||
.route("/hook", post(hook))
|
.route("/hook", post(hook))
|
||||||
|
.layer(middleware::from_fn(verify_signature))
|
||||||
|
.route("/health", get(health))
|
||||||
.layer((
|
.layer((
|
||||||
TraceLayer::new_for_http(),
|
TraceLayer::new_for_http(),
|
||||||
// Add a timeout layer so graceful shutdown can't wait forever.
|
// Add a timeout layer so graceful shutdown can't wait forever.
|
||||||
TimeoutLayer::new(Duration::from_secs(600)),
|
TimeoutLayer::new(Duration::from_secs(600)),
|
||||||
));
|
));
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?;
|
let listener = tokio::net::TcpListener::bind("0.0.0.0:9988").await?;
|
||||||
tracing::info!("listening on {}", listener.local_addr().unwrap());
|
tracing::info!("listening on {}", listener.local_addr().unwrap());
|
||||||
axum::serve(listener, app)
|
axum::serve(listener, app)
|
||||||
.with_graceful_shutdown(shutdown_signal())
|
.with_graceful_shutdown(shutdown_signal())
|
||||||
|
|
|
@ -1,17 +1,29 @@
|
||||||
|
use std::future::Future;
|
||||||
|
|
||||||
use axum::async_trait;
|
use axum::async_trait;
|
||||||
|
use axum::body::Body;
|
||||||
|
use axum::body::Bytes;
|
||||||
use axum::extract::FromRequest;
|
use axum::extract::FromRequest;
|
||||||
use axum::extract::Request;
|
use axum::extract::Request;
|
||||||
use axum::http::HeaderMap;
|
use axum::http::HeaderMap;
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
|
use axum::middleware::Next;
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum::response::Response;
|
use axum::response::Response;
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
use axum::RequestExt;
|
use axum::RequestExt;
|
||||||
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
|
use hmac::Hmac;
|
||||||
|
use hmac::Mac;
|
||||||
|
use http_body_util::BodyExt;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use sha2::Sha256;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
use crate::hook_push::HookPush;
|
use crate::hook_push::HookPush;
|
||||||
|
|
||||||
|
type HmacSha256 = Hmac<Sha256>;
|
||||||
|
|
||||||
pub(crate) async fn hook(
|
pub(crate) async fn hook(
|
||||||
_headers: HeaderMap,
|
_headers: HeaderMap,
|
||||||
payload: HookRequest,
|
payload: HookRequest,
|
||||||
|
@ -72,3 +84,68 @@ pub(crate) struct HookResponse {
|
||||||
ok: bool,
|
ok: bool,
|
||||||
message: Option<String>,
|
message: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn verify_signature(
|
||||||
|
request: Request,
|
||||||
|
next: Next,
|
||||||
|
) -> Result<impl IntoResponse, Response> {
|
||||||
|
let signature = request
|
||||||
|
.headers()
|
||||||
|
.get("X-Gitea-Signature")
|
||||||
|
.ok_or(StatusCode::BAD_REQUEST.into_response())?;
|
||||||
|
let signature = signature
|
||||||
|
.to_str()
|
||||||
|
.map_err(|_| StatusCode::BAD_REQUEST.into_response())?;
|
||||||
|
let signature = hex_to_bytes(signature).ok_or(StatusCode::BAD_REQUEST.into_response())?;
|
||||||
|
let secret = std::env::var("WEBHOOK_BRIDGE_HMAC_SECRET")
|
||||||
|
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response())?;
|
||||||
|
|
||||||
|
let request =
|
||||||
|
inspect_request_body(request, move |body| check_hash(body, secret, signature)).await?;
|
||||||
|
|
||||||
|
Ok(next.run(request).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn inspect_request_body<F, Fut>(request: Request, inspector: F) -> Result<Request, Response>
|
||||||
|
where
|
||||||
|
F: FnOnce(Bytes) -> Fut,
|
||||||
|
Fut: Future<Output = Result<Bytes, Response>>,
|
||||||
|
{
|
||||||
|
let (parts, body) = request.into_parts();
|
||||||
|
|
||||||
|
let bytes = body
|
||||||
|
.collect()
|
||||||
|
.await
|
||||||
|
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response())?
|
||||||
|
.to_bytes();
|
||||||
|
|
||||||
|
let bytes = inspector(bytes).await?;
|
||||||
|
|
||||||
|
Ok(Request::from_parts(parts, Body::from(bytes)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_hash(body: Bytes, secret: String, signature: Vec<u8>) -> Result<Bytes, Response> {
|
||||||
|
tracing::info!("Checking signature {:02x?}", signature.as_slice());
|
||||||
|
tracing::info!("Using secret {:?}", secret);
|
||||||
|
tracing::info!("and body {}", general_purpose::STANDARD.encode(&body));
|
||||||
|
let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
|
||||||
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response())?;
|
||||||
|
mac.update(&body);
|
||||||
|
mac.verify_slice(&signature)
|
||||||
|
.map_err(|e| (StatusCode::UNAUTHORIZED, e.to_string()).into_response())?;
|
||||||
|
Ok(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hex_to_bytes(s: &str) -> Option<Vec<u8>> {
|
||||||
|
if s.len() % 2 == 0 {
|
||||||
|
(0..s.len())
|
||||||
|
.step_by(2)
|
||||||
|
.map(|i| {
|
||||||
|
s.get(i..i + 2)
|
||||||
|
.and_then(|sub| u8::from_str_radix(sub, 16).ok())
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue