k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/cmd/kubeadm/app/util/version.go (about)

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