Skip to content

Commit 90a3e5f

Browse files
committed
refactor: implement dynamic naming strategy to resolve PipelineRun conflicts
1 parent d26f10d commit 90a3e5f

File tree

5 files changed

+44
-37
lines changed

5 files changed

+44
-37
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.38.0"
11+
version = "0.39.0"
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.38.0"
9+
"version": "0.39.0"
1010
},
1111
"paths": {
1212
"/v1/courses": {

src/service/pipeline.rs

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,17 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
use std::{collections::HashMap, sync::Arc, time::Duration};
15+
use std::{sync::Arc, time::Duration};
1616

1717
use k8s_openapi::apimachinery::pkg::apis::meta::v1::Condition;
1818
use kube::{
19-
Api,
19+
Api, ResourceExt,
2020
api::{ApiResource, DynamicObject, GroupVersionKind, PostParams},
2121
};
22-
use serde_json::json;
22+
use serde_json::{Error as JsonError, Value, json};
2323
use tokio::time::interval;
2424
use tracing::debug;
25+
use uuid::Uuid;
2526

2627
use crate::{
2728
context::Context,
@@ -42,13 +43,13 @@ impl PipelineService {
4243
}
4344

4445
/// Triggers a Tekton PipelineRun for the given repository.
45-
pub async fn trigger(&self, repo: &str, course: &str, current_stage_slug: &str) -> Result<()> {
46+
pub async fn trigger(&self, repo: &str, course: &str, stage: &str) -> Result<String> {
4647
debug!("Triggering PipelineRun for repository: {course} - {repo}");
4748

48-
let resource = self.generate(repo, course, current_stage_slug).await?;
49-
self.api().create(&PostParams::default(), &resource).await?;
49+
let resource = self.generate(repo, course, stage).await?;
50+
let res = self.api().create(&PostParams::default(), &resource).await?;
5051

51-
Ok(())
52+
Ok(res.name_any())
5253
}
5354

5455
/// Watches a PipelineRun and invokes the callback only on success.
@@ -108,8 +109,17 @@ impl PipelineService {
108109
)
109110
}
110111

111-
/// Generates a PipelineRun resource for the given repository name.
112-
async fn generate(&self, name: &str, course: &str, stage: &str) -> Result<DynamicObject> {
112+
/// Generates a PipelineRun resource for the given repository.
113+
async fn generate(&self, repo: &str, course: &str, stage: &str) -> Result<DynamicObject> {
114+
let name = Uuid::new_v4().to_string();
115+
116+
// Define labels for identification
117+
let labels = vec![
118+
("stackclass.dev/repo", repo.to_string()),
119+
("stackclass.dev/course", course.to_string()),
120+
("stackclass.dev/stage", stage.to_string()),
121+
];
122+
113123
// Build test cases JSON value from all stages up to the current stage
114124
let stages = StageRepository::find_stages_until(&self.ctx.database, course, stage).await?;
115125
let slugs: Vec<&str> = stages.iter().map(|stage| stage.slug.as_str()).collect();
@@ -119,30 +129,26 @@ impl PipelineService {
119129
let registry = url::hostname(&self.ctx.config.docker_registry_endpoint)?;
120130
let org = &self.ctx.config.namespace;
121131

122-
// Define params as a HashMap and then convert it to JSON value
123-
let params = HashMap::from([
124-
("REPO_URL", format!("{git_endpoint}/{org}/{name}.git")),
125-
("REPO_REF", "main".to_string()),
126-
("COURSE_IMAGE", format!("{registry}/{org}/{name}:latest")),
132+
// Define parameters for the PipelineRun
133+
let params = vec![
134+
("REPO_URL", format!("{git_endpoint}/{org}/{repo}.git")),
135+
("COURSE_IMAGE", format!("{registry}/{org}/{repo}:latest")),
127136
("TESTER_IMAGE", format!("ghcr.io/stackclass/{course}-tester")),
128-
("TEST_IMAGE", format!("{registry}/{org}/{name}-test:latest")),
137+
("TEST_IMAGE", format!("{registry}/{org}/{repo}-test:latest")),
129138
("COMMAND", format!("/app/{course}-tester")),
130139
("TEST_CASES_JSON", cases),
131-
("DEBUG_MODE", "false".to_string()),
132-
("TIMEOUT_SECONDS", "15".to_string()),
133-
("SKIP_ANTI_CHEAT", "false".to_string()),
134-
]);
140+
];
135141

136-
// Render a PipelineRun resource using the provided repo and parameters
137-
resource(name, params).map_err(ApiError::SerializationError)
142+
// Render a PipelineRun resource with the given name, labels, and params
143+
resource(&name, labels, params).map_err(ApiError::SerializationError)
138144
}
139145
}
140146

141147
/// Builds a JSON string representing test cases from a list of slugs.
142148
fn build_test_cases_json(slugs: &[&str]) -> String {
143149
let mut test_cases = Vec::new();
144150
for (index, slug) in slugs.iter().enumerate() {
145-
test_cases.push(serde_json::json!({
151+
test_cases.push(json!({
146152
"slug": slug,
147153
"log_prefix": format!("test-{}", index + 1),
148154
"title": format!("Stage #{}: {}", index + 1, slug),
@@ -152,18 +158,19 @@ fn build_test_cases_json(slugs: &[&str]) -> String {
152158
}
153159

154160
/// Creates a new DynamicObject representing a Tekton PipelineRun resource.
155-
fn resource(
156-
name: &str,
157-
params: HashMap<&'static str, String>,
158-
) -> Result<DynamicObject, serde_json::Error> {
159-
let params: serde_json::Value =
160-
params.into_iter().map(|(name, value)| json!({"name": name, "value": value})).collect();
161+
fn resource<T>(name: &str, labels: T, params: T) -> Result<DynamicObject, JsonError>
162+
where
163+
T: IntoIterator<Item = (&'static str, String)>,
164+
{
165+
let labels: Value = labels.into_iter().collect();
166+
let params: Value = params.into_iter().map(|(k, v)| json!({"name": k, "value": v})).collect();
161167

162168
let resource = json!({
163169
"apiVersion": "tekton.dev/v1",
164170
"kind": "PipelineRun",
165171
"metadata": {
166-
"name": name
172+
"name": name,
173+
"labels": labels
167174
},
168175
"spec": {
169176
"pipelineRef": {

src/service/repository.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,10 @@ impl RepoService {
105105
/// - Otherwise, it triggers the pipeline for the current stage and monitors completion.
106106
/// - On success, marks the stage as complete.
107107
pub async fn process(&self, event: &Event) -> Result<()> {
108-
let name = &event.repository.name;
109-
debug!("Handling push event for repository: {}", name);
108+
let repo = &event.repository.name;
109+
debug!("Handling push event for repository: {}", repo);
110110

111-
let id = Uuid::parse_str(name)?;
111+
let id = Uuid::parse_str(repo)?;
112112
let mut course = CourseRepository::get_user_course_by_id(&self.ctx.database, &id).await?;
113113

114114
// If there's no current stage, this is the first setup of the course,
@@ -120,7 +120,7 @@ impl RepoService {
120120

121121
// Trigger the pipeline run
122122
let pipeline = PipelineService::new(self.ctx.clone());
123-
pipeline.trigger(name, &course.course_slug, &current_stage_slug).await?;
123+
let name = pipeline.trigger(repo, &course.course_slug, &current_stage_slug).await?;
124124

125125
// Define a callback function that will be executed when the pipeline
126126
// succeeds. This callback marks the current stage as complete in DB.
@@ -137,7 +137,7 @@ impl RepoService {
137137
};
138138

139139
// Watch the pipeline and handle only success
140-
pipeline.watch(name, callback).await?;
140+
pipeline.watch(&name, callback).await?;
141141

142142
Ok(())
143143
}

0 commit comments

Comments
 (0)