spin_doctor/manifest/
upgrade.rs

1use anyhow::{anyhow, Context, Result};
2use async_trait::async_trait;
3use spin_common::ui::quoted_path;
4use spin_manifest::{compat::v1_to_v2_app, schema::v1::AppManifestV1, ManifestVersion};
5use toml_edit::{de::from_document, ser::to_document, Item, Table};
6
7use crate::{Diagnosis, Diagnostic, PatientApp, Treatment};
8
9/// UpgradeDiagnostic detects old manifest versions and upgrades them.
10#[derive(Default)]
11pub struct UpgradeDiagnostic;
12
13#[async_trait]
14impl Diagnostic for UpgradeDiagnostic {
15    type Diagnosis = UpgradeDiagnosis;
16
17    async fn diagnose(&self, patient: &PatientApp) -> Result<Vec<Self::Diagnosis>> {
18        Ok(
19            match ManifestVersion::detect(&patient.manifest_doc.to_string())? {
20                ManifestVersion::V1 => vec![UpgradeDiagnosis],
21                _ => vec![],
22            },
23        )
24    }
25}
26
27/// UpgradeDiagnosis represents an upgradable manifest.
28#[derive(Debug)]
29pub struct UpgradeDiagnosis;
30
31impl Diagnosis for UpgradeDiagnosis {
32    fn description(&self) -> String {
33        "Version 1 manifest can be upgraded to version 2".into()
34    }
35
36    fn is_critical(&self) -> bool {
37        false
38    }
39
40    fn treatment(&self) -> Option<&dyn crate::Treatment> {
41        Some(self)
42    }
43}
44
45#[async_trait]
46impl Treatment for UpgradeDiagnosis {
47    fn summary(&self) -> String {
48        "Upgrade manifest to version 2".into()
49    }
50
51    async fn treat(&self, patient: &mut PatientApp) -> Result<()> {
52        let v1: AppManifestV1 = from_document(patient.manifest_doc.clone())
53            .context("failed to decode AppManifestV1")?;
54        let v2 = v1_to_v2_app(v1).context("failed to upgrade version 1 manifest to version 2")?;
55        let mut v2_doc = to_document(&v2)?;
56
57        // Format [application] table
58        let application = uninline_table(&mut v2_doc["application"])?;
59        if let Some(application_trigger) = application.get_mut("trigger") {
60            let application_trigger = uninline_table(application_trigger)?;
61            application_trigger.set_dotted(true);
62            for (_, trigger_config) in application_trigger.iter_mut() {
63                uninline_table(trigger_config)?.set_implicit(true);
64            }
65        }
66
67        // Format [variables]
68        if let Some(variables) = v2_doc.get_mut("variables") {
69            uninline_table(variables)?;
70        }
71
72        // Format [[trigger.*]] tables
73        if let Some(triggers) = v2_doc.get_mut("trigger") {
74            let triggers = uninline_table(triggers)?;
75            triggers.set_dotted(true);
76            for (_, typed_triggers) in triggers.iter_mut() {
77                *typed_triggers = Item::ArrayOfTables(
78                    std::mem::take(typed_triggers)
79                        .into_array_of_tables()
80                        .map_err(expected_table)?,
81                );
82            }
83        }
84
85        // Format [component.*] tables
86        if let Some(components) = v2_doc.get_mut("component") {
87            let components = uninline_table(components)?;
88            components.set_dotted(true);
89            for (_, component) in components.iter_mut() {
90                let component = uninline_table(component)?;
91                if let Some(build) = component.get_mut("build") {
92                    uninline_table(build)?;
93                }
94            }
95        }
96
97        // Back-up original V1 manifest
98        let v1_backup_path = patient.manifest_path.with_extension("toml.v1_backup");
99        std::fs::rename(&patient.manifest_path, &v1_backup_path)
100            .context("failed to back up existing manifest")?;
101        println!(
102            "Version 1 manifest backed up to {}.",
103            quoted_path(&v1_backup_path)
104        );
105
106        // Write new V2 manifest
107        std::fs::write(&patient.manifest_path, v2_doc.to_string())
108            .context("failed to write version 2 manifest")?;
109        patient.manifest_doc = v2_doc;
110
111        Ok(())
112    }
113}
114
115fn uninline_table(item: &mut Item) -> Result<&mut Table> {
116    *item = Item::Table(std::mem::take(item).into_table().map_err(expected_table)?);
117    Ok(item.as_table_mut().unwrap())
118}
119
120fn expected_table(got: Item) -> anyhow::Error {
121    anyhow!("expected table, got {}", got.type_name())
122}