1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
use base64::encode;
use yaml_rust::yaml::Hash;
use yaml_rust::{EmitError, Yaml, YamlEmitter, YamlLoader};

use crate::{
    parameter::{ParamMap, Parameter, ParameterValues},
    processor::process_yaml,
    secret::{Secret, Secrets},
};

/// A Kubernetes manifest template and the values for each of its parameters.
#[derive(Debug)]
pub struct Template {
    objects: Vec<Yaml>,
    param_map: ParamMap,
    secrets: Option<Secrets>,
}

impl Template {
    /// Creates a new template.
    ///
    /// # Parameters
    ///
    /// * template_contents: The YAML template file's contents.
    /// * parameter_values: A map of the template's parameters and the user-supplied values for
    ///   each.
    /// * secrets: A list of Kubernetes secrets whose data keys should be Base64 encoded after
    ///   parameter interpolation.
    ///
    /// # Errors
    ///
    /// Returns an error if:
    ///
    /// * There was more than one YAML document present in the template contents.
    /// * The YAML document did not contain an "objects" key or it was not an array value.
    /// * The YAML document did not contain a "parameters" key or it was not an array value.
    /// * One of the parameters doesn't have a "name" key.
    /// * One of the parameters specifies an invalid "parameterType".
    /// * One of the parameters requires a value which wasn't supplied.
    /// * Any of the provided secrets were not found in the template.
    /// * There was an error in the structure of a secret that prevented its data from being Base64
    /// encoded.
    pub fn new(
        template_contents: String,
        parameter_values: ParameterValues,
        secrets: Option<Secrets>,
    ) -> Result<Self, String> {
        let docs = YamlLoader::load_from_str(&template_contents)
            .map_err(|err| err.to_string())?;

        if docs.len() != 1 {
            return Err("Only one YAML document can be present in the template.".to_owned());
        }

        let doc = &docs[0];

        let mut template_objects = vec![];
        let objects = match doc["objects"].as_vec() {
            Some(objects) => objects,
            None => return Err("Key \"objects\" must be present and must be an array.".to_owned()),
        };

        for object in objects {
            template_objects.push(object.clone());
        }

        let mut param_map = ParamMap::new();
        let parameter_specs = match doc["parameters"].as_vec() {
            Some(parameter_specs) => parameter_specs,
            None => {
                return Err("Key \"parameters\" must be present and must be an array.".to_owned())
            }
        };

        for parameter_spec in parameter_specs {
            let parameter = Parameter::new(parameter_spec, &parameter_values)?;

            param_map.insert(parameter.name.clone(), parameter);
        }

        Ok(Template {
            objects: template_objects,
            param_map: param_map,
            secrets: secrets,
        })
    }

    /// Interpolates the parameters' values into the YAML template, returning the results.
    ///
    /// # Errors
    ///
    /// Returns an error if the processed template was not valid YAML, or if any specified secrets
    /// could not be found and Base64 encoded.
    pub fn process(mut self) -> Result<String, String> {
        let mut secrets_encoded = 0;

        for object in self.objects.iter_mut() {
            process_yaml(object, &self.param_map);

            if let Some(ref secrets) = self.secrets {
                if maybe_base64_encode_secret(secrets, object)? {
                    secrets_encoded += 1;
                }
            }
        }

        if let Some(ref secrets) = self.secrets {
            if secrets_encoded != secrets.len() {
                return Err("Not all secrets specified were found.".to_string());
            }
        }

        dump(self.objects)
    }
}

fn maybe_base64_encode_secret(secrets: &Secrets, object: &mut Yaml) -> Result<bool, String> {
    let hash = match object {
        &mut Yaml::Hash(ref mut hash) => hash,
        _ => return Ok(false),
    };

    if let Some(kind) = hash.get(&Yaml::String("kind".to_string())) {
        match kind {
            &Yaml::String(ref kind_string) => {
                if kind_string != "Secret" {
                    return Ok(false);
                }
            }
            _ => {
                return Err(
                    "Encountered a resource with a non-string value for the \"kind\" field."
                        .to_string(),
                )
            }
        }
    } else {
        return Err("Encountered a resource without a \"kind\" field.".to_string());
    }

    let metadata = match hash.get(&Yaml::String("metadata".to_string())) {
        Some(&Yaml::Hash(ref metadata)) => metadata.clone(),
        Some(_) => {
            return Err("Encountered a resource with a non-hash \"metadata\" field.".to_string())
        }
        None => return Err("Encountered a resource without a \"metadata\" field.".to_string()),
    };

    let name = match metadata.get(&ystring("name")) {
        Some(&Yaml::String(ref name)) => name.to_string(),
        Some(_) => {
            return Err(
                "Encountered a resource with a non-string \"metadata.name\" field.".to_string(),
            )
        }
        None => return Err("Encountered a resource without a \"metadata.name\" field.".to_string()),
    };

    let namespace = match metadata.get(&ystring("namespace")) {
        Some(&Yaml::String(ref namespace)) => namespace.to_string(),
        Some(_) => {
            return Err(
                "Encountered a resource with a non-string \"metadata.namespace\" field."
                    .to_string(),
            )
        }
        None => "default".to_string(),
    };

    let secret = Secret {
        name: name,
        namespace: namespace,
    };

    if secrets.contains(&secret) {
        if let Some(data) = hash.get_mut(&ystring("data")) {
            match data {
                &mut Yaml::Hash(ref mut data_hash) => {
                    base64_encode_secret_data(data_hash)?;
                    return Ok(true);
                }
                _ => return Err("Encountered secret with non-hash \"data\" field.".to_string()),
            }
        }
    }

    return Ok(false);
}
fn base64_encode_secret_data(data: &mut Hash) -> Result<(), String> {
    for (_, value) in data.iter_mut() {
        let encoded = match value {
            &mut Yaml::String(ref value_string) => encode(value_string.as_bytes()),
            _ => return Err("Encountered non-string secret data value.".to_string()),
        };

        *value = ystring(&encoded);
    }

    Ok(())
}

fn dump(objects: Vec<Yaml>) -> Result<String, String> {
    let mut manifests = String::new();
    let last = objects.len() - 1;

    for (i, object) in objects.iter().enumerate() {
        {
            let mut emitter = YamlEmitter::new(&mut manifests);
            emitter.dump(&object).map_err(|error| match error {
                EmitError::FmtError(error) => format!("{}", error),
                EmitError::BadHashmapKey => "Bad hashmap key in YAML structure.".to_owned(),
            })?;
        }

        if i != last {
            manifests.push_str("\n");
        }
    }

    Ok(manifests)
}

fn ystring(s: &str) -> Yaml {
    Yaml::String(s.to_string())
}