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  }