libcoreinst/io/
ignition.rs

1// Copyright 2021 Red Hat
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use anyhow::{bail, Context, Result};
16use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
17use flate2::read::GzEncoder;
18use flate2::Compression;
19use ignition_config as ign_multi;
20use ignition_config::v3_3 as ign;
21use std::io::Read;
22
23#[derive(Debug, Default)]
24pub struct Ignition {
25    config: ign::Config,
26}
27
28impl Ignition {
29    pub fn merge_config(&mut self, config: &ign_multi::Config) -> Result<()> {
30        let buf = serde_json::to_vec(config).context("serializing child Ignition config")?;
31        self.config
32            .ignition
33            .config
34            .get_or_insert_with(Default::default)
35            .merge
36            .get_or_insert_with(Default::default)
37            .push(make_resource(&buf)?);
38        Ok(())
39    }
40
41    pub fn add_file(&mut self, path: String, data: &[u8], mode: i64) -> Result<()> {
42        // Perform the same alias check that Ignition config validation does.
43        // This doesn't catch aliases known only at runtime, such as
44        // /usr/local and /var/usrlocal.
45        if self.have_path(&path) {
46            bail!("config already specifies path {}", path);
47        }
48        self.config
49            .storage
50            .get_or_insert_with(Default::default)
51            .files
52            .get_or_insert_with(Default::default)
53            .push(ign::File {
54                contents: Some(make_resource(data)?),
55                mode: Some(mode),
56                ..ign::File::new(path)
57            });
58        Ok(())
59    }
60
61    pub fn add_unit(&mut self, name: String, contents: String, enabled: bool) -> Result<()> {
62        let units = self
63            .config
64            .systemd
65            .get_or_insert_with(Default::default)
66            .units
67            .get_or_insert_with(Default::default);
68        if units.iter().any(|u| u.name == name) {
69            bail!("config already specifies unit {}", name);
70        }
71        units.push(ign::Unit {
72            contents: Some(contents),
73            enabled: Some(enabled),
74            ..ign::Unit::new(name)
75        });
76        Ok(())
77    }
78
79    pub fn add_ca(&mut self, data: &[u8]) -> Result<()> {
80        self.config
81            .ignition
82            .security
83            .get_or_insert_with(Default::default)
84            .tls
85            .get_or_insert_with(Default::default)
86            .certificate_authorities
87            .get_or_insert_with(Default::default)
88            .push(make_resource(data)?);
89        Ok(())
90    }
91
92    pub fn to_bytes(&self) -> Result<Vec<u8>> {
93        let mut json = serde_json::to_vec(&self.config).context("serializing Ignition config")?;
94        json.push(b'\n');
95        Ok(json)
96    }
97
98    fn have_path(&self, path: &str) -> bool {
99        let storage = self.config.storage.clone().unwrap_or_default();
100        storage
101            .files
102            .unwrap_or_default()
103            .iter()
104            .map(|f| &f.path)
105            .chain(
106                storage
107                    .directories
108                    .unwrap_or_default()
109                    .iter()
110                    .map(|d| &d.path),
111            )
112            .chain(storage.links.unwrap_or_default().iter().map(|l| &l.path))
113            .any(|p| p == path)
114    }
115}
116
117fn make_resource(data: &[u8]) -> Result<ign::Resource> {
118    let mut compressed = Vec::new();
119    GzEncoder::new(data, Compression::best()).read_to_end(&mut compressed)?;
120    Ok(ign::Resource {
121        source: Some(format!("data:;base64,{}", BASE64.encode(&compressed))),
122        compression: Some("gzip".into()),
123        ..Default::default()
124    })
125}
126
127#[cfg(test)]
128mod test {
129    use super::*;
130
131    #[test]
132    fn duplicate_path() {
133        let mut ignition = Ignition::default();
134        ignition.add_file("/a/b".into(), &[], 0o755).unwrap();
135        ignition.add_file("/a/b".into(), &[], 0o755).unwrap_err();
136    }
137}