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())