Skip to main content

winterbaume_codebuild/
views.rs

1//! Serde-compatible view types for CodeBuild state snapshots.
2
3use 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/// Serializable view of the entire CodeBuild state for one account/region.
14#[derive(Debug, Clone, Default, Serialize, Deserialize)]
15pub struct CodeBuildStateView {
16    /// Projects keyed by project name.
17    #[serde(default)]
18    pub projects: HashMap<String, ProjectView>,
19    /// Builds keyed by build ID.
20    #[serde(default)]
21    pub builds: HashMap<String, BuildView>,
22    /// Ordered list of build IDs.
23    #[serde(default)]
24    pub build_ids: Vec<String>,
25    /// Next build number per project.
26    #[serde(default)]
27    pub build_counters: HashMap<String, i64>,
28    /// Webhooks keyed by project name.
29    #[serde(default)]
30    pub webhooks: HashMap<String, WebhookView>,
31    /// Source credentials keyed by ARN.
32    #[serde(default)]
33    pub source_credentials: HashMap<String, SourceCredentialView>,
34    /// Resource policies keyed by resource ARN.
35    #[serde(default)]
36    pub resource_policies: HashMap<String, String>,
37    /// Report groups keyed by ARN.
38    #[serde(default)]
39    pub report_groups: HashMap<String, ReportGroupView>,
40    /// Ordered list of report group ARNs.
41    #[serde(default)]
42    pub report_group_arns: Vec<String>,
43}
44
45/// Serializable view of a tag.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct TagView {
48    pub key: String,
49    pub value: String,
50}
51
52/// Serializable view of a CodeBuild project.
53#[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    /// `build_batch_config` nested block.
71    #[serde(default)]
72    pub build_batch_config: Option<serde_json::Value>,
73    /// `cache` nested block.
74    #[serde(default)]
75    pub cache: Option<serde_json::Value>,
76    /// `file_system_locations` nested blocks.
77    #[serde(default)]
78    pub file_system_locations: Vec<serde_json::Value>,
79    /// `logs_config` nested block.
80    #[serde(default)]
81    pub logs_config: Option<serde_json::Value>,
82    /// `secondary_artifacts` nested blocks.
83    #[serde(default)]
84    pub secondary_artifacts: Vec<serde_json::Value>,
85    /// `secondary_sources` nested blocks.
86    #[serde(default)]
87    pub secondary_sources: Vec<serde_json::Value>,
88    /// `vpc_config` nested block.
89    #[serde(default)]
90    pub vpc_config: Option<serde_json::Value>,
91}
92
93/// Serializable view of a build phase.
94#[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/// Serializable view of a CodeBuild build.
104#[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/// Serializable view of a CodeBuild webhook.
128#[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/// Serializable view of a source credential.
138#[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/// Serializable view of a report group.
147#[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
160// ---------------------------------------------------------------------------
161// From conversions
162// ---------------------------------------------------------------------------
163
164impl 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
317// ---------------------------------------------------------------------------
318// StatefulService implementation
319// ---------------------------------------------------------------------------
320
321impl 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}