k8s.io/test-infra@v0.0.0-20240520184403-27c6b4c223d8/experiment/migrate_testgrid_tabs.py (about) 1 #!/usr/bin/env python3 2 3 # Copyright 2019 The Kubernetes Authors. 4 # 5 # Licensed under the Apache License, Version 2.0 (the "License"); 6 # you may not use this file except in compliance with the License. 7 # You may obtain a copy of the License at 8 # 9 # http://www.apache.org/licenses/LICENSE-2.0 10 # 11 # Unless required by applicable law or agreed to in writing, software 12 # distributed under the License is distributed on an "AS IS" BASIS, 13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 # See the License for the specific language governing permissions and 15 # limitations under the License. 16 17 """Migrates information from Testgrid's Config.yaml to a subdirectory of Prow Jobs 18 19 Moves Dashboard Tabs and redundant Test Groups 20 Skips any Configuration that contains unusual keys, even if they're incorrect keys. 21 """ 22 23 import re 24 import argparse 25 from os import walk 26 import ruamel.yaml 27 28 # Prow files that will be ignored 29 EXEMPT_FILES = [ 30 # Ruamel won't be able to successfully dump fejta-bot-periodics 31 # See https://bitbucket.org/ruamel/yaml/issues/258/applying-json-patch-breaks-comment 32 "fejta-bot-periodics.yaml", 33 # Generated security jobs are generated with the same name as kubernetes/kubernetes 34 # presubmits, but we never want to migrate to the generated ones. 35 "generated-security-jobs.yaml", 36 # generated.yaml is generated by generate_tests.py, and will be overwritten. 37 "generated.yaml", 38 ] 39 MAX_WIDTH = 2000000000 40 41 42 def main(testgrid_config, prow_dir): 43 with open(testgrid_config, "r") as config_fp: 44 config = ruamel.yaml.load(config_fp, 45 Loader=ruamel.yaml.SafeLoader, 46 preserve_quotes=True) 47 48 for dashboard in config["dashboards"]: 49 if "dashboard_tab" in dashboard: 50 for dash_tab in dashboard["dashboard_tab"][:]: 51 if assert_tab_keys(dash_tab): 52 move_tab(dashboard, dash_tab, prow_dir) 53 54 if "test_groups" in config: 55 for test_group in config["test_groups"][:]: 56 if assert_group_keys(test_group): 57 move_group(config, test_group, prow_dir) 58 59 with open(testgrid_config, "w") as config_fp: 60 ruamel.yaml.dump(config, config_fp, 61 Dumper=ruamel.yaml.RoundTripDumper, width=MAX_WIDTH) 62 config_fp.truncate() 63 64 65 def move_tab(dashboard, dash_tab, prow_dir): 66 """Moves a given tab to the matching prow job in prow_dir, if possible""" 67 dashboard_name = dashboard["name"] 68 prow_job_name = dash_tab["test_group_name"] 69 prow_job_file_name = find_prow_job(prow_job_name, prow_dir) 70 if prow_job_file_name == "": 71 return 72 73 # Matching file found; patch and delete 74 print("Patching tab {0} in {1}".format(prow_job_name, prow_job_file_name)) 75 76 with open(prow_job_file_name, "r") as job_fp: 77 prow_config = ruamel.yaml.load(job_fp, 78 Loader=ruamel.yaml.SafeLoader, 79 preserve_quotes=True) 80 81 # For each presubmits, postsubmits, periodic: 82 # presubmits -> <any repository> -> [{name: prowjob}] 83 if "presubmits" in prow_config: 84 for _, jobs in prow_config["presubmits"].items(): 85 for job in jobs: 86 if prow_job_name == job["name"]: 87 job = patch_prow_job_with_tab(job, dash_tab, dashboard_name) 88 89 # postsubmits -> <any repository> -> [{name: prowjob}] 90 if "postsubmits" in prow_config: 91 for _, jobs in prow_config["postsubmits"].items(): 92 for job in jobs: 93 if prow_job_name == job["name"]: 94 job = patch_prow_job_with_tab(job, dash_tab, dashboard_name) 95 96 # periodics -> [{name: prowjob}] 97 if "periodics" in prow_config: 98 for job in prow_config["periodics"]: 99 if prow_job_name == job["name"]: 100 job = patch_prow_job_with_tab(job, dash_tab, dashboard_name) 101 102 # Dump ProwConfig to prowJobFile 103 with open(prow_job_file_name, "w") as job_fp: 104 ruamel.yaml.dump(prow_config, job_fp, 105 Dumper=ruamel.yaml.RoundTripDumper, width=MAX_WIDTH) 106 job_fp.truncate() 107 108 # delete tab 109 dashboard["dashboard_tab"].remove(dash_tab) 110 111 112 def move_group(config, group, prow_dir): 113 """Moves a given test group to the first matching prow job in prow_dir, if possible""" 114 prow_job_name = group["name"] 115 prow_job_file_name = find_prow_job(prow_job_name, prow_dir) 116 if prow_job_file_name == "": 117 return 118 119 # Matching file found; patch and delete 120 print("Patching group {0} in {1}".format(prow_job_name, prow_job_file_name)) 121 122 with open(prow_job_file_name, "r") as job_fp: 123 prow_config = ruamel.yaml.load(job_fp, 124 Loader=ruamel.yaml.SafeLoader, 125 preserve_quotes=True) 126 127 # For each presubmit, postsubmit, and periodic 128 # presubmits -> <any repository> -> [{name: prowjob}] 129 # An annotation must be forced, or else the testgroup will not be generated 130 if "presubmits" in prow_config: 131 for _, jobs in prow_config["presubmits"].items(): 132 for job in jobs: 133 if prow_job_name == job["name"]: 134 job = patch_prow_job_with_group(job, group, force_group_creation=True) 135 break 136 137 # postsubmits -> <any repository> -> [{name: prowjob}] 138 if "postsubmits" in prow_config: 139 for _, jobs in prow_config["postsubmits"].items(): 140 for job in jobs: 141 if prow_job_name == job["name"]: 142 job = patch_prow_job_with_group(job, group) 143 break 144 145 # periodics -> [{name: prowjob}] 146 if "periodics" in prow_config: 147 for job in prow_config["periodics"]: 148 if prow_job_name == job["name"]: 149 job = patch_prow_job_with_group(job, group) 150 break 151 152 # Dump ProwConfig to prowJobFile 153 with open(prow_job_file_name, "w") as job_fp: 154 ruamel.yaml.dump(prow_config, job_fp, 155 Dumper=ruamel.yaml.RoundTripDumper, width=MAX_WIDTH) 156 job_fp.truncate() 157 158 config["test_groups"].remove(group) 159 160 def assert_tab_keys(tab): 161 """Asserts if a dashboard tab is able to be migrated. 162 163 To be migratable, the annotations must only contain allowed keys 164 AND alert_options, if present, must contain and only contain "alert_mail_to_addresses" 165 """ 166 allowedKeys = ["name", "description", "test_group_name", "alert_options", 167 "num_failures_to_alert", "alert_stale_results_hours", "num_columns_recent"] 168 169 if [k for k in tab.keys() if k not in allowedKeys]: 170 return False 171 172 if "alert_options" in tab: 173 alert_keys = tab["alert_options"].keys() 174 if len(alert_keys) != 1 or "alert_mail_to_addresses" not in alert_keys: 175 return False 176 177 return True 178 179 def assert_group_keys(group): 180 """Asserts if a testgroup is able to be migrated. 181 182 To be migratable, the group must only contain allowed keys 183 """ 184 allowedKeys = ["name", "gcs_prefix", "alert_stale_results_hours", 185 "num_failures_to_alert", "num_columns_recent"] 186 187 if [k for k in group.keys() if k not in allowedKeys]: 188 return False 189 return True 190 191 192 def find_prow_job(name, path): 193 """Finds a Prow Job in a given subdirectory. 194 195 Returns the first file that contains the named prow job 196 Returns "" if there isn't one 197 Dives into subdirectories 198 Ignores EXEMPT_FILES 199 """ 200 pattern = re.compile("name: '?\"?" + name + "'?\"?$", re.MULTILINE) 201 for (dirpath, _, filenames) in walk(path): 202 for filename in filenames: 203 if filename.endswith(".yaml") and filename not in EXEMPT_FILES: 204 for _, line in enumerate(open(dirpath + "/" + filename)): 205 for _ in re.finditer(pattern, line): 206 #print "Found %s in %s" % (name, filename) 207 return dirpath + "/" + filename 208 return "" 209 210 211 def patch_prow_job_with_tab(prow_yaml, dash_tab, dashboard_name): 212 """Updates a Prow YAML object. 213 214 Assumes a valid prow yaml and a compatible dashTab 215 Will create a new annotation or amend an existing one 216 """ 217 if "annotations" in prow_yaml: 218 # There exists an annotation; amend it 219 annotation = prow_yaml["annotations"] 220 if "testgrid-dashboards" in prow_yaml["annotations"]: 221 # Existing annotation includes a testgrid annotation 222 # The dashboard name must come first if it's a sig-release-master-* dashboard 223 if dashboard_name.startswith("sig-release-master"): 224 annotation["testgrid-dashboards"] = (dashboard_name 225 + ", " 226 + annotation["testgrid-dashboards"]) 227 else: 228 annotation["testgrid-dashboards"] += (", " + dashboard_name) 229 else: 230 #Existing annotation is non-testgrid-related 231 annotation["testgrid-dashboards"] = dashboard_name 232 233 else: 234 # There is no annotation; construct it 235 annotation = {"testgrid-dashboards": dashboard_name} 236 237 # Append optional annotations 238 if ("name" in dash_tab 239 and "testgrid-tab-name" not in annotation 240 and dash_tab["name"] != dash_tab["test_group_name"]): 241 annotation["testgrid-tab-name"] = dash_tab["name"] 242 243 if ("alert_options" in dash_tab 244 and "alert_mail_to_addresses" in dash_tab["alert_options"] 245 and "testgrid-alert-email" not in annotation): 246 annotation["testgrid-alert-email"] = dash_tab["alert_options"]["alert_mail_to_addresses"] 247 248 opt_arguments = [("description", "description"), 249 ("num_failures_to_alert", "testgrid-num-failures-to-alert"), 250 ("alert_stale_results_hours", "testgrid-alert-stale-results-hours"), 251 ("num_columns_recent", "testgrid-num-columns-recent")] 252 253 for tab_arg, annotation_arg in opt_arguments: 254 if (tab_arg in dash_tab and annotation_arg not in annotation): 255 # Numeric arguments need to be coerced into strings to be parsed correctly 256 annotation[annotation_arg] = str(dash_tab[tab_arg]) 257 258 prow_yaml["annotations"] = annotation 259 return prow_yaml 260 261 262 def patch_prow_job_with_group(prow_yaml, test_group, force_group_creation=False): 263 """Updates a prow YAML object 264 265 Assumes a valid prow yaml and a compatible test group 266 Will amend existing annotations or create one if there is data to migrate 267 If there is no migratable data, an annotation will be forced only if specified 268 """ 269 if "annotations" in prow_yaml: 270 # There exists an annotation; amend it 271 annotation = prow_yaml["annotations"] 272 else: 273 annotation = {} 274 275 # migrate info 276 opt_arguments = [("num_failures_to_alert", "testgrid-num-failures-to-alert"), 277 ("alert_stale_results_hours", "testgrid-alert-stale-results-hours"), 278 ("num_columns_recent", "testgrid-num-columns-recent")] 279 280 for group_arg, annotation_arg in opt_arguments: 281 if (group_arg in test_group and annotation_arg not in annotation): 282 # Numeric arguments need to be coerced into strings to be parsed correctly 283 annotation[annotation_arg] = str(test_group[group_arg]) 284 285 if force_group_creation and "testgrid-dashboards" not in annotation: 286 annotation["testgrid-create-test-group"] = "true" 287 288 if not any(annotation): 289 return prow_yaml 290 291 prow_yaml["annotations"] = annotation 292 return prow_yaml 293 294 295 if __name__ == '__main__': 296 PARSER = argparse.ArgumentParser( 297 description='Migrates Testgrid Tabs to Prow') 298 PARSER.add_argument( 299 '--testgrid-config', 300 default='../testgrid/config.yaml', 301 help='Path to testgrid/config.yaml') 302 PARSER.add_argument( 303 '--prow-job-dir', 304 default='../config/jobs', 305 help='Path to Prow Job Directory') 306 ARGS = PARSER.parse_args() 307 308 main(ARGS.testgrid_config, ARGS.prow_job_dir)