1use std::collections::HashMap;
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use winterbaume_core::{StateChangeNotifier, StateViewError, StatefulService};
8
9use crate::handlers::CodeBuildService;
10use crate::state::CodeBuildState;
11use crate::types::{Build, BuildPhase, Project, ReportGroup, SourceCredential, Tag, Webhook};
12
13#[derive(Debug, Clone, Default, Serialize, Deserialize)]
15pub struct CodeBuildStateView {
16 #[serde(default)]
18 pub projects: HashMap<String, ProjectView>,
19 #[serde(default)]
21 pub builds: HashMap<String, BuildView>,
22 #[serde(default)]
24 pub build_ids: Vec<String>,
25 #[serde(default)]
27 pub build_counters: HashMap<String, i64>,
28 #[serde(default)]
30 pub webhooks: HashMap<String, WebhookView>,
31 #[serde(default)]
33 pub source_credentials: HashMap<String, SourceCredentialView>,
34 #[serde(default)]
36 pub resource_policies: HashMap<String, String>,
37 #[serde(default)]
39 pub report_groups: HashMap<String, ReportGroupView>,
40 #[serde(default)]
42 pub report_group_arns: Vec<String>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct TagView {
48 pub key: String,
49 pub value: String,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct ProjectView {
55 pub name: String,
56 pub arn: String,
57 pub description: String,
58 pub source_type: String,
59 pub source_location: String,
60 pub artifact_type: String,
61 pub artifact_location: Option<String>,
62 pub environment_type: String,
63 pub environment_image: String,
64 pub environment_compute_type: String,
65 pub service_role: String,
66 #[serde(default)]
67 pub tags: Vec<TagView>,
68 pub created: String,
69 pub last_modified: String,
70 #[serde(default)]
72 pub build_batch_config: Option<serde_json::Value>,
73 #[serde(default)]
75 pub cache: Option<serde_json::Value>,
76 #[serde(default)]
78 pub file_system_locations: Vec<serde_json::Value>,
79 #[serde(default)]
81 pub logs_config: Option<serde_json::Value>,
82 #[serde(default)]
84 pub secondary_artifacts: Vec<serde_json::Value>,
85 #[serde(default)]
87 pub secondary_sources: Vec<serde_json::Value>,
88 #[serde(default)]
90 pub vpc_config: Option<serde_json::Value>,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct BuildPhaseView {
96 pub phase_type: String,
97 pub phase_status: Option<String>,
98 pub start_time: f64,
99 pub end_time: Option<f64>,
100 pub duration_in_seconds: Option<i64>,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct BuildView {
106 pub id: String,
107 pub arn: String,
108 pub project_name: String,
109 pub build_status: String,
110 pub current_phase: String,
111 pub source_type: String,
112 pub source_location: String,
113 pub source_version: String,
114 pub artifact_type: String,
115 pub artifact_location: Option<String>,
116 pub environment_type: String,
117 pub environment_image: String,
118 pub environment_compute_type: String,
119 pub service_role: String,
120 pub start_time: String,
121 pub end_time: Option<String>,
122 pub build_number: i64,
123 #[serde(default)]
124 pub phases: Vec<BuildPhaseView>,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct WebhookView {
130 pub project_name: String,
131 pub url: String,
132 pub branch_filter: Option<String>,
133 pub build_type: Option<String>,
134 pub secret: Option<String>,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct SourceCredentialView {
140 pub arn: String,
141 pub server_type: String,
142 pub auth_type: String,
143 pub resource: Option<String>,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct ReportGroupView {
149 pub arn: String,
150 pub name: String,
151 pub r#type: String,
152 pub export_config_type: Option<String>,
153 #[serde(default)]
154 pub tags: Vec<TagView>,
155 pub created: String,
156 pub last_modified: String,
157 pub status: String,
158}
159
160impl From<&Tag> for TagView {
165 fn from(t: &Tag) -> Self {
166 TagView {
167 key: t.key.clone(),
168 value: t.value.clone(),
169 }
170 }
171}
172
173impl From<&Project> for ProjectView {
174 fn from(p: &Project) -> Self {
175 ProjectView {
176 name: p.name.clone(),
177 arn: p.arn.clone(),
178 description: p.description.clone(),
179 source_type: p.source_type.clone(),
180 source_location: p.source_location.clone(),
181 artifact_type: p.artifact_type.clone(),
182 artifact_location: p.artifact_location.clone(),
183 environment_type: p.environment_type.clone(),
184 environment_image: p.environment_image.clone(),
185 environment_compute_type: p.environment_compute_type.clone(),
186 service_role: p.service_role.clone(),
187 tags: p.tags.iter().map(TagView::from).collect(),
188 created: p.created.to_rfc3339(),
189 last_modified: p.last_modified.to_rfc3339(),
190 build_batch_config: None,
191 cache: None,
192 file_system_locations: vec![],
193 logs_config: None,
194 secondary_artifacts: vec![],
195 secondary_sources: vec![],
196 vpc_config: None,
197 }
198 }
199}
200
201impl From<&BuildPhase> for BuildPhaseView {
202 fn from(ph: &BuildPhase) -> Self {
203 BuildPhaseView {
204 phase_type: ph.phase_type.clone(),
205 phase_status: ph.phase_status.clone(),
206 start_time: ph.start_time,
207 end_time: ph.end_time,
208 duration_in_seconds: ph.duration_in_seconds,
209 }
210 }
211}
212
213impl From<&Build> for BuildView {
214 fn from(b: &Build) -> Self {
215 BuildView {
216 id: b.id.clone(),
217 arn: b.arn.clone(),
218 project_name: b.project_name.clone(),
219 build_status: b.build_status.clone(),
220 current_phase: b.current_phase.clone(),
221 source_type: b.source_type.clone(),
222 source_location: b.source_location.clone(),
223 source_version: b.source_version.clone(),
224 artifact_type: b.artifact_type.clone(),
225 artifact_location: b.artifact_location.clone(),
226 environment_type: b.environment_type.clone(),
227 environment_image: b.environment_image.clone(),
228 environment_compute_type: b.environment_compute_type.clone(),
229 service_role: b.service_role.clone(),
230 start_time: b.start_time.to_rfc3339(),
231 end_time: b.end_time.as_ref().map(|d| d.to_rfc3339()),
232 build_number: b.build_number,
233 phases: b.phases.iter().map(BuildPhaseView::from).collect(),
234 }
235 }
236}
237
238impl From<&Webhook> for WebhookView {
239 fn from(w: &Webhook) -> Self {
240 WebhookView {
241 project_name: w.project_name.clone(),
242 url: w.url.clone(),
243 branch_filter: w.branch_filter.clone(),
244 build_type: w.build_type.clone(),
245 secret: w.secret.clone(),
246 }
247 }
248}
249
250impl From<&SourceCredential> for SourceCredentialView {
251 fn from(c: &SourceCredential) -> Self {
252 SourceCredentialView {
253 arn: c.arn.clone(),
254 server_type: c.server_type.clone(),
255 auth_type: c.auth_type.clone(),
256 resource: c.resource.clone(),
257 }
258 }
259}
260
261impl From<&ReportGroup> for ReportGroupView {
262 fn from(rg: &ReportGroup) -> Self {
263 ReportGroupView {
264 arn: rg.arn.clone(),
265 name: rg.name.clone(),
266 r#type: rg.r#type.clone(),
267 export_config_type: rg.export_config_type.clone(),
268 tags: rg.tags.iter().map(TagView::from).collect(),
269 created: rg.created.to_rfc3339(),
270 last_modified: rg.last_modified.to_rfc3339(),
271 status: rg.status.clone(),
272 }
273 }
274}
275
276impl From<&CodeBuildState> for CodeBuildStateView {
277 fn from(s: &CodeBuildState) -> Self {
278 let projects = s
279 .projects
280 .iter()
281 .map(|(k, v)| (k.clone(), ProjectView::from(v)))
282 .collect();
283 let builds = s
284 .builds
285 .iter()
286 .map(|(k, v)| (k.clone(), BuildView::from(v)))
287 .collect();
288 let webhooks = s
289 .webhooks
290 .iter()
291 .map(|(k, v)| (k.clone(), WebhookView::from(v)))
292 .collect();
293 let source_credentials = s
294 .source_credentials
295 .iter()
296 .map(|(k, v)| (k.clone(), SourceCredentialView::from(v)))
297 .collect();
298 let report_groups = s
299 .report_groups
300 .iter()
301 .map(|(k, v)| (k.clone(), ReportGroupView::from(v)))
302 .collect();
303 CodeBuildStateView {
304 projects,
305 builds,
306 build_ids: s.build_ids.clone(),
307 build_counters: s.build_counters.clone(),
308 webhooks,
309 source_credentials,
310 resource_policies: s.resource_policies.clone(),
311 report_groups,
312 report_group_arns: s.report_group_arns.clone(),
313 }
314 }
315}
316
317impl StatefulService for CodeBuildService {
322 type StateView = CodeBuildStateView;
323
324 async fn snapshot(&self, account_id: &str, region: &str) -> Self::StateView {
325 let state = self.state.get(account_id, region);
326 let guard = state.read().await;
327 CodeBuildStateView::from(&*guard)
328 }
329
330 async fn restore(
331 &self,
332 account_id: &str,
333 region: &str,
334 view: Self::StateView,
335 ) -> Result<(), StateViewError> {
336 let mut new_state = CodeBuildState::default();
337
338 for (name, pv) in view.projects {
339 let created = DateTime::parse_from_rfc3339(&pv.created)
340 .map(|d| d.with_timezone(&Utc))
341 .unwrap_or_else(|_| Utc::now());
342 let last_modified = DateTime::parse_from_rfc3339(&pv.last_modified)
343 .map(|d| d.with_timezone(&Utc))
344 .unwrap_or_else(|_| Utc::now());
345 new_state.projects.insert(
346 name,
347 Project {
348 name: pv.name,
349 arn: pv.arn,
350 description: pv.description,
351 source_type: pv.source_type,
352 source_location: pv.source_location,
353 artifact_type: pv.artifact_type,
354 artifact_location: pv.artifact_location,
355 environment_type: pv.environment_type,
356 environment_image: pv.environment_image,
357 environment_compute_type: pv.environment_compute_type,
358 service_role: pv.service_role,
359 tags: pv
360 .tags
361 .into_iter()
362 .map(|t| Tag {
363 key: t.key,
364 value: t.value,
365 })
366 .collect(),
367 created,
368 last_modified,
369 },
370 );
371 }
372
373 for (id, bv) in view.builds {
374 let start_time = DateTime::parse_from_rfc3339(&bv.start_time)
375 .map(|d| d.with_timezone(&Utc))
376 .unwrap_or_else(|_| Utc::now());
377 let end_time = bv
378 .end_time
379 .as_deref()
380 .and_then(|s| DateTime::parse_from_rfc3339(s).ok())
381 .map(|d| d.with_timezone(&Utc));
382 new_state.builds.insert(
383 id,
384 Build {
385 id: bv.id,
386 arn: bv.arn,
387 project_name: bv.project_name,
388 build_status: bv.build_status,
389 current_phase: bv.current_phase,
390 source_type: bv.source_type,
391 source_location: bv.source_location,
392 source_version: bv.source_version,
393 artifact_type: bv.artifact_type,
394 artifact_location: bv.artifact_location,
395 environment_type: bv.environment_type,
396 environment_image: bv.environment_image,
397 environment_compute_type: bv.environment_compute_type,
398 service_role: bv.service_role,
399 start_time,
400 end_time,
401 build_number: bv.build_number,
402 phases: bv
403 .phases
404 .into_iter()
405 .map(|ph| BuildPhase {
406 phase_type: ph.phase_type,
407 phase_status: ph.phase_status,
408 start_time: ph.start_time,
409 end_time: ph.end_time,
410 duration_in_seconds: ph.duration_in_seconds,
411 })
412 .collect(),
413 },
414 );
415 }
416
417 new_state.build_ids = view.build_ids;
418 new_state.build_counters = view.build_counters;
419
420 for (name, wv) in view.webhooks {
421 new_state.webhooks.insert(
422 name,
423 Webhook {
424 project_name: wv.project_name,
425 url: wv.url,
426 branch_filter: wv.branch_filter,
427 build_type: wv.build_type,
428 secret: wv.secret,
429 },
430 );
431 }
432
433 for (arn, cv) in view.source_credentials {
434 new_state.source_credentials.insert(
435 arn,
436 SourceCredential {
437 arn: cv.arn,
438 server_type: cv.server_type,
439 auth_type: cv.auth_type,
440 resource: cv.resource,
441 },
442 );
443 }
444
445 new_state.resource_policies = view.resource_policies;
446
447 for (arn, rgv) in view.report_groups {
448 let created = DateTime::parse_from_rfc3339(&rgv.created)
449 .map(|d| d.with_timezone(&Utc))
450 .unwrap_or_else(|_| Utc::now());
451 let last_modified = DateTime::parse_from_rfc3339(&rgv.last_modified)
452 .map(|d| d.with_timezone(&Utc))
453 .unwrap_or_else(|_| Utc::now());
454 new_state.report_groups.insert(
455 arn,
456 ReportGroup {
457 arn: rgv.arn,
458 name: rgv.name,
459 r#type: rgv.r#type,
460 export_config_type: rgv.export_config_type,
461 tags: rgv
462 .tags
463 .into_iter()
464 .map(|t| Tag {
465 key: t.key,
466 value: t.value,
467 })
468 .collect(),
469 created,
470 last_modified,
471 status: rgv.status,
472 },
473 );
474 }
475
476 new_state.report_group_arns = view.report_group_arns;
477
478 {
479 let state = self.state.get(account_id, region);
480 *state.write().await = new_state;
481 }
482 self.notify_state_changed(account_id, region).await;
483 Ok(())
484 }
485
486 async fn merge(
487 &self,
488 account_id: &str,
489 region: &str,
490 view: Self::StateView,
491 ) -> Result<(), StateViewError> {
492 let state = self.state.get(account_id, region);
493 {
494 let mut guard = state.write().await;
495
496 for (name, pv) in view.projects {
497 let created = DateTime::parse_from_rfc3339(&pv.created)
498 .map(|d| d.with_timezone(&Utc))
499 .unwrap_or_else(|_| Utc::now());
500 let last_modified = DateTime::parse_from_rfc3339(&pv.last_modified)
501 .map(|d| d.with_timezone(&Utc))
502 .unwrap_or_else(|_| Utc::now());
503 guard.projects.insert(
504 name,
505 Project {
506 name: pv.name,
507 arn: pv.arn,
508 description: pv.description,
509 source_type: pv.source_type,
510 source_location: pv.source_location,
511 artifact_type: pv.artifact_type,
512 artifact_location: pv.artifact_location,
513 environment_type: pv.environment_type,
514 environment_image: pv.environment_image,
515 environment_compute_type: pv.environment_compute_type,
516 service_role: pv.service_role,
517 tags: pv
518 .tags
519 .into_iter()
520 .map(|t| Tag {
521 key: t.key,
522 value: t.value,
523 })
524 .collect(),
525 created,
526 last_modified,
527 },
528 );
529 }
530
531 for (id, bv) in view.builds {
532 let start_time = DateTime::parse_from_rfc3339(&bv.start_time)
533 .map(|d| d.with_timezone(&Utc))
534 .unwrap_or_else(|_| Utc::now());
535 let end_time = bv
536 .end_time
537 .as_deref()
538 .and_then(|s| DateTime::parse_from_rfc3339(s).ok())
539 .map(|d| d.with_timezone(&Utc));
540 if !guard.build_ids.contains(&bv.id) {
541 guard.build_ids.push(bv.id.clone());
542 }
543 guard.builds.insert(
544 id,
545 Build {
546 id: bv.id,
547 arn: bv.arn,
548 project_name: bv.project_name,
549 build_status: bv.build_status,
550 current_phase: bv.current_phase,
551 source_type: bv.source_type,
552 source_location: bv.source_location,
553 source_version: bv.source_version,
554 artifact_type: bv.artifact_type,
555 artifact_location: bv.artifact_location,
556 environment_type: bv.environment_type,
557 environment_image: bv.environment_image,
558 environment_compute_type: bv.environment_compute_type,
559 service_role: bv.service_role,
560 start_time,
561 end_time,
562 build_number: bv.build_number,
563 phases: bv
564 .phases
565 .into_iter()
566 .map(|ph| BuildPhase {
567 phase_type: ph.phase_type,
568 phase_status: ph.phase_status,
569 start_time: ph.start_time,
570 end_time: ph.end_time,
571 duration_in_seconds: ph.duration_in_seconds,
572 })
573 .collect(),
574 },
575 );
576 }
577
578 for (name, counter) in view.build_counters {
579 let entry = guard.build_counters.entry(name).or_insert(0);
580 if counter > *entry {
581 *entry = counter;
582 }
583 }
584
585 for (name, wv) in view.webhooks {
586 guard.webhooks.insert(
587 name,
588 Webhook {
589 project_name: wv.project_name,
590 url: wv.url,
591 branch_filter: wv.branch_filter,
592 build_type: wv.build_type,
593 secret: wv.secret,
594 },
595 );
596 }
597
598 for (arn, cv) in view.source_credentials {
599 guard.source_credentials.insert(
600 arn,
601 SourceCredential {
602 arn: cv.arn,
603 server_type: cv.server_type,
604 auth_type: cv.auth_type,
605 resource: cv.resource,
606 },
607 );
608 }
609
610 for (arn, policy) in view.resource_policies {
611 guard.resource_policies.insert(arn, policy);
612 }
613
614 for (arn, rgv) in view.report_groups {
615 let created = DateTime::parse_from_rfc3339(&rgv.created)
616 .map(|d| d.with_timezone(&Utc))
617 .unwrap_or_else(|_| Utc::now());
618 let last_modified = DateTime::parse_from_rfc3339(&rgv.last_modified)
619 .map(|d| d.with_timezone(&Utc))
620 .unwrap_or_else(|_| Utc::now());
621 if !guard.report_group_arns.contains(&rgv.arn) {
622 guard.report_group_arns.push(rgv.arn.clone());
623 }
624 guard.report_groups.insert(
625 arn,
626 ReportGroup {
627 arn: rgv.arn,
628 name: rgv.name,
629 r#type: rgv.r#type,
630 export_config_type: rgv.export_config_type,
631 tags: rgv
632 .tags
633 .into_iter()
634 .map(|t| Tag {
635 key: t.key,
636 value: t.value,
637 })
638 .collect(),
639 created,
640 last_modified,
641 status: rgv.status,
642 },
643 );
644 }
645 }
646 self.notify_state_changed(account_id, region).await;
647 Ok(())
648 }
649
650 fn notifier(&self) -> &StateChangeNotifier<Self::StateView> {
651 &self.notifier
652 }
653}