shrub_rs/models/
project.rs

1//! Evergreen Project are the top level configuration for an Evergreen landscape.
2//!
3//! See Evergreen's
4//! [documentation](https://github.com/evergreen-ci/evergreen/wiki/Project-Configuration-Files)
5//! for more details on how a projects configuration.
6use crate::models::builtin::EvgCommandType;
7use crate::models::commands::EvgCommand;
8use crate::models::task::EvgTask;
9use crate::models::task_group::EvgTaskGroup;
10use crate::models::variant::BuildVariant;
11use serde::{Deserialize, Serialize};
12use simple_error::bail;
13use std::{collections::HashMap, error::Error};
14use yaml_merge_keys::merge_keys;
15use yaml_rust::{YamlEmitter, YamlLoader};
16
17/// Description of an evergreen parameter.
18///
19/// Parameters allow patch builds to specific customized behavior.
20/// See Evergreen's
21/// [Parameterized Builds](https://github.com/evergreen-ci/evergreen/wiki/Parameterized-Builds)
22/// documentation for more information.
23#[derive(Serialize, Deserialize, Debug, Clone)]
24pub struct EvgParameter {
25    /// Name of parameter.
26    pub key: String,
27    /// Default value to use for parameter.
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub value: Option<String>,
30    /// Description of parameter.
31    pub description: String,
32}
33
34/// Description of a module to include in a landscape.
35#[derive(Serialize, Deserialize, Debug, Clone)]
36pub struct EvgModule {
37    /// Name of module being defined.
38    pub name: String,
39    /// Repository containing module to be included.
40    pub repo: String,
41    /// Branch of repository to use.
42    pub branch: String,
43    /// Path to store module code at.
44    pub prefix: String,
45}
46
47/// Definition of an Evergreen function.
48#[derive(Serialize, Deserialize, Debug, Clone)]
49#[serde(untagged)]
50pub enum FunctionDefinition {
51    /// Function composed of a single Evergreen command.
52    SingleCommand(EvgCommand),
53    /// Function composed of several Evergreen commands.
54    CommandList(Vec<EvgCommand>),
55}
56
57/// Description of an Evergreen Project.
58#[derive(Serialize, Deserialize, Debug, Default)]
59pub struct EvgProject {
60    /// List of build variants belonging to this landscape.
61    pub buildvariants: Vec<BuildVariant>,
62    /// List of task definitions.
63    pub tasks: Vec<EvgTask>,
64    /// List of task group definitions.
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub task_groups: Option<Vec<EvgTaskGroup>>,
67    /// Definitions of functions belonging to this landscape.
68    #[serde(skip_serializing_if = "HashMap::is_empty")]
69    pub functions: HashMap<String, FunctionDefinition>,
70    /// List of commands to run at the start of each task.
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub pre: Option<Vec<EvgCommand>>,
73    /// List of commands to run at the end of each task.
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub post: Option<Vec<EvgCommand>>,
76    /// List of commands to run whenever a task hits a timeout.
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub timeout: Option<Vec<EvgCommand>>,
79
80    /// Description of modules to include in this landscape.
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub modules: Option<Vec<EvgModule>>,
83
84    /// Describe if skipped tasks should be run on failures to determine source of failure.
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub stepback: Option<bool>,
87    /// Describe if failures in `pre` commands should cause a task to be failed.
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub pre_error_fails_task: Option<bool>,
90    /// Describe if evergreen should track out of memory failure in this landscape.
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub oom_tracker: Option<bool>,
93    /// Describe the type of failure a task failure should trigger.
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub command_type: Option<EvgCommandType>,
96    /// List of globs that describe file changes that won't trigger a new build.
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub ignore: Option<Vec<String>>,
99    /// Parameters that can be specified to customize patch build functionality.
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub parameters: Option<Vec<EvgParameter>>,
102}
103
104impl EvgProject {
105    /// Parse the given YAML string into an Evergreen Project.
106    pub fn from_yaml_str(yaml_contents: &str) -> Result<EvgProject, Box<dyn Error>> {
107        // Evergreen config can use merge-keys, which is not supported by
108        // serde-yaml, so we need to merge the keys first.
109        let mut raw = YamlLoader::load_from_str(yaml_contents)?;
110        if raw.len() != 1 {
111            bail!("Expected 1 and only 1 yaml document")
112        }
113        let raw = raw.remove(0);
114        let merged = merge_keys(raw)?;
115
116        let mut out_str = String::new();
117        {
118            let mut emitter = YamlEmitter::new(&mut out_str);
119            emitter.dump(&merged)?;
120        }
121
122        Ok(serde_yaml::from_str(&out_str)?)
123    }
124
125    /// Build a map of the defined build variants.
126    pub fn build_variant_map(&self) -> HashMap<String, &BuildVariant> {
127        let mut map = HashMap::with_capacity(self.buildvariants.len());
128        self.buildvariants.iter().for_each(|bv| {
129            map.insert(bv.name.to_string(), bv);
130        });
131        map
132    }
133
134    /// Build a map of the defined tasks.
135    pub fn task_def_map(&self) -> HashMap<String, &EvgTask> {
136        let mut map = HashMap::with_capacity(self.tasks.len());
137        self.tasks.iter().for_each(|t| {
138            map.insert(t.name.to_string(), t);
139        });
140        map
141    }
142}
143
144#[cfg(test)]
145mod test {
146    use super::*;
147
148    #[test]
149    fn test_an_empty_document_fails() {
150        let document = "";
151
152        let result = EvgProject::from_yaml_str(document);
153        assert!(result.is_err());
154    }
155
156    #[test]
157    fn test_invalid_yaml_fails() {
158        let document = "garbage input";
159
160        let result = EvgProject::from_yaml_str(document);
161        assert!(result.is_err());
162    }
163    #[test]
164    fn test_valid_document() {
165        let document = r#"
166functions:
167  "example function" :
168    - command: git.get_project
169      params:
170        directory: src
171    - command: shell.exec
172      params:
173        working_dir: src
174        script: |
175          ls
176    - command: s3.put
177      params:
178        aws_key: "test_key"
179        aws_secret: "test_secret"
180        remote_file: remote_file
181        bucket: bucket-name
182        permissions: private
183        local_file: local-file
184        content_type: application/gzip
185        skip_existing: "true"
186    - command: s3.put
187      params:
188        aws_key: "test_key"
189        aws_secret: "test_secret"
190        aws_session_token: "test_token"
191        remote_file: remote_file
192        bucket: bucket-name
193        permissions: private
194        local_file: local-file
195        content_type: application/gzip
196        skip_existing: "true"
197    - command: s3.put
198      params:
199        role_arn: my_arn
200        remote_file: remote_file
201        bucket: bucket-name
202        permissions: private
203        local_file: local-file
204        content_type: application/gzip
205        skip_existing: true
206
207tasks:
208- name: compile
209  depends_on: []
210  commands:
211      - func: "example function"
212
213buildvariants:
214- name: ubuntu
215  display_name: Ubuntu
216  modules: ["render-module"]
217  run_on:
218  - ubuntu1404-test
219  expansions:
220    test_flags: "blah blah"
221  tasks:
222  - name: compile
223"#;
224
225        let result = EvgProject::from_yaml_str(document);
226        assert!(result.is_ok());
227    }
228}