rusty_ci/buildbot/
merge.rs

1use crate::{unwrap, File};
2
3use rusty_yaml::Yaml;
4use std::fmt::{Display, Error, Formatter};
5use std::process::exit;
6
7/// A version control system is a system that allows programmers to manage
8/// changes on a product in development. A few examples include, but are not limited to,
9/// `GitHub`, `GitLab`, `Mercurial`.
10pub enum VersionControlSystem {
11    GitHub,
12    GitLab,
13    Unsupported,
14}
15
16/// This is the path to the file containing the auth / api token
17/// for the version control system
18pub const AUTH_TOKEN_PATH: &str = "auth.token";
19
20/// The purpose of a continuous integration tool is to continuously confirm the
21/// validity and robustness of code. It follows then that you must check code BEFORE
22/// it is deployed. To do this, you must take the code that someone wants to merge into
23/// the repository, and test what the merged code would look like. This struct
24/// allows us to add this functionality to the output buildbot project.
25pub struct MergeRequestHandler {
26    /// A VCS is a verison control system. This is used to determine
27    /// how to tailor the output Python code to the specific VCS.
28    /// In the future, we should implement the abstractions for the
29    /// VCS in the Python, instead of abstracting it in the Rust.
30    /// The VCS, currently, must be one of:
31    /// - github
32    vcs: VersionControlSystem,
33    /// The username of the owner of the repository
34    owner: String,
35    /// The name of the repo
36    /// The name of the rusty-ci repo, for example,
37    /// is just `rusty-ci`, not the entire url.
38    repo_name: String,
39    /// Running code from pull requests is dangerous: the request could contain
40    /// malicious code. To stop anyone from executing arbitrary code on our machines,
41    /// we must have a whitelist. This list contains the usernames of people that the CI
42    /// will run code for.
43    /// If the whitelist contains "adam-mcdaniel", and a user named "adam-mcdaniel" makes
44    /// a pull request on my repository, rusty-ci will run the code in his PR. If his username
45    /// is `im_not_in_the_whitelist` then his code will not be run on our machines until his
46    /// username is added to the whitelist, or a whitelisted user grants permission to test.
47    whitelist: Vec<String>,
48    /// This is the authentication token for the VCS for write access to the repository
49    auth_token: String,
50    /// This field is not to be changed by the user because if youre using something other
51    /// than git, youre doing it wrong :)
52    repository_type: String,
53}
54
55impl MergeRequestHandler {
56    pub fn new(
57        vcs: VersionControlSystem,
58        owner: String,
59        repo_name: String,
60        whitelist: Vec<String>,
61    ) -> Self {
62        let auth_token = match File::read(AUTH_TOKEN_PATH) {
63            Ok(s) => s.trim().to_string(),
64            Err(e) => {
65                error!(
66                    "Could not read authentication token from file '{}' because {}",
67                    AUTH_TOKEN_PATH, e
68                );
69                exit(1);
70            }
71        };
72
73        if auth_token.is_empty() {
74            error!(
75                "You didn't write your VCS's authentication token to '{}'!",
76                AUTH_TOKEN_PATH
77            );
78            exit(0);
79        }
80
81        Self {
82            vcs,
83            owner,
84            repo_name,
85            whitelist,
86            auth_token,
87            repository_type: String::from("git"), // We dont support any other repo type.
88        }
89    }
90}
91
92/// This trait implementation tells rust how to convert a MergeRequestHandler object
93/// into the output python.
94impl Display for MergeRequestHandler {
95    fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
96        match &self.vcs {
97            VersionControlSystem::GitHub => writeln!(
98                f,
99                "whitelist_authors = {:?}
100
101try:
102    c['change_source'].append(changes.GitHubPullrequestPoller(
103            owner=\"{owner}\",
104            repo=\"{name}\",
105            # right now just poll every 60 seconds
106            # this will need to change in the future, but this is just for testing.
107            pollInterval=120,
108            repository_type=\"{repository_type}\",
109            github_property_whitelist=[\"*\"],
110            token=\"{token}\"))
111except Exception as e:
112    print(f\"Could not create merge request handler: {{str(e)}}\")
113
114
115context = util.Interpolate(\"%(prop:buildername)s\")
116github_status_service = reporters.GitHubStatusPush(token='{token}',
117                                context=context,
118                                startDescription='Build started.',
119                                endDescription='Build done.')
120
121c['services'].append(github_status_service)
122
123
124def is_whitelisted(props, password):
125    for prop in ['github.number', 'github.comments_url', 'github.user.login']:
126        # If these properties arent present, its not a pull request
127        if not (props.hasProperty(prop)):
128            return True
129    
130    # URL for comments info
131    comments_url = props['github.comments_url']
132
133    # The pull request number that we'll try to whitelist
134    pr_number = props['github.number']
135
136    # The author of the PR
137    author = props['github.user.login']
138
139    resp = req.get(comments_url)
140    try:
141        # Try to convert to a JSON object so we can read the data
142        json_acceptable_string = resp.text.replace(\"'\", \"\\\"\")
143        comments_json = json.loads(json_acceptable_string)
144
145
146        # Check each comment
147        for comment in comments_json:
148            # If the comment was made by an admin and matches the password
149            if comment['user']['login'] in whitelist_authors and re.fullmatch(password, comment['body']):
150                # If the pull request was not already in the whitelisted PRs, add it
151                print(\"ADMIN: \" + str(comment['user']['login']) + \" PASSWORD: \" + str(comment['body']))
152                print(f\"PR NUMBER {{pr_number}} IS GOOD TO TEST\")
153                return True
154    except Exception as e:
155        # There was a problem converting to JSON, github returned bad data
156        print(f\"There was an error: {{str(e)}}. If this error has anything to do with JSON, its likely that you've queried GitHub too many times.\")
157        # Write the returned webpage to BAD
158        open('BAD', 'w').write(resp.text)
159
160    
161    if author in whitelist_authors:
162        print(\"WHITELISTED AUTHOR\")
163        return True
164
165    return False
166",
167                self.whitelist,
168                token = self.auth_token.trim_matches('"'),
169                name = self.repo_name.trim_matches('"'),
170                owner = self.owner.trim_matches('"'),
171                repository_type = self.repository_type.trim_matches('"'),
172
173            ),
174            VersionControlSystem::GitLab => writeln!(
175                f,
176                "
177def is_whitelisted(props, password): return True
178
179context = util.Interpolate(\"%(prop:buildername)s\")
180gitlab_status_service = reporters.GitLabStatusPush(token='{token}',
181                                context=context,
182                                startDescription='Build started.',
183                                endDescription='Build done.')
184
185c['services'].append(gitlab_status_service)
186               
187",
188                token = self.auth_token.trim_matches('"'),
189            ),
190            VersionControlSystem::Unsupported => writeln!(
191                f,
192                "print('We currently dont support building merge requests on your VCS.')"
193            ),
194        }
195    }
196}
197
198impl From<Yaml> for MergeRequestHandler {
199    fn from(yaml: Yaml) -> Self {
200        // Confirm that the merge request handler has the required sections
201        for section in ["version-control-system", "owner", "repo-name", "whitelist"].iter() {
202            if !yaml.has_section(section) {
203                error!("There was an error creating the merge request handler: '{}' section not specified", section);
204                exit(1);
205            }
206        }
207        // Now that we've verified the required sections exist, continue
208
209        let vcs: VersionControlSystem = match unwrap(&yaml, "version-control-system").as_str() {
210            "github" => VersionControlSystem::GitHub,
211            "gitlab" => VersionControlSystem::GitLab,
212            _ => {
213                warn!(
214                    "We do not support building merge requests on your version control system yet!"
215                );
216                warn!(
217                    "We will proceed with the build. All other features should function as intended."
218                );
219                VersionControlSystem::Unsupported
220            }
221        };
222
223        // Get the username of the owner of the repository
224        let owner: String = unwrap(&yaml, "owner");
225
226        // Get the name of the repository
227        let repo_name: String = unwrap(&yaml, "repo-name");
228
229        // Iterate over the whitelist section to get the names
230        // of the whitelisted authors
231        let mut whitelist: Vec<String> = vec![];
232        for author in yaml.get_section("whitelist").unwrap() {
233            whitelist.push(
234                author
235                    .to_string()
236                    .trim_matches('"')
237                    .trim_matches('\'')
238                    .to_string(),
239            );
240        }
241
242        // Return the constructed Self
243        Self::new(vcs, owner, repo_name, whitelist)
244    }
245}