...

Source file src/k8s.io/kubernetes/cmd/kubeadm/app/util/version.go

Documentation: k8s.io/kubernetes/cmd/kubeadm/app/util

     1  /*
     2  Copyright 2016 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 util
    18  
    19  import (
    20  	"fmt"
    21  	"io"
    22  	"net/http"
    23  	"regexp"
    24  	"strings"
    25  	"time"
    26  
    27  	"github.com/pkg/errors"
    28  
    29  	netutil "k8s.io/apimachinery/pkg/util/net"
    30  	versionutil "k8s.io/apimachinery/pkg/util/version"
    31  	pkgversion "k8s.io/component-base/version"
    32  	"k8s.io/klog/v2"
    33  
    34  	"k8s.io/kubernetes/cmd/kubeadm/app/constants"
    35  )
    36  
    37  const (
    38  	getReleaseVersionTimeout = 10 * time.Second
    39  )
    40  
    41  var (
    42  	kubeReleaseBucketURL  = "https://dl.k8s.io"
    43  	kubeCIBucketURL       = "https://storage.googleapis.com/k8s-release-dev"
    44  	kubeReleaseRegex      = regexp.MustCompile(`^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)([-\w.+]*)?$`)
    45  	kubeReleaseLabelRegex = regexp.MustCompile(`^((latest|stable)+(-[1-9](\.[1-9](\d)?)?)?)\z`)
    46  	kubeBucketPrefixes    = regexp.MustCompile(`^((release|ci)/)?([-\w.+]+)$`)
    47  )
    48  
    49  // KubernetesReleaseVersion is helper function that can fetch
    50  // available version information from release servers based on
    51  // label names, like "stable" or "latest".
    52  //
    53  // If argument is already semantic version string, it
    54  // will return same string.
    55  //
    56  // In case of labels, it tries to fetch from release
    57  // servers and then return actual semantic version.
    58  //
    59  // Available names on release servers:
    60  //
    61  //	stable      (latest stable release)
    62  //	stable-1    (latest stable release in 1.x)
    63  //	stable-1.0  (and similarly 1.1, 1.2, 1.3, ...)
    64  //	latest      (latest release, including alpha/beta)
    65  //	latest-1    (latest release in 1.x, including alpha/beta)
    66  //	latest-1.0  (and similarly 1.1, 1.2, 1.3, ...)
    67  func KubernetesReleaseVersion(version string) (string, error) {
    68  	return kubernetesReleaseVersion(version, fetchFromURL)
    69  }
    70  
    71  // kubernetesReleaseVersion is a helper function to fetch
    72  // available version information. Used for testing to eliminate
    73  // the need for internet calls.
    74  func kubernetesReleaseVersion(version string, fetcher func(string, time.Duration) (string, error)) (string, error) {
    75  	ver := normalizedBuildVersion(version)
    76  	if len(ver) != 0 {
    77  		return ver, nil
    78  	}
    79  
    80  	bucketURL, versionLabel, err := splitVersion(version)
    81  	if err != nil {
    82  		return "", err
    83  	}
    84  
    85  	// revalidate, if exact build from e.g. CI bucket requested.
    86  	ver = normalizedBuildVersion(versionLabel)
    87  	if len(ver) != 0 {
    88  		return ver, nil
    89  	}
    90  
    91  	// kubeReleaseLabelRegex matches labels such as: latest, latest-1, latest-1.10
    92  	if kubeReleaseLabelRegex.MatchString(versionLabel) {
    93  		// Try to obtain a client version.
    94  		// pkgversion.Get().String() should always return a correct version added by the golang
    95  		// linker and the build system. The version can still be missing when doing unit tests
    96  		// on individual packages.
    97  		clientVersion, clientVersionErr := kubeadmVersion(pkgversion.Get().String())
    98  		// Fetch version from the internet.
    99  		url := fmt.Sprintf("%s/%s.txt", bucketURL, versionLabel)
   100  		body, err := fetcher(url, getReleaseVersionTimeout)
   101  		if err != nil {
   102  			if clientVersionErr == nil {
   103  				// Handle air-gapped environments by falling back to the client version.
   104  				klog.Warningf("could not fetch a Kubernetes version from the internet: %v", err)
   105  				klog.Warningf("falling back to the local client version: %s", clientVersion)
   106  				return kubernetesReleaseVersion(clientVersion, fetcher)
   107  			}
   108  		}
   109  
   110  		if clientVersionErr != nil {
   111  			if err != nil {
   112  				klog.Warningf("could not obtain neither client nor remote version; fall back to: %s", constants.CurrentKubernetesVersion)
   113  				return kubernetesReleaseVersion(constants.CurrentKubernetesVersion.String(), fetcher)
   114  			}
   115  
   116  			klog.Warningf("could not obtain client version; using remote version: %s", body)
   117  			return kubernetesReleaseVersion(body, fetcher)
   118  		}
   119  
   120  		// both the client and the remote version are obtained; validate them and pick a stable version
   121  		body, err = validateStableVersion(body, clientVersion)
   122  		if err != nil {
   123  			return "", err
   124  		}
   125  		// Re-validate received version and return.
   126  		return kubernetesReleaseVersion(body, fetcher)
   127  	}
   128  	return "", errors.Errorf("version %q doesn't match patterns for neither semantic version nor labels (stable, latest, ...)", version)
   129  }
   130  
   131  // KubernetesVersionToImageTag is helper function that replaces all
   132  // non-allowed symbols in tag strings with underscores.
   133  // Image tag can only contain lowercase and uppercase letters, digits,
   134  // underscores, periods and dashes.
   135  // Current usage is for CI images where all of symbols except '+' are valid,
   136  // but function is for generic usage where input can't be always pre-validated.
   137  func KubernetesVersionToImageTag(version string) string {
   138  	allowed := regexp.MustCompile(`[^-\w.]`)
   139  	return allowed.ReplaceAllString(version, "_")
   140  }
   141  
   142  // KubernetesIsCIVersion checks if user requested CI version
   143  func KubernetesIsCIVersion(version string) bool {
   144  	subs := kubeBucketPrefixes.FindAllStringSubmatch(version, 1)
   145  	if len(subs) == 1 && len(subs[0]) == 4 && strings.HasPrefix(subs[0][2], "ci") {
   146  		return true
   147  	}
   148  	return false
   149  }
   150  
   151  // Internal helper: returns normalized build version (with "v" prefix if needed)
   152  // If input doesn't match known version pattern, returns empty string.
   153  func normalizedBuildVersion(version string) string {
   154  	if kubeReleaseRegex.MatchString(version) {
   155  		if strings.HasPrefix(version, "v") {
   156  			return version
   157  		}
   158  		return "v" + version
   159  	}
   160  	return ""
   161  }
   162  
   163  // Internal helper: split version parts,
   164  // Return base URL and cleaned-up version
   165  func splitVersion(version string) (string, string, error) {
   166  	var bucketURL, urlSuffix string
   167  	subs := kubeBucketPrefixes.FindAllStringSubmatch(version, 1)
   168  	if len(subs) != 1 || len(subs[0]) != 4 {
   169  		return "", "", errors.Errorf("invalid version %q", version)
   170  	}
   171  
   172  	switch {
   173  	case strings.HasPrefix(subs[0][2], "ci"):
   174  		// Just use whichever the user specified
   175  		urlSuffix = subs[0][2]
   176  		bucketURL = kubeCIBucketURL
   177  	default:
   178  		urlSuffix = "release"
   179  		bucketURL = kubeReleaseBucketURL
   180  	}
   181  	url := fmt.Sprintf("%s/%s", bucketURL, urlSuffix)
   182  	return url, subs[0][3], nil
   183  }
   184  
   185  // Internal helper: return content of URL
   186  func fetchFromURL(url string, timeout time.Duration) (string, error) {
   187  	klog.V(2).Infof("fetching Kubernetes version from URL: %s", url)
   188  	client := &http.Client{Timeout: timeout, Transport: netutil.SetOldTransportDefaults(&http.Transport{})}
   189  	resp, err := client.Get(url)
   190  	if err != nil {
   191  		return "", errors.Errorf("unable to get URL %q: %s", url, err.Error())
   192  	}
   193  	defer resp.Body.Close()
   194  	body, err := io.ReadAll(resp.Body)
   195  	if err != nil {
   196  		return "", errors.Errorf("unable to read content of URL %q: %s", url, err.Error())
   197  	}
   198  	bodyString := strings.TrimSpace(string(body))
   199  
   200  	if resp.StatusCode != http.StatusOK {
   201  		msg := fmt.Sprintf("unable to fetch file. URL: %q, status: %v", url, resp.Status)
   202  		return bodyString, errors.New(msg)
   203  	}
   204  	return bodyString, nil
   205  }
   206  
   207  // kubeadmVersion returns the version of the client without metadata.
   208  func kubeadmVersion(info string) (string, error) {
   209  	v, err := versionutil.ParseSemantic(info)
   210  	if err != nil {
   211  		return "", errors.Wrap(err, "kubeadm version error")
   212  	}
   213  	// There is no utility in versionutil to get the version without the metadata,
   214  	// so this needs some manual formatting.
   215  	// Discard offsets after a release label and keep the labels down to e.g. `alpha.0` instead of
   216  	// including the offset e.g. `alpha.0.206`. This is done to comply with GCR image tags.
   217  	pre := v.PreRelease()
   218  	patch := v.Patch()
   219  	if len(pre) > 0 {
   220  		if patch > 0 {
   221  			// If the patch version is more than zero, decrement it and remove the label.
   222  			// this is done to comply with the latest stable patch release.
   223  			patch = patch - 1
   224  			pre = ""
   225  		} else {
   226  			split := strings.Split(pre, ".")
   227  			if len(split) > 2 {
   228  				pre = split[0] + "." + split[1] // Exclude the third element
   229  			} else if len(split) < 2 {
   230  				pre = split[0] + ".0" // Append .0 to a partial label
   231  			}
   232  			pre = "-" + pre
   233  		}
   234  	}
   235  	vStr := fmt.Sprintf("v%d.%d.%d%s", v.Major(), v.Minor(), patch, pre)
   236  	return vStr, nil
   237  }
   238  
   239  // Validate if the remote version is one Minor release newer than the client version.
   240  // This is done to conform with "stable-X" and only allow remote versions from
   241  // the same Patch level release.
   242  func validateStableVersion(remoteVersion, clientVersion string) (string, error) {
   243  	verRemote, err := versionutil.ParseGeneric(remoteVersion)
   244  	if err != nil {
   245  		return "", errors.Wrap(err, "remote version error")
   246  	}
   247  	verClient, err := versionutil.ParseGeneric(clientVersion)
   248  	if err != nil {
   249  		return "", errors.Wrap(err, "client version error")
   250  	}
   251  	// If the remote Major version is bigger or if the Major versions are the same,
   252  	// but the remote Minor is bigger use the client version release. This handles Major bumps too.
   253  	if verClient.Major() < verRemote.Major() ||
   254  		(verClient.Major() == verRemote.Major()) && verClient.Minor() < verRemote.Minor() {
   255  		estimatedRelease := fmt.Sprintf("stable-%d.%d", verClient.Major(), verClient.Minor())
   256  		klog.Infof("remote version is much newer: %s; falling back to: %s", remoteVersion, estimatedRelease)
   257  		return estimatedRelease, nil
   258  	}
   259  	return remoteVersion, nil
   260  }
   261  

View as plain text