k8s.io/test-infra@v0.0.0-20240520184403-27c6b4c223d8/scenarios/kubernetes_e2e.py (about)

     1  #!/usr/bin/env python3
     2  
     3  # Copyright 2017 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  # Need to figure out why this only fails on travis
    18  # pylint: disable=bad-continuation
    19  
    20  """Runs kubernetes e2e test with specified config"""
    21  
    22  import argparse
    23  import hashlib
    24  import os
    25  import shutil
    26  import subprocess
    27  import sys
    28  import urllib.request, urllib.error, urllib.parse
    29  import time
    30  
    31  ORIG_CWD = os.getcwd()  # Checkout changes cwd
    32  
    33  
    34  def test_infra(*paths):
    35      """Return path relative to root of test-infra repo."""
    36      return os.path.join(ORIG_CWD, os.path.dirname(__file__), '..', *paths)
    37  
    38  
    39  def check(*cmd):
    40      """Log and run the command, raising on errors."""
    41      print('Run:', cmd, file=sys.stderr)
    42      subprocess.check_call(cmd)
    43  
    44  
    45  def check_output(*cmd):
    46      """Log and run the command, raising on errors, return output"""
    47      print('Run:', cmd, file=sys.stderr)
    48      return subprocess.check_output(cmd)
    49  
    50  
    51  def check_env(env, *cmd):
    52      """Log and run the command with a specific env, raising on errors."""
    53      print('Environment:', file=sys.stderr)
    54      for key, value in sorted(env.items()):
    55          print('%s=%s' % (key, value), file=sys.stderr)
    56      print('Run:', cmd, file=sys.stderr)
    57      subprocess.check_call(cmd, env=env)
    58  
    59  
    60  def kubekins(tag):
    61      """Return full path to kubekins-e2e:tag."""
    62      return 'gcr.io/k8s-staging-test-infra/kubekins-e2e:%s' % tag
    63  
    64  
    65  def parse_env(env):
    66      """Returns (FOO, BAR=MORE) for FOO=BAR=MORE."""
    67      return env.split('=', 1)
    68  
    69  
    70  class LocalMode(object):
    71      """Runs e2e tests by calling kubetest."""
    72      def __init__(self, workspace, artifacts):
    73          self.command = 'kubetest'
    74          self.workspace = workspace
    75          self.artifacts = artifacts
    76          self.env = []
    77          self.os_env = []
    78          self.env_files = []
    79          self.add_environment(
    80              'HOME=%s' % workspace,
    81              'WORKSPACE=%s' % workspace,
    82              'PATH=%s' % os.getenv('PATH'),
    83          )
    84  
    85      def add_environment(self, *envs):
    86          """Adds FOO=BAR to the list of environment overrides."""
    87          self.env.extend(parse_env(e) for e in envs)
    88  
    89      def add_os_environment(self, *envs):
    90          """Adds FOO=BAR to the list of os environment overrides."""
    91          self.os_env.extend(parse_env(e) for e in envs)
    92  
    93      def add_file(self, env_file):
    94          """Reads all FOO=BAR lines from env_file."""
    95          with open(env_file) as fp:
    96              for line in fp:
    97                  line = line.rstrip()
    98                  if not line or line.startswith('#'):
    99                      continue
   100                  self.env_files.append(parse_env(line))
   101  
   102      def add_env(self, env):
   103          self.env_files.append(parse_env(env))
   104  
   105      def add_gce_ssh(self, priv, pub):
   106          """Copies priv, pub keys to $WORKSPACE/.ssh."""
   107          ssh_dir = os.path.join(self.workspace, '.ssh')
   108          if not os.path.isdir(ssh_dir):
   109              os.makedirs(ssh_dir)
   110  
   111          gce_ssh = os.path.join(ssh_dir, 'google_compute_engine')
   112          gce_pub = os.path.join(ssh_dir, 'google_compute_engine.pub')
   113          shutil.copy(priv, gce_ssh)
   114          shutil.copy(pub, gce_pub)
   115          self.add_environment(
   116              'JENKINS_GCE_SSH_PRIVATE_KEY_FILE=%s' % gce_ssh,
   117              'JENKINS_GCE_SSH_PUBLIC_KEY_FILE=%s' % gce_pub,
   118          )
   119  
   120      @staticmethod
   121      def add_service_account(path):
   122          """Returns path."""
   123          return path
   124  
   125      def add_k8s(self, *a, **kw):
   126          """Add specified k8s.io repos (noop)."""
   127          pass
   128  
   129      def start(self, args):
   130          """Starts kubetest."""
   131          print('starts with local mode', file=sys.stderr)
   132          env = {}
   133          env.update(self.os_env)
   134          env.update(self.env_files)
   135          env.update(self.env)
   136          check_env(env, self.command, *args)
   137  
   138  
   139  def cluster_name(cluster, tear_down_previous=False):
   140      """Return or select a cluster name."""
   141      if cluster:
   142          return cluster
   143      # Create a suffix based on the build number and job name.
   144      # This ensures no conflict across runs of different jobs (see #7592).
   145      # For PR jobs, we use PR number instead of build number to ensure the
   146      # name is constant across different runs of the presubmit on the PR.
   147      # This helps clean potentially leaked resources from earlier run that
   148      # could've got evicted midway (see #7673).
   149      job_type = os.getenv('JOB_TYPE')
   150      if job_type == 'batch':
   151          suffix = 'batch-%s' % os.getenv('BUILD_ID', 0)
   152      elif job_type == 'presubmit' and tear_down_previous:
   153          suffix = '%s' % os.getenv('PULL_NUMBER', 0)
   154      else:
   155          suffix = '%s' % os.getenv('BUILD_ID', 0)
   156      if len(suffix) > 10:
   157          suffix = hashlib.md5(suffix.encode('utf-8')).hexdigest()[:10]
   158      job_hash = hashlib.md5(os.getenv('JOB_NAME', '').encode('utf-8')).hexdigest()[:5]
   159      return 'e2e-%s-%s' % (suffix, job_hash)
   160  
   161  
   162  def read_gcs_path(gcs_path):
   163      """reads a gcs path (gs://...) by HTTP GET to storage.googleapis.com"""
   164      link = gcs_path.replace('gs://', 'https://storage.googleapis.com/')
   165      loc = urllib.request.urlopen(link).read()
   166      print("Read GCS Path: %s" % loc, file=sys.stderr)
   167      return loc
   168  
   169  
   170  def get_shared_gcs_path(gcs_shared, use_shared_build):
   171      """return the shared path for this set of jobs using args and $PULL_REFS."""
   172      build_file = ''
   173      if use_shared_build:
   174          build_file += use_shared_build + '-'
   175      build_file += 'build-location.txt'
   176      return os.path.join(gcs_shared, os.getenv('PULL_REFS', ''), build_file)
   177  
   178  
   179  def main(args):
   180      """Set up env, start kubekins-e2e, handle termination. """
   181      # pylint: disable=too-many-branches,too-many-statements,too-many-locals
   182  
   183      # Rules for env var priority here in docker:
   184      # -e FOO=a -e FOO=b -> FOO=b
   185      # --env-file FOO=a --env-file FOO=b -> FOO=b
   186      # -e FOO=a --env-file FOO=b -> FOO=a(!!!!)
   187      # --env-file FOO=a -e FOO=b -> FOO=b
   188      #
   189      # So if you overwrite FOO=c for a local run it will take precedence.
   190      #
   191  
   192      # Set up workspace/artifacts dir
   193      workspace = os.environ.get('WORKSPACE', os.getcwd())
   194      artifacts = os.environ.get('ARTIFACTS', os.path.join(workspace, '_artifacts'))
   195      if not os.path.isdir(artifacts):
   196          os.makedirs(artifacts)
   197  
   198      mode = LocalMode(workspace, artifacts)
   199  
   200      for env_file in args.env_file:
   201          mode.add_file(test_infra(env_file))
   202      for env in args.env:
   203          mode.add_env(env)
   204  
   205      # TODO(fejta): remove after next image push
   206      mode.add_environment('KUBETEST_MANUAL_DUMP=y')
   207      if args.dump_before_and_after:
   208          before_dir = os.path.join(mode.artifacts, 'before')
   209          if not os.path.exists(before_dir):
   210              os.makedirs(before_dir)
   211          after_dir = os.path.join(mode.artifacts, 'after')
   212          if not os.path.exists(after_dir):
   213              os.makedirs(after_dir)
   214  
   215          runner_args = [
   216              '--dump-pre-test-logs=%s' % before_dir,
   217              '--dump=%s' % after_dir,
   218              ]
   219      else:
   220          runner_args = [
   221              '--dump=%s' % mode.artifacts,
   222          ]
   223  
   224      if args.service_account:
   225          runner_args.append(
   226              '--gcp-service-account=%s' % mode.add_service_account(args.service_account))
   227  
   228      if args.use_shared_build is not None:
   229          # find shared build location from GCS
   230          gcs_path = get_shared_gcs_path(args.gcs_shared, args.use_shared_build)
   231          print('Getting shared build location from: '+gcs_path, file=sys.stderr)
   232          # retry loop for reading the location
   233          attempts_remaining = 12
   234          while True:
   235              attempts_remaining -= 1
   236              try:
   237                  # tell kubetest to extract from this location
   238                  shared_build_gcs_path = read_gcs_path(gcs_path)
   239                  args.kubetest_args.append('--extract=' + shared_build_gcs_path)
   240                  args.build = None
   241                  break
   242              except urllib.error.URLError as err:
   243                  print('Failed to get shared build location: %s' % err, file=sys.stderr)
   244                  if attempts_remaining > 0:
   245                      print('Waiting 5 seconds and retrying...', file=sys.stderr)
   246                      time.sleep(5)
   247                  else:
   248                      raise RuntimeError('Failed to get shared build location too many times!')
   249  
   250      elif args.build is not None:
   251          if args.build == '':
   252              # Empty string means --build was passed without any arguments;
   253              # if --build wasn't passed, args.build would be None
   254              runner_args.append('--build')
   255          else:
   256              runner_args.append('--build=%s' % args.build)
   257          k8s = os.getcwd()
   258          if not os.path.basename(k8s) == 'kubernetes':
   259              raise ValueError(k8s)
   260          mode.add_k8s(os.path.dirname(k8s), 'kubernetes', 'release')
   261  
   262      if args.stage is not None:
   263          runner_args.append('--stage=%s' % args.stage)
   264  
   265      # TODO(fejta): move these out of this file
   266      if args.up == 'true':
   267          runner_args.append('--up')
   268      if args.down == 'true':
   269          runner_args.append('--down')
   270      if args.test == 'true':
   271          runner_args.append('--test')
   272  
   273      # Passthrough some args to kubetest
   274      if args.deployment:
   275          runner_args.append('--deployment=%s' % args.deployment)
   276      if args.provider:
   277          runner_args.append('--provider=%s' % args.provider)
   278  
   279      cluster = cluster_name(args.cluster, args.tear_down_previous)
   280      runner_args.append('--cluster=%s' % cluster)
   281      runner_args.append('--gcp-network=%s' % cluster)
   282      runner_args.extend(args.kubetest_args)
   283  
   284      if args.use_logexporter:
   285          runner_args.append('--logexporter-gcs-path=%s' % args.logexporter_gcs_path)
   286  
   287      if args.deployment != 'kind' and args.gce_ssh:
   288          mode.add_gce_ssh(args.gce_ssh, args.gce_pub)
   289  
   290      # TODO(fejta): delete this?
   291      mode.add_os_environment(*(
   292          '%s=%s' % (k, v) for (k, v) in list(os.environ.items())))
   293  
   294      mode.add_environment(
   295        # Boilerplate envs
   296        # Skip gcloud update checking
   297        'CLOUDSDK_COMPONENT_MANAGER_DISABLE_UPDATE_CHECK=true',
   298        # Use default component update behavior
   299        'CLOUDSDK_EXPERIMENTAL_FAST_COMPONENT_UPDATE=false',
   300        # AWS
   301        'KUBE_AWS_INSTANCE_PREFIX=%s' % cluster,
   302        # GCE
   303        'INSTANCE_PREFIX=%s' % cluster,
   304        'KUBE_GCE_INSTANCE_PREFIX=%s' % cluster,
   305      )
   306  
   307      mode.start(runner_args)
   308  
   309  
   310  def create_parser():
   311      """Create argparser."""
   312      parser = argparse.ArgumentParser()
   313      parser.add_argument(
   314          '--env-file', default=[], action="append",
   315          help='Job specific environment file')
   316      parser.add_argument(
   317          '--env', default=[], action="append",
   318          help='Job specific environment setting ' +
   319          '(usage: "--env=VAR=SETTING" will set VAR to SETTING).')
   320      parser.add_argument(
   321          '--gce-ssh',
   322          default=os.environ.get('JENKINS_GCE_SSH_PRIVATE_KEY_FILE'),
   323          help='Path to .ssh/google_compute_engine keys')
   324      parser.add_argument(
   325          '--gce-pub',
   326          default=os.environ.get('JENKINS_GCE_SSH_PUBLIC_KEY_FILE'),
   327          help='Path to pub gce ssh key')
   328      parser.add_argument(
   329          '--service-account',
   330          default=os.environ.get('GOOGLE_APPLICATION_CREDENTIALS'),
   331          help='Path to service-account.json')
   332      parser.add_argument(
   333          '--build', nargs='?', default=None, const='',
   334          help='Build kubernetes binaries if set, optionally specifying strategy')
   335      parser.add_argument(
   336          '--use-shared-build', nargs='?', default=None, const='',
   337          help='Use prebuilt kubernetes binaries if set, optionally specifying strategy')
   338      parser.add_argument(
   339          '--gcs-shared',
   340          default='gs://kubernetes-jenkins/shared-results/',
   341          help='Get shared build from this bucket')
   342      parser.add_argument(
   343          '--cluster', default='bootstrap-e2e', help='Name of the cluster')
   344      parser.add_argument(
   345          '--stage', default=None, help='Stage release to GCS path provided')
   346      parser.add_argument(
   347          '--test', default='true', help='If we need to run any actual test within kubetest')
   348      parser.add_argument(
   349          '--down', default='true', help='If we need to tear down the e2e cluster')
   350      parser.add_argument(
   351          '--up', default='true', help='If we need to bring up a e2e cluster')
   352      parser.add_argument(
   353          '--tear-down-previous', action='store_true',
   354          help='If we need to tear down previous e2e cluster')
   355      parser.add_argument(
   356          '--use-logexporter',
   357          action='store_true',
   358          help='If we need to use logexporter tool to upload logs from nodes to GCS directly')
   359      parser.add_argument(
   360          '--logexporter-gcs-path',
   361          default=os.environ.get('GCS_ARTIFACTS_DIR',''),
   362          help='GCS path where logexporter tool will upload logs if enabled')
   363      parser.add_argument(
   364          '--kubetest_args',
   365          action='append',
   366          default=[],
   367          help='Send unrecognized args directly to kubetest')
   368      parser.add_argument(
   369          '--dump-before-and-after', action='store_true',
   370          help='Dump artifacts from both before and after the test run')
   371  
   372      # kubetest flags that also trigger behaviour here
   373      parser.add_argument(
   374          '--provider', help='provider flag as used by kubetest')
   375      parser.add_argument(
   376          '--deployment', help='deployment flag as used by kubetest')
   377  
   378      return parser
   379  
   380  
   381  def parse_args(args=None):
   382      """Return args, adding unrecognized args to kubetest_args."""
   383      parser = create_parser()
   384      args, extra = parser.parse_known_args(args)
   385      args.kubetest_args += extra
   386  
   387      return args
   388  
   389  
   390  if __name__ == '__main__':
   391      main(parse_args())