Sergey Otpuschennikov | fc3a383 | 2020-09-04 16:07:37 +0400 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | |
| 3 | """ |
| 4 | Those tests checks the following requirements for the `projects.yaml` file: |
| 5 | - Its syntax is valid |
| 6 | - Each project definition should consist of the following mandatory parts: |
| 7 | * project |
| 8 | * description |
| 9 | and could contain the following optional parts: |
| 10 | * acl-config |
| 11 | * upstream |
| 12 | No other parts are possible. |
| 13 | - All the projects listed in the `projects.yaml` file |
| 14 | must be sorted alphabetically. |
| 15 | """ |
| 16 | |
| 17 | import logging |
| 18 | import os |
| 19 | import sys |
| 20 | |
| 21 | import git |
| 22 | import jsonschema |
| 23 | import yaml |
| 24 | |
| 25 | |
| 26 | logging.basicConfig(level=logging.INFO) |
| 27 | |
| 28 | # Only lower case letters (a-z), digits (0-9), plus (+) and minus (-) |
| 29 | # and periods (.). |
| 30 | # They must be at least two characters long and must start with an |
| 31 | # alphanumeric character. |
| 32 | # https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Source |
| 33 | # Additionally allow using underline symbol (required by openstack projects) |
| 34 | |
| 35 | _PREFIX_PATTERN = '\A([a-z]([a-z]|\d|-)+/)*' |
| 36 | _NAMES_PATTERN = '([a-zA-Z]|\d)([a-zA-Z]|\d|[+-_.])+\Z' |
| 37 | PROJECT_NAME_PATTERN = _PREFIX_PATTERN + _NAMES_PATTERN |
| 38 | |
| 39 | PROJECT_SCHEMA = { |
| 40 | "$schema": "http://json-schema.org/draft-04/schema#", |
| 41 | "type": "array", |
| 42 | "items": { |
| 43 | "type": "object", |
| 44 | "additionalProperties": False, |
| 45 | "properties": { |
| 46 | "project": { |
| 47 | "type": "string", |
| 48 | "pattern": PROJECT_NAME_PATTERN |
| 49 | }, |
| 50 | "description": { |
| 51 | "type": "string" |
| 52 | }, |
| 53 | "upstream": { |
| 54 | "type": "string" |
| 55 | }, |
| 56 | |
| 57 | "acl-config": { |
| 58 | "type": "string" |
| 59 | } |
| 60 | }, |
| 61 | "required": ["project", "description"] |
| 62 | } |
| 63 | } |
| 64 | |
| 65 | |
| 66 | def parse_yaml_file(file_path): |
| 67 | try: |
| 68 | data = yaml.safe_load(open(file_path)) |
| 69 | if data is None: |
| 70 | logging.error("File {0} is empty".format(file_path)) |
| 71 | sys.exit(1) |
| 72 | return data |
| 73 | except yaml.YAMLError as exc: |
| 74 | msg = "File {0} could not be parsed: {1}".format(file_path, exc) |
| 75 | logging.error(msg) |
| 76 | sys.exit(1) |
| 77 | |
| 78 | |
| 79 | def validate_data_by_schema(data, file_path): |
| 80 | try: |
| 81 | jsonschema.validate(data, PROJECT_SCHEMA) |
| 82 | except jsonschema.exceptions.ValidationError as exc: |
| 83 | raise ValueError(_make_error_message(exc, file_path)) |
| 84 | |
| 85 | |
| 86 | def _make_error_message(exc, file_path): |
| 87 | value_path = [] |
| 88 | |
| 89 | if exc.absolute_path: |
| 90 | value_path.extend(exc.absolute_path) |
| 91 | |
| 92 | error_msg = "File '{0}', {1}".format(file_path, exc.message) |
| 93 | |
| 94 | if value_path: |
| 95 | value_path = ' -> '.join(map(str, value_path)) |
| 96 | error_msg = '{0}, value path {1}'.format(error_msg, value_path) |
| 97 | |
| 98 | return error_msg |
| 99 | |
| 100 | |
| 101 | def check_duplicate_projects(data): |
| 102 | projects_items = [] |
| 103 | for item in data: |
| 104 | if item['project'] not in projects_items: |
| 105 | projects_items.append(item['project']) |
| 106 | else: |
| 107 | msg = "Project '{0}' is duplicated".format(item['project']) |
| 108 | raise ValueError(msg) |
| 109 | |
| 110 | |
| 111 | def check_alphabetical_order(data): |
| 112 | for i in range(len(data) - 1): |
| 113 | if not data[i]['project'] < data[i + 1]['project']: |
| 114 | msg = ("Alphabetical order violation: project '{0}' must be " |
| 115 | "placed after '{1}'".format(data[i]['project'], |
| 116 | data[i + 1]['project'])) |
| 117 | raise ValueError(msg) |
| 118 | |
| 119 | |
| 120 | def check_acls_config_path(data): |
| 121 | valid = True |
| 122 | |
| 123 | for item in data: |
| 124 | acl_config_path = item.get('acl-config') |
| 125 | if not acl_config_path: |
| 126 | continue |
| 127 | # Allow to skip acls/ prefix in acl-config |
| 128 | if acl_config_path[:4] != 'acls': |
| 129 | acl_config_path = os.path.join('acls', acl_config_path) |
| 130 | |
| 131 | config_path = os.path.join(os.path.abspath(os.curdir), |
| 132 | acl_config_path) |
| 133 | if not os.path.isfile(config_path): |
| 134 | logging.error("Config file for project '{0}' is not found " |
| 135 | "at {1}.".format(item.get('project'), config_path)) |
| 136 | valid = False |
| 137 | if not valid: |
| 138 | sys.exit(1) |
| 139 | |
| 140 | |
| 141 | def check_upstream_clonable(data): |
| 142 | clonable = True |
| 143 | |
| 144 | for item in data: |
| 145 | upstream_repo = item.get('upstream') |
| 146 | if not upstream_repo: |
| 147 | continue |
| 148 | logging.info("Checking availability: {0}".format(upstream_repo)) |
| 149 | try: |
| 150 | g = git.cmd.Git() |
| 151 | g.ls_remote(upstream_repo) |
| 152 | except git.exc.GitCommandError as e: |
| 153 | err_msg = ("Unable to clone '{0}':" |
| 154 | "{1}".format(upstream_repo, str(e))) |
| 155 | logging.error(err_msg) |
| 156 | clonable = False |
| 157 | |
| 158 | if not clonable: |
| 159 | sys.exit(1) |
| 160 | |
| 161 | |
| 162 | def run_checks(file_to_check): |
| 163 | data = parse_yaml_file(file_to_check) |
| 164 | validate_data_by_schema(data, file_to_check) |
| 165 | check_duplicate_projects(data) |
| 166 | check_alphabetical_order(data) |
| 167 | check_acls_config_path(data) |
| 168 | #check_upstream_clonable(data) |
| 169 | |
| 170 | |
| 171 | if __name__ == '__main__': |
| 172 | if len(sys.argv) < 2: |
| 173 | sys.stderr.write("Usage: {0} path/to/projects.yaml" |
| 174 | "\n".format(sys.argv[0])) |
| 175 | sys.exit(1) |
| 176 | run_checks(sys.argv[1]) |