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)