Skip to content

Commit a4020cb

Browse files
committed
chore: support delete pipelinerun and optimize webhook handle and crypto utils
1 parent 6e9ee65 commit a4020cb

File tree

6 files changed

+40
-31
lines changed

6 files changed

+40
-31
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ resolver = "2"
88
[package]
99
name = "backend"
1010
description = "Backend API and services for StackClass"
11-
version = "0.41.1"
11+
version = "0.41.2"
1212
edition = "2024"
1313

1414
default-run = "stackclass-server"

openapi.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"license": {
77
"name": ""
88
},
9-
"version": "0.41.1"
9+
"version": "0.41.2"
1010
},
1111
"paths": {
1212
"/v1/courses": {

src/handler/webhook.rs

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ use gitea_client::types::Event;
1717
use std::sync::Arc;
1818
use uuid::Uuid;
1919

20-
use tracing::{error, info};
20+
use tracing::{debug, error, info};
2121

2222
use crate::{
2323
context::Context,
2424
errors::{ApiError, Result},
2525
repository::CourseRepository,
2626
request::event::PipelineEvent,
27-
service::{RepoService, StageService},
27+
service::{PipelineService, RepoService, StageService},
2828
utils::crypto,
2929
};
3030

@@ -52,51 +52,54 @@ pub async fn handle_tekton_webhook(
5252
State(ctx): State<Arc<Context>>,
5353
Json(event): Json<PipelineEvent>,
5454
) -> Result<impl IntoResponse> {
55-
info!("Received pipeline event: {:?}", event);
55+
debug!("Received pipeline event: {:?}", event);
56+
let PipelineEvent { name, status, repo, course, stage, secret, tasks } = &event;
5657

5758
// Verify HMAC signature to prevent request forgery
5859
let auth_secret = &ctx.config.auth_secret;
59-
let payload = format!("{}{}{}", event.repo, event.course, event.stage);
60-
let is_valid = crypto::hmac_sha256_verify(&payload, auth_secret, &event.secret)
61-
.map_err(|e| ApiError::InternalError(format!("Signature verification failed: {}", e)))?;
62-
63-
if !is_valid {
60+
let payload = format!("{}{}{}", repo, course, stage);
61+
if crypto::hmac_sha256_verify(&payload, auth_secret, secret)? {
62+
error!("Received pipeline event with invalid signature");
6463
return Err(ApiError::Unauthorized("Invalid signature".into()));
6564
}
6665

6766
// Check overall pipeline status first
68-
if event.status != "Succeeded" {
69-
error!("Pipeline run failed, please check it. Aggregate status: {}", event.status);
67+
if status != "Succeeded" {
68+
error!("Pipeline run failed, please check it.");
7069
return Ok(StatusCode::OK);
7170
}
7271

7372
// Process the pipeline event based on test task status:
7473
// - If succeeded: mark the stage as complete for the user
7574
// - If failed: log error (TODO: collect error details from Tekton and save to database)
7675
// - Other states: log and ignore non-terminal states
77-
match event.tasks.test.status.as_str() {
76+
match tasks.test.status.as_str() {
7877
"Succeeded" => {
7978
// Look up the course to get the user_id
80-
let id = Uuid::parse_str(&event.repo)?;
81-
let course = CourseRepository::get_user_course_by_id(&ctx.database, &id).await?;
79+
let id = Uuid::parse_str(repo)?;
80+
let user_course = CourseRepository::get_user_course_by_id(&ctx.database, &id).await?;
8281

8382
// Mark the stage as complete
84-
StageService::complete(ctx, &course.user_id, &event.course, &event.stage).await?;
85-
86-
info!("Stage {} completed successfully for course {}", event.stage, event.course);
83+
StageService::complete(ctx.clone(), &user_course.user_id, course, stage).await?;
84+
info!("Stage {} completed successfully for course {}", stage, course);
8785
}
8886
"Failed" => {
89-
info!("Test task failed: reason={}, stage={}", event.tasks.test.reason, event.stage);
87+
info!("Test task failed: reason={}, stage={}", tasks.test.reason, stage);
9088
return Ok(StatusCode::OK);
9189
}
9290
_ => {
9391
error!(
9492
"Test task in non-terminal state: status={}, reason={}",
95-
event.tasks.test.status, event.tasks.test.reason
93+
tasks.test.status, tasks.test.reason
9694
);
9795
return Ok(StatusCode::OK);
9896
}
9997
}
10098

99+
// Delete the PipelineRun after successful processing
100+
if let Err(e) = PipelineService::new(ctx.clone()).delete(name).await {
101+
error!("Failed to delete PipelineRun {}: {}", name, e);
102+
}
103+
101104
Ok(StatusCode::OK)
102105
}

src/service/pipeline.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use std::sync::Arc;
1616

1717
use kube::{
1818
Api,
19-
api::{ApiResource, DynamicObject, GroupVersionKind, PostParams},
19+
api::{ApiResource, DeleteParams, DynamicObject, GroupVersionKind, PostParams},
2020
};
2121
use serde_json::{Error as JsonError, Value, json};
2222
use tracing::debug;
@@ -50,6 +50,13 @@ impl PipelineService {
5050
Ok(())
5151
}
5252

53+
/// Deletes a Tekton PipelineRun by name.
54+
pub async fn delete(&self, name: &str) -> Result<()> {
55+
debug!("Deleting PipelineRun: {name}");
56+
self.api().delete(name, &DeleteParams::default()).await?;
57+
Ok(())
58+
}
59+
5360
#[inline]
5461
fn api(&self) -> Api<DynamicObject> {
5562
let gvk = GroupVersionKind::gvk("tekton.dev", "v1", "PipelineRun");
@@ -88,9 +95,7 @@ impl PipelineService {
8895
// Generate HMAC signature for webhook authentication
8996
let auth_secret = &self.ctx.config.auth_secret;
9097
let payload = format!("{}{}{}", repo, course, stage);
91-
let secret = crypto::hmac_sha256_sign(&payload, auth_secret).map_err(|e| {
92-
ApiError::InternalError(format!("Failed to generate HMAC signature: {}", e))
93-
})?;
98+
let secret = crypto::hmac_sha256_sign(&payload, auth_secret)?;
9499

95100
// Define parameters for the PipelineRun
96101
let params = vec![

src/utils/crypto.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,16 @@ use hex;
1616
use hmac::{Hmac, Mac};
1717
use sha2::Sha256;
1818

19+
use crate::errors::ApiError;
20+
1921
type HmacSha256 = Hmac<Sha256>;
2022

2123
/// Generates an HMAC-SHA256 signature for the given payload using the provided
2224
/// secret. Returns the signature as a hex-encoded string.
23-
pub fn hmac_sha256_sign(payload: &str, secret: &str) -> Result<String, String> {
24-
let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
25-
.map_err(|e| format!("Failed to create HMAC: {}", e))?;
25+
pub fn hmac_sha256_sign(payload: &str, secret: &str) -> Result<String, ApiError> {
26+
let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).map_err(|e| {
27+
ApiError::InternalError(format!("Failed to generate HMAC signature: {}", e))
28+
})?;
2629
mac.update(payload.as_bytes());
2730
let result = mac.finalize();
2831

@@ -31,9 +34,7 @@ pub fn hmac_sha256_sign(payload: &str, secret: &str) -> Result<String, String> {
3134

3235
/// Verifies an HMAC-SHA256 signature for the given payload using the provided
3336
/// secret. Uses constant-time comparison to prevent timing attacks.
34-
pub fn hmac_sha256_verify(payload: &str, secret: &str, sign: &str) -> Result<bool, String> {
37+
pub fn hmac_sha256_verify(payload: &str, secret: &str, sign: &str) -> Result<bool, ApiError> {
3538
let expected = hmac_sha256_sign(payload, secret)?;
36-
37-
// Constant-time comparison to prevent timing attacks
3839
Ok(subtle::ConstantTimeEq::ct_eq(sign.as_bytes(), expected.as_bytes()).into())
3940
}

0 commit comments

Comments
 (0)