1use std::collections::HashMap;
2
3use chrono::Utc;
4
5use crate::types::*;
6
7#[derive(Debug, Default)]
8pub struct CodeBuildState {
9 pub projects: HashMap<String, Project>,
10 pub builds: HashMap<String, Build>,
12 pub build_ids: Vec<String>,
14 pub build_counters: HashMap<String, i64>,
16 pub webhooks: HashMap<String, crate::types::Webhook>,
18 pub source_credentials: HashMap<String, crate::types::SourceCredential>,
20 pub resource_policies: HashMap<String, String>,
22 pub report_groups: HashMap<String, crate::types::ReportGroup>,
24 pub report_group_arns: Vec<String>,
26}
27
28#[derive(Debug, thiserror::Error)]
29pub enum CodeBuildError {
30 #[error("Only alphanumeric characters, dash, and underscore are supported")]
31 InvalidProjectName,
32 #[error("Invalid service role: Service role account ID does not match caller's account")]
33 InvalidServiceRole,
34 #[error("Invalid build ID provided")]
35 InvalidBuildId,
36 #[error("Project already exists: arn:aws:codebuild:{region}:{account_id}:project/{name}")]
37 ProjectAlreadyExists {
38 region: String,
39 account_id: String,
40 name: String,
41 },
42 #[error("Project cannot be found: arn:aws:codebuild:{region}:{account_id}:project/{name}")]
43 ProjectNotFound {
44 region: String,
45 account_id: String,
46 name: String,
47 },
48 #[error("Build {build_id} does not exist")]
49 BuildNotFound { build_id: String },
50 #[error("Project {name} does not exist")]
51 ProjectDoesNotExist { name: String },
52 #[error("Webhook for project {project_name} already exists")]
53 WebhookAlreadyExists { project_name: String },
54 #[error("Webhook for project {project_name} does not exist")]
55 WebhookNotFound { project_name: String },
56 #[error("Source credentials {arn} do not exist")]
57 SourceCredentialsNotFound { arn: String },
58 #[error("Resource policy for {resource_arn} does not exist")]
59 ResourcePolicyNotFound { resource_arn: String },
60 #[error("Report group with name {name} already exists")]
61 ReportGroupAlreadyExists { name: String },
62 #[error("Report group {arn} does not exist")]
63 ReportGroupNotFound { arn: String },
64}
65
66fn validate_project_name(name: &str) -> Result<(), CodeBuildError> {
67 if name.len() >= 150 {
68 return Err(CodeBuildError::InvalidProjectName);
69 }
70
71 let re = regex::Regex::new(r"^[A-Za-z].*[^!£$%^&*()\+=\|?`¬{}\@~#:;<>\\/\[\]]$").unwrap();
73 if !re.is_match(name) {
74 return Err(CodeBuildError::InvalidProjectName);
75 }
76
77 Ok(())
78}
79
80fn validate_service_role(account_id: &str, service_role: &str) -> Result<(), CodeBuildError> {
81 let prefix = format!("arn:aws:iam::{account_id}:role/");
82 if !service_role.starts_with(&prefix) {
83 return Err(CodeBuildError::InvalidServiceRole);
84 }
85 Ok(())
86}
87
88fn validate_build_id(build_id: &str) -> Result<(), CodeBuildError> {
89 if !build_id.contains(':') {
90 return Err(CodeBuildError::InvalidBuildId);
91 }
92 Ok(())
93}
94
95impl CodeBuildState {
96 pub fn create_project(
97 &mut self,
98 name: &str,
99 description: &str,
100 source_type: &str,
101 source_location: &str,
102 artifact_type: &str,
103 artifact_location: Option<&str>,
104 env_type: &str,
105 env_image: &str,
106 env_compute: &str,
107 service_role: &str,
108 tags: Vec<Tag>,
109 account_id: &str,
110 region: &str,
111 ) -> Result<&Project, CodeBuildError> {
112 validate_project_name(name)?;
113 validate_service_role(account_id, service_role)?;
114
115 if self.projects.contains_key(name) {
116 return Err(CodeBuildError::ProjectAlreadyExists {
117 region: region.to_string(),
118 account_id: account_id.to_string(),
119 name: name.to_string(),
120 });
121 }
122
123 let arn = format!("arn:aws:codebuild:{region}:{account_id}:project/{name}");
124 let now = Utc::now();
125
126 let project = Project {
127 name: name.to_string(),
128 arn,
129 description: description.to_string(),
130 source_type: source_type.to_string(),
131 source_location: source_location.to_string(),
132 artifact_type: artifact_type.to_string(),
133 artifact_location: artifact_location.map(|s| s.to_string()),
134 environment_type: env_type.to_string(),
135 environment_image: env_image.to_string(),
136 environment_compute_type: env_compute.to_string(),
137 service_role: service_role.to_string(),
138 tags,
139 created: now,
140 last_modified: now,
141 };
142
143 self.projects.insert(name.to_string(), project);
144 Ok(self.projects.get(name).unwrap())
145 }
146
147 pub fn batch_get_projects(&self, names: &[String]) -> Vec<&Project> {
148 names
149 .iter()
150 .filter_map(|n| {
151 if let Some(p) = self.projects.get(n.as_str()) {
153 return Some(p);
154 }
155 if n.starts_with("arn:") {
157 for project in self.projects.values() {
158 if project.arn == *n {
159 return Some(project);
160 }
161 }
162 }
163 None
164 })
165 .collect()
166 }
167
168 pub fn delete_project(&mut self, name: &str) -> Result<(), CodeBuildError> {
169 self.projects.remove(name);
171 self.build_ids.retain(|id| {
173 if let Some(build) = self.builds.get(id) {
174 build.project_name != name
175 } else {
176 true
177 }
178 });
179 self.builds.retain(|_, b| b.project_name != name);
180 self.build_counters.remove(name);
181 Ok(())
182 }
183
184 pub fn list_projects(&self) -> Vec<&str> {
185 self.projects.keys().map(|s| s.as_str()).collect()
186 }
187
188 pub fn start_build(
189 &mut self,
190 project_name: &str,
191 source_version: Option<&str>,
192 account_id: &str,
193 region: &str,
194 ) -> Result<&Build, CodeBuildError> {
195 let project = self
196 .projects
197 .get(project_name)
198 .ok_or_else(|| CodeBuildError::ProjectNotFound {
199 region: region.to_string(),
200 account_id: account_id.to_string(),
201 name: project_name.to_string(),
202 })?
203 .clone();
204
205 let counter = self
206 .build_counters
207 .entry(project_name.to_string())
208 .or_insert(0);
209 *counter += 1;
210 let build_number = *counter;
211
212 let build_id = format!("{project_name}:{}", uuid::Uuid::new_v4());
213 let arn = format!("arn:aws:codebuild:{region}:{account_id}:build/{build_id}");
214
215 let now = Utc::now();
216 let now_ts = now.timestamp() as f64;
217
218 let resolved_source_version = source_version.unwrap_or("refs/heads/main").to_string();
219
220 let phases = vec![
222 BuildPhase {
223 phase_type: "SUBMITTED".to_string(),
224 phase_status: Some("SUCCEEDED".to_string()),
225 start_time: now_ts,
226 end_time: Some(now_ts),
227 duration_in_seconds: Some(0),
228 },
229 BuildPhase {
230 phase_type: "QUEUED".to_string(),
231 phase_status: None,
232 start_time: now_ts,
233 end_time: None,
234 duration_in_seconds: None,
235 },
236 ];
237
238 let build = Build {
239 id: build_id.clone(),
240 arn,
241 project_name: project_name.to_string(),
242 build_status: "IN_PROGRESS".to_string(),
243 current_phase: "QUEUED".to_string(),
244 source_type: project.source_type.clone(),
245 source_location: project.source_location.clone(),
246 source_version: resolved_source_version,
247 artifact_type: project.artifact_type.clone(),
248 artifact_location: project.artifact_location.clone(),
249 environment_type: project.environment_type.clone(),
250 environment_image: project.environment_image.clone(),
251 environment_compute_type: project.environment_compute_type.clone(),
252 service_role: project.service_role.clone(),
253 start_time: now,
254 end_time: None,
255 build_number,
256 phases,
257 };
258
259 self.builds.insert(build_id.clone(), build);
260 self.build_ids.push(build_id.clone());
261 Ok(self.builds.get(&build_id).unwrap())
262 }
263
264 pub fn stop_build(&mut self, build_id: &str) -> Result<&Build, CodeBuildError> {
265 validate_build_id(build_id)?;
266
267 let build = self
268 .builds
269 .get_mut(build_id)
270 .ok_or_else(|| CodeBuildError::BuildNotFound {
271 build_id: build_id.to_string(),
272 })?;
273
274 set_completion_phases(&mut build.phases);
276 build.build_status = "STOPPED".to_string();
277 build.current_phase = "COMPLETED".to_string();
278 build.end_time = Some(Utc::now());
279 Ok(self.builds.get(build_id).unwrap())
280 }
281
282 pub fn batch_get_builds(&self, build_ids: &[String]) -> Result<Vec<Build>, CodeBuildError> {
283 for id in build_ids {
285 validate_build_id(id)?;
286 }
287
288 let mut result = Vec::new();
289 for id in build_ids {
290 if let Some(build) = self.builds.get(id.as_str()) {
291 let mut build = build.clone();
292 set_completion_phases(&mut build.phases);
294 build.current_phase = "COMPLETED".to_string();
295 build.build_status = "SUCCEEDED".to_string();
296 if build.end_time.is_none() {
297 build.end_time = Some(Utc::now());
298 }
299 result.push(build);
300 }
301 }
302 Ok(result)
303 }
304
305 pub fn list_builds(&self) -> Vec<&str> {
306 self.build_ids.iter().map(|s| s.as_str()).collect()
307 }
308
309 pub fn list_builds_for_project(&self, project_name: &str) -> Vec<&str> {
310 self.build_ids
311 .iter()
312 .filter(|id| {
313 self.builds
314 .get(id.as_str())
315 .map(|b| b.project_name == project_name)
316 .unwrap_or(false)
317 })
318 .map(|s| s.as_str())
319 .collect()
320 }
321
322 pub fn batch_delete_builds(&mut self, ids: &[String]) -> Vec<String> {
323 let mut deleted = Vec::new();
324 for id in ids {
325 if self.builds.remove(id.as_str()).is_some() {
326 self.build_ids.retain(|bid| bid != id);
327 deleted.push(id.clone());
328 }
329 }
330 deleted
331 }
332
333 pub fn update_project(
334 &mut self,
335 name: &str,
336 description: Option<&str>,
337 source_type: Option<&str>,
338 source_location: Option<&str>,
339 artifact_type: Option<&str>,
340 artifact_location: Option<Option<&str>>,
341 env_type: Option<&str>,
342 env_image: Option<&str>,
343 env_compute: Option<&str>,
344 service_role: Option<&str>,
345 tags: Option<Vec<crate::types::Tag>>,
346 account_id: &str,
347 ) -> Result<&Project, CodeBuildError> {
348 let project =
349 self.projects
350 .get_mut(name)
351 .ok_or_else(|| CodeBuildError::ProjectDoesNotExist {
352 name: name.to_string(),
353 })?;
354 if let Some(d) = description {
355 project.description = d.to_string();
356 }
357 if let Some(t) = source_type {
358 project.source_type = t.to_string();
359 }
360 if let Some(l) = source_location {
361 project.source_location = l.to_string();
362 }
363 if let Some(t) = artifact_type {
364 project.artifact_type = t.to_string();
365 }
366 if let Some(l) = artifact_location {
367 project.artifact_location = l.map(|s| s.to_string());
368 }
369 if let Some(t) = env_type {
370 project.environment_type = t.to_string();
371 }
372 if let Some(i) = env_image {
373 project.environment_image = i.to_string();
374 }
375 if let Some(c) = env_compute {
376 project.environment_compute_type = c.to_string();
377 }
378 if let Some(r) = service_role {
379 let prefix = format!("arn:aws:iam::{account_id}:role/");
381 if !r.starts_with(&prefix) {
382 return Err(CodeBuildError::InvalidServiceRole);
383 }
384 project.service_role = r.to_string();
385 }
386 if let Some(t) = tags {
387 project.tags = t;
388 }
389 project.last_modified = Utc::now();
390 Ok(self.projects.get(name).unwrap())
391 }
392
393 pub fn retry_build(
394 &mut self,
395 build_id: &str,
396 account_id: &str,
397 region: &str,
398 ) -> Result<&Build, CodeBuildError> {
399 validate_build_id(build_id)?;
400
401 let original = self
402 .builds
403 .get(build_id)
404 .ok_or_else(|| CodeBuildError::BuildNotFound {
405 build_id: build_id.to_string(),
406 })?
407 .clone();
408
409 let project_name = original.project_name.clone();
410 let source_version = original.source_version.clone();
411
412 self.start_build(&project_name, Some(&source_version), account_id, region)
413 }
414
415 pub fn create_webhook(
418 &mut self,
419 project_name: &str,
420 branch_filter: Option<&str>,
421 build_type: Option<&str>,
422 account_id: &str,
423 region: &str,
424 ) -> Result<&crate::types::Webhook, CodeBuildError> {
425 if !self.projects.contains_key(project_name) {
426 return Err(CodeBuildError::ProjectNotFound {
427 region: region.to_string(),
428 account_id: account_id.to_string(),
429 name: project_name.to_string(),
430 });
431 }
432 if self.webhooks.contains_key(project_name) {
433 return Err(CodeBuildError::WebhookAlreadyExists {
434 project_name: project_name.to_string(),
435 });
436 }
437 let webhook = crate::types::Webhook {
438 project_name: project_name.to_string(),
439 url: format!("https://codebuild.{region}.amazonaws.com/webhooks/{project_name}"),
440 branch_filter: branch_filter.map(|s| s.to_string()),
441 build_type: build_type.map(|s| s.to_string()),
442 secret: Some(uuid::Uuid::new_v4().to_string()),
443 };
444 self.webhooks.insert(project_name.to_string(), webhook);
445 Ok(self.webhooks.get(project_name).unwrap())
446 }
447
448 pub fn update_webhook(
449 &mut self,
450 project_name: &str,
451 branch_filter: Option<&str>,
452 build_type: Option<&str>,
453 ) -> Result<&crate::types::Webhook, CodeBuildError> {
454 let webhook =
455 self.webhooks
456 .get_mut(project_name)
457 .ok_or_else(|| CodeBuildError::WebhookNotFound {
458 project_name: project_name.to_string(),
459 })?;
460 if let Some(bf) = branch_filter {
461 webhook.branch_filter = Some(bf.to_string());
462 }
463 if let Some(bt) = build_type {
464 webhook.build_type = Some(bt.to_string());
465 }
466 Ok(self.webhooks.get(project_name).unwrap())
467 }
468
469 pub fn delete_webhook(&mut self, project_name: &str) -> Result<(), CodeBuildError> {
470 match self.webhooks.remove(project_name) {
471 Some(_) => Ok(()),
472 None => Err(CodeBuildError::WebhookNotFound {
473 project_name: project_name.to_string(),
474 }),
475 }
476 }
477
478 pub fn import_source_credentials(
481 &mut self,
482 token: &str,
483 server_type: &str,
484 auth_type: &str,
485 username: Option<&str>,
486 account_id: &str,
487 region: &str,
488 ) -> Result<&crate::types::SourceCredential, CodeBuildError> {
489 let id = uuid::Uuid::new_v4().to_string();
490 let arn = format!("arn:aws:codebuild:{region}:{account_id}:token/{server_type}/{id}");
491 let _ = token; let cred = crate::types::SourceCredential {
493 arn: arn.clone(),
494 server_type: server_type.to_string(),
495 auth_type: auth_type.to_string(),
496 resource: username.map(|s| s.to_string()),
497 };
498 self.source_credentials.insert(arn.clone(), cred);
499 Ok(self.source_credentials.get(&arn).unwrap())
500 }
501
502 pub fn list_source_credentials(&self) -> Vec<&crate::types::SourceCredential> {
503 self.source_credentials.values().collect()
504 }
505
506 pub fn delete_source_credentials(&mut self, arn: &str) -> Result<(), CodeBuildError> {
507 match self.source_credentials.remove(arn) {
508 Some(_) => Ok(()),
509 None => Err(CodeBuildError::SourceCredentialsNotFound {
510 arn: arn.to_string(),
511 }),
512 }
513 }
514
515 pub fn put_resource_policy(
518 &mut self,
519 resource_arn: &str,
520 policy: &str,
521 ) -> Result<String, CodeBuildError> {
522 self.resource_policies
523 .insert(resource_arn.to_string(), policy.to_string());
524 Ok(resource_arn.to_string())
525 }
526
527 pub fn get_resource_policy(&self, resource_arn: &str) -> Result<String, CodeBuildError> {
528 self.resource_policies
529 .get(resource_arn)
530 .cloned()
531 .ok_or_else(|| CodeBuildError::ResourcePolicyNotFound {
532 resource_arn: resource_arn.to_string(),
533 })
534 }
535
536 pub fn delete_resource_policy(&mut self, resource_arn: &str) -> Result<(), CodeBuildError> {
537 self.resource_policies.remove(resource_arn);
538 Ok(())
539 }
540
541 pub fn create_report_group(
544 &mut self,
545 name: &str,
546 report_type: &str,
547 export_config_type: Option<&str>,
548 tags: Vec<crate::types::Tag>,
549 account_id: &str,
550 region: &str,
551 ) -> Result<&crate::types::ReportGroup, CodeBuildError> {
552 for rg in self.report_groups.values() {
554 if rg.name == name {
555 return Err(CodeBuildError::ReportGroupAlreadyExists {
556 name: name.to_string(),
557 });
558 }
559 }
560
561 let id = uuid::Uuid::new_v4().to_string();
562 let arn = format!("arn:aws:codebuild:{region}:{account_id}:report-group/{name}-{id}");
563 let now = Utc::now();
564
565 let rg = crate::types::ReportGroup {
566 arn: arn.clone(),
567 name: name.to_string(),
568 r#type: report_type.to_string(),
569 export_config_type: export_config_type.map(|s| s.to_string()),
570 tags,
571 created: now,
572 last_modified: now,
573 status: "ACTIVE".to_string(),
574 };
575
576 self.report_groups.insert(arn.clone(), rg);
577 self.report_group_arns.push(arn.clone());
578 Ok(self.report_groups.get(&arn).unwrap())
579 }
580
581 pub fn batch_get_report_groups(&self, arns: &[String]) -> Vec<&crate::types::ReportGroup> {
582 arns.iter()
583 .filter_map(|arn| self.report_groups.get(arn.as_str()))
584 .collect()
585 }
586
587 pub fn list_report_groups(&self) -> Vec<&str> {
588 self.report_group_arns.iter().map(|s| s.as_str()).collect()
589 }
590
591 pub fn delete_report_group(&mut self, arn: &str) -> Result<(), CodeBuildError> {
592 match self.report_groups.remove(arn) {
593 Some(_) => {
594 self.report_group_arns.retain(|a| a != arn);
595 Ok(())
596 }
597 None => Err(CodeBuildError::ReportGroupNotFound {
598 arn: arn.to_string(),
599 }),
600 }
601 }
602
603 pub fn update_report_group(
604 &mut self,
605 arn: &str,
606 export_config_type: Option<&str>,
607 tags: Option<Vec<crate::types::Tag>>,
608 ) -> Result<&crate::types::ReportGroup, CodeBuildError> {
609 let rg =
610 self.report_groups
611 .get_mut(arn)
612 .ok_or_else(|| CodeBuildError::ReportGroupNotFound {
613 arn: arn.to_string(),
614 })?;
615
616 if let Some(ect) = export_config_type {
617 rg.export_config_type = Some(ect.to_string());
618 }
619 if let Some(t) = tags {
620 rg.tags = t;
621 }
622 rg.last_modified = Utc::now();
623 Ok(self.report_groups.get(arn).unwrap())
624 }
625}
626
627fn set_completion_phases(phases: &mut Vec<BuildPhase>) {
628 let now_ts = Utc::now().timestamp() as f64;
629
630 for phase in phases.iter_mut() {
632 if phase.phase_type == "QUEUED" && phase.phase_status.is_none() {
633 phase.phase_status = Some("SUCCEEDED".to_string());
634 }
635 }
636
637 let additional_phases = [
638 "PROVISIONING",
639 "DOWNLOAD_SOURCE",
640 "INSTALL",
641 "PRE_BUILD",
642 "BUILD",
643 "POST_BUILD",
644 "UPLOAD_ARTIFACTS",
645 "FINALIZING",
646 "COMPLETED",
647 ];
648
649 for phase_type in &additional_phases {
650 if !phases.iter().any(|p| p.phase_type == *phase_type) {
652 phases.push(BuildPhase {
653 phase_type: phase_type.to_string(),
654 phase_status: Some("SUCCEEDED".to_string()),
655 start_time: now_ts,
656 end_time: Some(now_ts),
657 duration_in_seconds: Some(0),
658 });
659 }
660 }
661}