k8s.io/test-infra@v0.0.0-20240520184403-27c6b4c223d8/kubetest/extract_k8s.go (about) 1 /* 2 Copyright 2017 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package main 18 19 import ( 20 "bytes" 21 "encoding/json" 22 "fmt" 23 "io" 24 "log" 25 "net/http" 26 "os" 27 "os/exec" 28 "path" 29 "path/filepath" 30 "regexp" 31 "strings" 32 "time" 33 34 "k8s.io/test-infra/kubetest/util" 35 ) 36 37 type extractMode int 38 39 const ( 40 _ extractMode = iota 41 localBazel // local bazel 42 local // local 43 gci // gci/FAMILY, gci/FAMILY?project=IMAGE_PROJECT:k8s-map-bucket=BUCKET_NAME 44 gciCi // gci/FAMILY/CI_VERSION 45 gke // gke(deprecated), gke-default, gke-latest, gke-channel-CHANNEL_NAME 46 ci // ci/latest, ci/latest-1.5 47 ciFast // ci/latest-fast, ci/latest-1.19-fast 48 rc // release/latest, release/latest-1.5 49 stable // release/stable, release/stable-1.5 50 version // v1.5.0, v1.5.0-beta.2 51 gcs // gs://bucket/prefix/v1.6.0-alpha.0 52 load // Load a --save cluster 53 bazel // A pre/postsubmit bazel build version, prefixed with bazel/ 54 ) 55 56 type extractStrategy struct { 57 mode extractMode 58 option string 59 ciVersion string 60 additionalInfo string 61 value string 62 } 63 64 type extractStrategies []extractStrategy 65 66 func (l *extractStrategies) String() string { 67 s := []string{} 68 for _, e := range *l { 69 s = append(s, e.value) 70 } 71 return strings.Join(s, ",") 72 } 73 74 // Converts --extract=release/stable, etc into an extractStrategy{} 75 func (l *extractStrategies) Set(value string) error { 76 var strategies = map[string]extractMode{ 77 `^(bazel)$`: localBazel, 78 `^(local)`: local, 79 `^gke-?(default|channel-(rapid|regular|stable)(?:-((?:[0-9]-)?latest))?|latest(-\d+.\d+(.\d+(-gke)?)?)?)?$`: gke, 80 `^gci/([\w-]+(?:\?{1}(?::?[\w-]+=[\w-]+)+)?)$`: gci, 81 `^gci/([\w-]+(?:\?{1}(?::?[\w-]+=[\w-]+)+)?)/(.+)$`: gciCi, 82 `^ci/(.+)$`: ci, 83 `^ci/(.+)-fast$`: ciFast, 84 `^release/(latest.*)$`: rc, 85 `^release/(stable.*)$`: stable, 86 `^(v\d+\.\d+\.\d+[\w.\-+]*)$`: version, 87 `^(gs://.*)$`: gcs, 88 `^(bazel/.*)$`: bazel, 89 } 90 91 if len(*l) == 2 { 92 return fmt.Errorf("May only define at most 2 --extract strategies: %v %s", *l, value) 93 } 94 for search, mode := range strategies { 95 re := regexp.MustCompile(search) 96 mat := re.FindStringSubmatch(value) 97 if mat == nil { 98 continue 99 } 100 if mode == ci && strings.HasSuffix(value, "-fast") { 101 // do not match ci mode if will also match ciFast 102 continue 103 } 104 e := extractStrategy{ 105 mode: mode, 106 option: mat[1], 107 value: value, 108 } 109 if len(mat) > 2 { 110 e.ciVersion = mat[2] 111 } 112 if len(mat) > 3 { 113 e.additionalInfo = mat[3] 114 } 115 *l = append(*l, e) 116 log.Printf("Matched extraction strategy: %s", search) 117 return nil 118 } 119 return fmt.Errorf("Unknown extraction strategy: %s", value) 120 121 } 122 123 func (l *extractStrategies) Type() string { 124 return "exactStrategies" 125 } 126 127 // True when this kubetest invocation wants to download and extract a release. 128 func (l *extractStrategies) Enabled() bool { 129 return len(*l) > 0 130 } 131 132 func (l extractStrategies) Extract(project, zone, region, ciBucket, releaseBucket string, extractSrc bool) error { 133 // rm -rf kubernetes* 134 files, err := os.ReadDir(".") 135 if err != nil { 136 return err 137 } 138 for _, file := range files { 139 name := file.Name() 140 if !strings.HasPrefix(name, "kubernetes") { 141 continue 142 } 143 log.Printf("rm %s", name) 144 if err = os.RemoveAll(name); err != nil { 145 return err 146 } 147 } 148 149 for i, e := range l { 150 if i > 0 { 151 // TODO(fejta): new strategy so we support more than 2 --extracts 152 if err := os.Rename("kubernetes", "kubernetes_skew"); err != nil { 153 return err 154 } 155 } 156 if err := e.Extract(project, zone, region, ciBucket, releaseBucket, extractSrc); err != nil { 157 return err 158 } 159 } 160 161 return nil 162 } 163 164 // Find get-kube.sh at PWD, in PATH or else download it. 165 func ensureKube() (string, error) { 166 // Does get-kube.sh exist in pwd? 167 i, err := os.Stat("./get-kube.sh") 168 if err == nil && !i.IsDir() && i.Mode()&0111 > 0 { 169 return "./get-kube.sh", nil 170 } 171 172 // How about in the path? 173 p, err := exec.LookPath("get-kube.sh") 174 if err == nil { 175 return p, nil 176 } 177 178 // Download it to a temp file 179 f, err := os.CreateTemp("", "get-kube") 180 if err != nil { 181 return "", err 182 } 183 defer f.Close() 184 if err := httpRead("https://get.k8s.io", f); err != nil { 185 return "", err 186 } 187 i, err = f.Stat() 188 if err != nil { 189 return "", err 190 } 191 if err := os.Chmod(f.Name(), i.Mode()|0111); err != nil { 192 return "", err 193 } 194 return f.Name(), nil 195 } 196 197 // Download named binaries for kubernetes 198 func getNamedBinaries(url, version, tarball string, retry int) error { 199 f, err := os.Create(tarball) 200 if err != nil { 201 return err 202 } 203 defer f.Close() 204 full := fmt.Sprintf("%s/%s/%s", url, version, tarball) 205 206 for i := 0; i < retry; i++ { 207 log.Printf("downloading %v from %v", tarball, full) 208 if err := httpRead(full, f); err == nil { 209 break 210 } 211 err = fmt.Errorf("url=%s version=%s failed get %v: %w", url, version, tarball, err) 212 if i == retry-1 { 213 return err 214 } 215 log.Println(err) 216 sleep(time.Duration(i) * time.Second) 217 } 218 219 f.Close() 220 o, err := control.Output(exec.Command("md5sum", f.Name())) 221 if err != nil { 222 return err 223 } 224 log.Printf("md5sum: %s", o) 225 226 cwd, err := os.Getwd() 227 if err != nil { 228 return fmt.Errorf("unable to get current directory: %w", err) 229 } 230 log.Printf("Extracting tar file %v into directory %v", f.Name(), cwd) 231 232 if err = control.FinishRunning(exec.Command("tar", "-xzf", f.Name())); err != nil { 233 return err 234 } 235 return nil 236 } 237 238 var ( 239 sleep = time.Sleep 240 ) 241 242 // Calls KUBERNETES_RELEASE_URL=url KUBERNETES_RELEASE=version get-kube.sh. 243 // This will download version from the specified url subdir and extract 244 // the tarballs. 245 var getKube = func(url, version string, getSrc bool) error { 246 // TODO(krzyzacy): migrate rest of the get-kube.sh logic into kubetest, using getNamedBinaries 247 // get/extract the src tarball first since bazel needs a clean tree 248 if getSrc { 249 cwd, err := os.Getwd() 250 if err != nil { 251 return err 252 } 253 if cwd != "kubernetes" { 254 if err = os.Mkdir("kubernetes", 0755); err != nil { 255 return err 256 } 257 if err = os.Chdir("kubernetes"); err != nil { 258 return err 259 } 260 } 261 262 if err := os.Setenv("KUBE_GIT_VERSION", version); err != nil { 263 return err 264 } 265 266 if err := getNamedBinaries(url, version, "kubernetes-src.tar.gz", 3); err != nil { 267 return err 268 } 269 } 270 271 k, err := ensureKube() 272 if err != nil { 273 return err 274 } 275 if err := os.Setenv("KUBERNETES_RELEASE_URL", url); err != nil { 276 return err 277 } 278 279 if err := os.Setenv("KUBERNETES_RELEASE", version); err != nil { 280 return err 281 } 282 if err := os.Setenv("KUBERNETES_SKIP_CONFIRM", "y"); err != nil { 283 return err 284 } 285 if err := os.Setenv("KUBERNETES_SKIP_CREATE_CLUSTER", "y"); err != nil { 286 return err 287 } 288 if err := os.Setenv("KUBERNETES_DOWNLOAD_TESTS", "y"); err != nil { 289 return err 290 } 291 // kube-up in cluster/gke/util.sh depends on this 292 if err := os.Setenv("CLUSTER_API_VERSION", version[1:]); err != nil { 293 return err 294 } 295 log.Printf("U=%s R=%s get-kube.sh", url, version) 296 for i := 0; i < 5; i++ { 297 err = control.FinishRunning(exec.Command(k)) 298 if err == nil { 299 break 300 } 301 err = fmt.Errorf("U=%s R=%s get-kube.sh failed: %w", url, version, err) 302 log.Println(err) 303 sleep(time.Duration(i) * time.Second) 304 } 305 306 return err 307 } 308 309 // wrapper for gsutil cat 310 var gsutilCat = func(url string) ([]byte, error) { 311 return control.Output(exec.Command("gsutil", "cat", url)) 312 } 313 314 func setReleaseFromGcs(prefix, suffix string, getSrc bool) error { 315 url := fmt.Sprintf("https://storage.googleapis.com/%v", prefix) 316 catURL := fmt.Sprintf("gs://%v/%v.txt", prefix, suffix) 317 release, err := gsutilCat(catURL) 318 if err != nil { 319 return fmt.Errorf("Failed to set release from %s (%v)", catURL, err) 320 } 321 return getKube(url, strings.TrimSpace(string(release)), getSrc) 322 } 323 324 var httpCat = func(url string) ([]byte, error) { 325 resp, err := http.Get(url) 326 if err != nil { 327 return nil, err 328 } 329 330 defer resp.Body.Close() 331 332 if resp.StatusCode != http.StatusOK { 333 return nil, fmt.Errorf("Unexpected HTTP status code: %d", resp.StatusCode) 334 } 335 336 release, err := io.ReadAll(resp.Body) 337 if err != nil { 338 return nil, err 339 } 340 341 return release, nil 342 } 343 344 func setReleaseFromHTTP(prefix, suffix string) (string, string, error) { 345 url := fmt.Sprintf("https://storage.googleapis.com/%s", prefix) 346 catURL := fmt.Sprintf("%s/%s.txt", url, suffix) 347 release, err := httpCat(catURL) 348 if err != nil { 349 return "", "", fmt.Errorf("Failed to set release from %s (%v)", catURL, err) 350 } 351 return url, strings.TrimSpace(string(release)), nil 352 } 353 354 var parseGciExtractOption = func(option string) (string, map[string]string) { 355 tokens := strings.Split(option, "?") 356 family := tokens[0] 357 paramsMap := map[string]string{ 358 // default values 359 "project": "container-vm-image-staging", 360 "k8s-map-bucket": "container-vm-image-staging", 361 } 362 if len(tokens) == 2 { 363 params := strings.Split(tokens[1], ":") 364 for _, param := range params { 365 kv := strings.Split(param, "=") 366 paramsMap[kv[0]] = kv[1] 367 } 368 } 369 return family, paramsMap 370 } 371 372 var gcloudGetImageName = func(family string, project string) ([]byte, error) { 373 return control.Output(exec.Command("gcloud", "compute", "images", "describe-from-family", family, fmt.Sprintf("--project=%v", project), "--format=value(name)")) 374 } 375 376 func setupGciVars(f string, p string) (string, error) { 377 b, err := gcloudGetImageName(f, p) 378 if err != nil { 379 return "", err 380 } 381 i := strings.TrimSpace(string(b)) 382 g := "gci" 383 m := map[string]string{ 384 "KUBE_GCE_MASTER_PROJECT": p, 385 "KUBE_GCE_MASTER_IMAGE": i, 386 "KUBE_MASTER_OS_DISTRIBUTION": g, 387 388 "KUBE_GCE_NODE_PROJECT": p, 389 "KUBE_GCE_NODE_IMAGE": i, 390 "KUBE_NODE_OS_DISTRIBUTION": g, 391 392 "BUILD_METADATA_GCE_MASTER_IMAGE": i, 393 "BUILD_METADATA_GCE_NODE_IMAGE": i, 394 395 "KUBE_OS_DISTRIBUTION": g, 396 } 397 if f == "gci-canary-test" { 398 var b bytes.Buffer 399 if err := httpRead("https://api.github.com/repos/docker/docker/releases", &b); err != nil { 400 return "", err 401 } 402 var v []map[string]interface{} 403 if err := json.NewDecoder(&b).Decode(&v); err != nil { 404 return "", err 405 } 406 // We want 1.13.0 407 m["KUBE_GCI_DOCKER_VERSION"] = v[0]["name"].(string)[1:] 408 } 409 for k, v := range m { 410 log.Printf("export %s=%s", k, v) 411 if err := os.Setenv(k, v); err != nil { 412 return "", err 413 } 414 } 415 return i, nil 416 } 417 418 func setReleaseFromGci(image, k8sMapBucket, releaseBucket string, getSrc bool) error { 419 catURL := fmt.Sprintf("gs://%s/k8s-version-map/%s", k8sMapBucket, image) 420 b, err := gsutilCat(catURL) 421 if err != nil { 422 return fmt.Errorf("Failed to set release from %s (%v)", catURL, err) 423 } 424 r := fmt.Sprintf("v%s", b) 425 return getKube(fmt.Sprintf("https://storage.googleapis.com/%s/release", releaseBucket), strings.TrimSpace(r), getSrc) 426 } 427 428 func (e extractStrategy) Extract(project, zone, region, ciBucket, releaseBucket string, extractSrc bool) error { 429 switch e.mode { 430 case localBazel: 431 vFile := util.K8s("kubernetes", "bazel-bin", "version") 432 vByte, err := os.ReadFile(vFile) 433 if err != nil { 434 return err 435 } 436 version := strings.TrimSpace(string(vByte)) 437 log.Printf("extracting version %v\n", version) 438 root := util.K8s("kubernetes", "bazel-bin", "build") 439 src := filepath.Join(root, "release-tars") 440 dst := filepath.Join(root, version) 441 log.Printf("copying files from %v to %v\n", src, dst) 442 if err := os.Rename(src, dst); err != nil { 443 return err 444 } 445 return getKube(fmt.Sprintf("file://%s", root), version, extractSrc) 446 case local: 447 url := util.K8s("kubernetes", "_output", "gcs-stage") 448 files, err := os.ReadDir(url) 449 if err != nil { 450 return err 451 } 452 var release string 453 for _, file := range files { 454 r := file.Name() 455 if strings.HasPrefix(r, "v") { 456 release = r 457 break 458 } 459 } 460 if len(release) == 0 { 461 return fmt.Errorf("No releases found in %s", url) 462 } 463 return getKube(fmt.Sprintf("file://%s", url), release, extractSrc) 464 case gci, gciCi: 465 family, gciExtractParams := parseGciExtractOption(e.option) 466 project := gciExtractParams["project"] 467 k8sMapBucket := gciExtractParams["k8s-map-bucket"] 468 if i, err := setupGciVars(family, project); err != nil { 469 return err 470 } else if e.ciVersion != "" { 471 return setReleaseFromGcs(fmt.Sprintf("%s/ci", ciBucket), e.ciVersion, extractSrc) 472 } else { 473 return setReleaseFromGci(i, k8sMapBucket, releaseBucket, extractSrc) 474 } 475 case gke: 476 // TODO(fejta): prod v staging v test 477 if project == "" { 478 return fmt.Errorf("--gcp-project unset") 479 } 480 if e.value == "gke" { 481 log.Print("*** --extract=gke is deprecated, migrate to --extract=gke-default ***") 482 } 483 if strings.HasPrefix(e.option, "latest") { 484 // get latest supported master version 485 releasePrefix := "" 486 if strings.HasPrefix(e.option, "latest-") { 487 releasePrefix = strings.TrimPrefix(e.option, "latest-") 488 } 489 version, err := getLatestGKEVersion(project, zone, region, releasePrefix) 490 if err != nil { 491 return fmt.Errorf("failed to get latest gke version: %s", err) 492 } 493 return getKube("https://storage.googleapis.com/gke-release-staging/kubernetes/release", version, extractSrc) 494 } 495 496 if strings.HasPrefix(e.option, "channel") { 497 // get version from selected channel. If -latest is specified after channel we try to 498 // download latest valid version. Otherwise we download default version for this channel 499 version, err := getChannelGKEVersion(project, zone, region, e.ciVersion, e.additionalInfo) 500 if err != nil { 501 return fmt.Errorf("failed to get gke version from channel %s: %s", e.ciVersion, err) 502 } 503 return getKube("https://storage.googleapis.com/gke-release-staging/kubernetes/release", version, extractSrc) 504 } 505 506 // TODO(krzyzacy): clean up gke-default logic 507 if zone == "" { 508 return fmt.Errorf("--gcp-zone unset") 509 } 510 511 // get default cluster version for default extract strategy 512 ci, err := control.Output(exec.Command("gcloud", "container", "get-server-config", fmt.Sprintf("--project=%v", project), fmt.Sprintf("--zone=%v", zone), "--format=value(defaultClusterVersion)")) 513 if err != nil { 514 return err 515 } 516 re := regexp.MustCompile(`(\d+\.\d+)(\..+)?$`) // 1.11.7-beta.0 -> 1.11 517 mat := re.FindStringSubmatch(strings.TrimSpace(string(ci))) 518 if mat == nil { 519 return fmt.Errorf("failed to parse version from %s", ci) 520 } 521 // When JENKINS_USE_SERVER_VERSION=y, we launch the default version as determined 522 // by GKE, but pull the latest version of that branch for tests. e.g. if the default 523 // version is 1.5.3, we would pull test binaries at ci/latest-1.5.txt, but launch 524 // the default (1.5.3). We have to unset CLUSTER_API_VERSION here to allow GKE to 525 // launch the default. 526 // TODO(fejta): clean up this logic. Setting/unsetting the same env var is gross. 527 defer os.Unsetenv("CLUSTER_API_VERSION") 528 return setReleaseFromGcs(fmt.Sprintf("%s/ci", ciBucket), "latest-"+mat[1], extractSrc) 529 case ci: 530 if strings.HasPrefix(e.option, "gke-") { 531 return setReleaseFromGcs("gke-release-staging/kubernetes/release", e.option, extractSrc) 532 } 533 534 url, release, err := setReleaseFromHTTP(fmt.Sprintf("%s/ci", ciBucket), e.option) 535 if err != nil { 536 return err 537 } 538 return getKube(url, release, extractSrc) 539 case ciFast: 540 // ciFast latest version marker is published to 541 // '<ciBucket>/ci/<version>-fast.txt' but the actual source 542 // is at '<ciBucket>/ci/fast/<version>/kubernetes.tar.gz' 543 url, release, err := setReleaseFromHTTP(fmt.Sprintf("%s/ci", ciBucket), fmt.Sprintf("%s-fast", e.option)) 544 if err != nil { 545 return err 546 } 547 return getKube(fmt.Sprintf("%s/fast", url), release, extractSrc) 548 case rc, stable: 549 url, release, err := setReleaseFromHTTP(fmt.Sprintf("%s/release", releaseBucket), e.option) 550 if err != nil { 551 return err 552 } 553 return getKube(url, release, extractSrc) 554 case version: 555 var url string 556 release := e.option 557 re := regexp.MustCompile(`(v\d+\.\d+\.\d+-gke.\d+)$`) // v1.8.0-gke.0 558 if re.FindStringSubmatch(release) != nil { 559 url = "https://storage.googleapis.com/gke-release-staging/kubernetes/release" 560 } else if strings.Contains(release, "+") { 561 url = fmt.Sprintf("https://storage.googleapis.com/%s/ci", ciBucket) 562 } else { 563 url = fmt.Sprintf("https://storage.googleapis.com/%s/release", releaseBucket) 564 } 565 return getKube(url, release, extractSrc) 566 case gcs: 567 // strip gs://foo/bar(.txt) -> foo/bar(.txt) 568 withoutGS := e.option[5:] 569 if strings.HasSuffix(e.option, ".txt") { 570 // foo/bar.txt -> bar 571 suffix := strings.TrimSuffix(path.Base(withoutGS), filepath.Ext(withoutGS)) 572 return setReleaseFromGcs(path.Dir(withoutGS), suffix, extractSrc) 573 } 574 url := "https://storage.googleapis.com" + "/" + path.Dir(withoutGS) 575 return getKube(url, path.Base(withoutGS), extractSrc) 576 case load: 577 return loadState(e.option, extractSrc) 578 case bazel: 579 return getKube("", e.option, extractSrc) 580 } 581 return fmt.Errorf("Unrecognized extraction: %v(%v)", e.mode, e.value) 582 } 583 584 func loadKubeconfig(save string) error { 585 cURL, err := util.JoinURL(save, "kube-config") 586 if err != nil { 587 return fmt.Errorf("bad load url %s: %w", save, err) 588 } 589 if err := os.MkdirAll(util.Home(".kube"), 0775); err != nil { 590 return err 591 } 592 return control.FinishRunning(exec.Command("gsutil", "cp", cURL, util.Home(".kube", "config"))) 593 } 594 595 func loadState(save string, getSrc bool) error { 596 log.Printf("Restore state from %s", save) 597 598 uURL, err := util.JoinURL(save, "release-url.txt") 599 if err != nil { 600 return fmt.Errorf("bad load url %s: %w", save, err) 601 } 602 rURL, err := util.JoinURL(save, "release.txt") 603 if err != nil { 604 return fmt.Errorf("bad load url %s: %w", save, err) 605 } 606 607 if err := loadKubeconfig(save); err != nil { 608 return fmt.Errorf("failed loading kubeconfig: %w", err) 609 } 610 611 url, err := gsutilCat(uURL) 612 if err != nil { 613 return err 614 } 615 release, err := gsutilCat(rURL) 616 if err != nil { 617 return err 618 } 619 return getKube(string(url), string(release), getSrc) 620 } 621 622 func saveState(save string) error { 623 url := os.Getenv("KUBERNETES_RELEASE_URL") // TODO(fejta): pass this in to saveState 624 version := os.Getenv("KUBERNETES_RELEASE") 625 log.Printf("Save U=%s R=%s to %s", url, version, save) 626 cURL, err := util.JoinURL(save, "kube-config") 627 if err != nil { 628 return fmt.Errorf("bad save url %s: %w", save, err) 629 } 630 uURL, err := util.JoinURL(save, "release-url.txt") 631 if err != nil { 632 return fmt.Errorf("bad save url %s: %w", save, err) 633 } 634 rURL, err := util.JoinURL(save, "release.txt") 635 if err != nil { 636 return fmt.Errorf("bad save url %s: %w", save, err) 637 } 638 639 if err := control.FinishRunning(exec.Command("gsutil", "cp", util.Home(".kube", "config"), cURL)); err != nil { 640 return fmt.Errorf("failed to save .kube/config to %s: %w", cURL, err) 641 } 642 if cmd, err := control.InputCommand(url, "gsutil", "cp", "-", uURL); err != nil { 643 return fmt.Errorf("failed to write url %s to %s: %w", url, uURL, err) 644 } else if err = control.FinishRunning(cmd); err != nil { 645 return fmt.Errorf("failed to upload url %s to %s: %w", url, uURL, err) 646 } 647 648 if cmd, err := control.InputCommand(version, "gsutil", "cp", "-", rURL); err != nil { 649 return fmt.Errorf("failed to write release %s to %s: %w", version, rURL, err) 650 } else if err = control.FinishRunning(cmd); err != nil { 651 return fmt.Errorf("failed to upload release %s to %s: %w", version, rURL, err) 652 } 653 return nil 654 }