omicron_zone_package/config/
imp.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Configuration for a package.
6
7use crate::package::{Package, PackageOutput, PackageSource};
8use crate::target::TargetMap;
9use serde_derive::Deserialize;
10use serde_derive::Serialize;
11use std::collections::BTreeMap;
12use std::path::Path;
13use thiserror::Error;
14use topological_sort::TopologicalSort;
15
16use super::{PackageName, PresetName};
17
18/// Describes a set of packages to act upon.
19///
20/// This structure maps "package name" to "package"
21pub struct PackageMap<'a>(pub BTreeMap<&'a PackageName, &'a Package>);
22
23// The name of a file which should be created by building a package.
24#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
25struct OutputFile(String);
26
27impl<'a> PackageMap<'a> {
28    pub fn build_order(&self) -> PackageDependencyIter<'a> {
29        let lookup_by_output = self
30            .0
31            .iter()
32            .map(|(name, package)| (OutputFile(package.get_output_file(name)), (*name, *package)))
33            .collect::<BTreeMap<_, _>>();
34
35        // Collect all packages, and sort them in dependency order,
36        // so we know which ones to build first.
37        let mut outputs = TopologicalSort::<OutputFile>::new();
38        for (package_output, (_, package)) in &lookup_by_output {
39            match &package.source {
40                PackageSource::Local { .. }
41                | PackageSource::Prebuilt { .. }
42                | PackageSource::Manual => {
43                    // Skip intermediate leaf packages; if necessary they'll be
44                    // added to the dependency graph by whatever composite package
45                    // actually depends on them.
46                    if !matches!(
47                        package.output,
48                        PackageOutput::Zone {
49                            intermediate_only: true
50                        }
51                    ) {
52                        outputs.insert(package_output.clone());
53                    }
54                }
55                PackageSource::Composite { packages: deps } => {
56                    for dep in deps {
57                        outputs.add_dependency(OutputFile(dep.clone()), package_output.clone());
58                    }
59                }
60            }
61        }
62
63        PackageDependencyIter {
64            lookup_by_output,
65            outputs,
66        }
67    }
68}
69
70/// Returns all packages in the order in which they should be built.
71///
72/// Returns packages in batches that may be built concurrently.
73pub struct PackageDependencyIter<'a> {
74    lookup_by_output: BTreeMap<OutputFile, (&'a PackageName, &'a Package)>,
75    outputs: TopologicalSort<OutputFile>,
76}
77
78impl<'a> Iterator for PackageDependencyIter<'a> {
79    type Item = Vec<(&'a PackageName, &'a Package)>;
80
81    fn next(&mut self) -> Option<Self::Item> {
82        if self.outputs.is_empty() {
83            return None;
84        }
85        let batch = self.outputs.pop_all();
86        assert!(
87            !batch.is_empty() || self.outputs.is_empty(),
88            "cyclic dependency in package manifest!"
89        );
90
91        Some(
92            batch
93                .into_iter()
94                .map(|output| {
95                    *self.lookup_by_output.get(&output).unwrap_or_else(|| {
96                        panic!("Could not find a package which creates '{}'", output.0)
97                    })
98                })
99                .collect(),
100        )
101    }
102}
103
104/// Describes the configuration for a set of packages.
105#[derive(Clone, Deserialize, Serialize, Debug)]
106pub struct Config {
107    /// Packages to be built and installed.
108    #[serde(default, rename = "package")]
109    pub packages: BTreeMap<PackageName, Package>,
110
111    /// Target configuration.
112    #[serde(default)]
113    pub target: TargetConfig,
114}
115
116impl Config {
117    /// Returns target packages to be assembled on the builder machine.
118    pub fn packages_to_build(&self, target: &TargetMap) -> PackageMap<'_> {
119        PackageMap(
120            self.packages
121                .iter()
122                .filter(|(_, pkg)| target.includes_package(pkg))
123                .collect(),
124        )
125    }
126
127    /// Returns target packages which should execute on the deployment machine.
128    pub fn packages_to_deploy(&self, target: &TargetMap) -> PackageMap<'_> {
129        let all_packages = self.packages_to_build(target).0;
130        PackageMap(
131            all_packages
132                .into_iter()
133                .filter(|(_, pkg)| match pkg.output {
134                    PackageOutput::Zone { intermediate_only } => !intermediate_only,
135                    PackageOutput::Tarball => true,
136                })
137                .collect(),
138        )
139    }
140}
141
142/// Configuration for targets, including preset configuration.
143#[derive(Clone, Deserialize, Serialize, Debug, Default)]
144pub struct TargetConfig {
145    /// Preset configuration for targets.
146    #[serde(default, rename = "preset")]
147    pub presets: BTreeMap<PresetName, TargetMap>,
148}
149
150/// Errors which may be returned when parsing the server configuration.
151#[derive(Error, Debug)]
152pub enum ParseError {
153    #[error("Cannot parse toml: {0}")]
154    Toml(#[from] toml::de::Error),
155    #[error("IO error: {0}")]
156    Io(#[from] std::io::Error),
157}
158
159/// Parses a manifest into a package [`Config`].
160pub fn parse_manifest(manifest: &str) -> Result<Config, ParseError> {
161    let cfg = toml::from_str::<Config>(manifest)?;
162    Ok(cfg)
163}
164/// Parses a path in the filesystem into a package [`Config`].
165pub fn parse<P: AsRef<Path>>(path: P) -> Result<Config, ParseError> {
166    let contents = std::fs::read_to_string(path.as_ref())?;
167    parse_manifest(&contents)
168}
169
170#[cfg(test)]
171mod test {
172    use crate::config::ServiceName;
173
174    use super::*;
175
176    #[test]
177    fn test_order() {
178        let pkg_a_name = PackageName::new_const("pkg-a");
179        let pkg_a = Package {
180            service_name: ServiceName::new_const("a"),
181            source: PackageSource::Manual,
182            output: PackageOutput::Tarball,
183            only_for_targets: None,
184            setup_hint: None,
185        };
186
187        let pkg_b_name = PackageName::new_const("pkg-b");
188        let pkg_b = Package {
189            service_name: ServiceName::new_const("b"),
190            source: PackageSource::Composite {
191                packages: vec![pkg_a.get_output_file(&pkg_a_name)],
192            },
193            output: PackageOutput::Tarball,
194            only_for_targets: None,
195            setup_hint: None,
196        };
197
198        let cfg = Config {
199            packages: BTreeMap::from([
200                (pkg_a_name.clone(), pkg_a.clone()),
201                (pkg_b_name.clone(), pkg_b.clone()),
202            ]),
203            target: TargetConfig::default(),
204        };
205
206        let mut order = cfg.packages_to_build(&TargetMap::default()).build_order();
207        // "pkg-a" comes first, because "pkg-b" depends on it.
208        assert_eq!(order.next(), Some(vec![(&pkg_a_name, &pkg_a)]));
209        assert_eq!(order.next(), Some(vec![(&pkg_b_name, &pkg_b)]));
210    }
211
212    // We're kinda limited by the topological-sort library here, as this is a documented
213    // behavior from [TopologicalSort::pop_all].
214    //
215    // Regardless, test that circular dependencies cause panics.
216    #[test]
217    #[should_panic(expected = "cyclic dependency in package manifest")]
218    fn test_cyclic_dependency() {
219        let pkg_a_name = PackageName::new_const("pkg-a");
220        let pkg_b_name = PackageName::new_const("pkg-b");
221        let pkg_a = Package {
222            service_name: ServiceName::new_const("a"),
223            source: PackageSource::Composite {
224                packages: vec![String::from("pkg-b.tar")],
225            },
226            output: PackageOutput::Tarball,
227            only_for_targets: None,
228            setup_hint: None,
229        };
230        let pkg_b = Package {
231            service_name: ServiceName::new_const("b"),
232            source: PackageSource::Composite {
233                packages: vec![String::from("pkg-a.tar")],
234            },
235            output: PackageOutput::Tarball,
236            only_for_targets: None,
237            setup_hint: None,
238        };
239
240        let cfg = Config {
241            packages: BTreeMap::from([
242                (pkg_a_name.clone(), pkg_a.clone()),
243                (pkg_b_name.clone(), pkg_b.clone()),
244            ]),
245            target: TargetConfig::default(),
246        };
247
248        let mut order = cfg.packages_to_build(&TargetMap::default()).build_order();
249        order.next();
250    }
251
252    // Make pkg-a depend on pkg-b.tar, but don't include pkg-b.tar anywhere.
253    //
254    // Ensure that we see an appropriate panic.
255    #[test]
256    #[should_panic(expected = "Could not find a package which creates 'pkg-b.tar'")]
257    fn test_missing_dependency() {
258        let pkg_a_name = PackageName::new_const("pkg-a");
259        let pkg_a = Package {
260            service_name: ServiceName::new_const("a"),
261            source: PackageSource::Composite {
262                packages: vec![String::from("pkg-b.tar")],
263            },
264            output: PackageOutput::Tarball,
265            only_for_targets: None,
266            setup_hint: None,
267        };
268
269        let cfg = Config {
270            packages: BTreeMap::from([(pkg_a_name.clone(), pkg_a.clone())]),
271            target: TargetConfig::default(),
272        };
273
274        let mut order = cfg.packages_to_build(&TargetMap::default()).build_order();
275        order.next();
276    }
277}