mirror of
				https://github.com/k3s-io/kubernetes.git
				synced 2025-11-03 23:40:03 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			245 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			245 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
/*
 | 
						|
Copyright 2016 The Kubernetes Authors.
 | 
						|
 | 
						|
Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
you may not use this file except in compliance with the License.
 | 
						|
You may obtain a copy of the License at
 | 
						|
 | 
						|
    http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
 | 
						|
Unless required by applicable law or agreed to in writing, software
 | 
						|
distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
See the License for the specific language governing permissions and
 | 
						|
limitations under the License.
 | 
						|
*/
 | 
						|
 | 
						|
package util
 | 
						|
 | 
						|
import (
 | 
						|
	"fmt"
 | 
						|
	"io/ioutil"
 | 
						|
	"net/http"
 | 
						|
	"regexp"
 | 
						|
	"strings"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/pkg/errors"
 | 
						|
 | 
						|
	netutil "k8s.io/apimachinery/pkg/util/net"
 | 
						|
	versionutil "k8s.io/apimachinery/pkg/util/version"
 | 
						|
	"k8s.io/klog"
 | 
						|
	pkgversion "k8s.io/kubernetes/pkg/version"
 | 
						|
)
 | 
						|
 | 
						|
const (
 | 
						|
	getReleaseVersionTimeout = time.Duration(10 * time.Second)
 | 
						|
)
 | 
						|
 | 
						|
var (
 | 
						|
	kubeReleaseBucketURL  = "https://dl.k8s.io"
 | 
						|
	kubeReleaseRegex      = regexp.MustCompile(`^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)([-0-9a-zA-Z_\.+]*)?$`)
 | 
						|
	kubeReleaseLabelRegex = regexp.MustCompile(`^[[:lower:]]+(-[-\w_\.]+)?$`)
 | 
						|
	kubeBucketPrefixes    = regexp.MustCompile(`^((release|ci|ci-cross)/)?([-\w_\.+]+)$`)
 | 
						|
)
 | 
						|
 | 
						|
// KubernetesReleaseVersion is helper function that can fetch
 | 
						|
// available version information from release servers based on
 | 
						|
// label names, like "stable" or "latest".
 | 
						|
//
 | 
						|
// If argument is already semantic version string, it
 | 
						|
// will return same string.
 | 
						|
//
 | 
						|
// In case of labels, it tries to fetch from release
 | 
						|
// servers and then return actual semantic version.
 | 
						|
//
 | 
						|
// Available names on release servers:
 | 
						|
//  stable      (latest stable release)
 | 
						|
//  stable-1    (latest stable release in 1.x)
 | 
						|
//  stable-1.0  (and similarly 1.1, 1.2, 1.3, ...)
 | 
						|
//  latest      (latest release, including alpha/beta)
 | 
						|
//  latest-1    (latest release in 1.x, including alpha/beta)
 | 
						|
//  latest-1.0  (and similarly 1.1, 1.2, 1.3, ...)
 | 
						|
func KubernetesReleaseVersion(version string) (string, error) {
 | 
						|
	ver := normalizedBuildVersion(version)
 | 
						|
	if len(ver) != 0 {
 | 
						|
		return ver, nil
 | 
						|
	}
 | 
						|
 | 
						|
	bucketURL, versionLabel, err := splitVersion(version)
 | 
						|
	if err != nil {
 | 
						|
		return "", err
 | 
						|
	}
 | 
						|
 | 
						|
	// revalidate, if exact build from e.g. CI bucket requested.
 | 
						|
	ver = normalizedBuildVersion(versionLabel)
 | 
						|
	if len(ver) != 0 {
 | 
						|
		return ver, nil
 | 
						|
	}
 | 
						|
 | 
						|
	// kubeReleaseLabelRegex matches labels such as: latest, latest-1, latest-1.10
 | 
						|
	if kubeReleaseLabelRegex.MatchString(versionLabel) {
 | 
						|
		// Try to obtain a client version.
 | 
						|
		// pkgversion.Get().String() should always return a correct version added by the golang
 | 
						|
		// linker and the build system. The version can still be missing when doing unit tests
 | 
						|
		// on individual packages.
 | 
						|
		clientVersion, clientVersionErr := kubeadmVersion(pkgversion.Get().String())
 | 
						|
		// Fetch version from the internet.
 | 
						|
		url := fmt.Sprintf("%s/%s.txt", bucketURL, versionLabel)
 | 
						|
		body, err := fetchFromURL(url, getReleaseVersionTimeout)
 | 
						|
		if err != nil {
 | 
						|
			// If the network operaton was successful but the server did not reply with StatusOK
 | 
						|
			if body != "" {
 | 
						|
				return "", err
 | 
						|
			}
 | 
						|
			// Handle air-gapped environments by falling back to the client version.
 | 
						|
			klog.Infof("could not fetch a Kubernetes version from the internet: %v", err)
 | 
						|
			klog.Infof("falling back to the local client version: %s", clientVersion)
 | 
						|
			return KubernetesReleaseVersion(clientVersion)
 | 
						|
		}
 | 
						|
 | 
						|
		if clientVersionErr != nil {
 | 
						|
			klog.Warningf("could not obtain client version; using remote version: %s", body)
 | 
						|
			return KubernetesReleaseVersion(body)
 | 
						|
		}
 | 
						|
 | 
						|
		// both the client and the remote version are obtained; validate them and pick a stable version
 | 
						|
		body, err = validateStableVersion(body, clientVersion)
 | 
						|
		if err != nil {
 | 
						|
			return "", err
 | 
						|
		}
 | 
						|
		// Re-validate received version and return.
 | 
						|
		return KubernetesReleaseVersion(body)
 | 
						|
	}
 | 
						|
	return "", errors.Errorf("version %q doesn't match patterns for neither semantic version nor labels (stable, latest, ...)", version)
 | 
						|
}
 | 
						|
 | 
						|
// KubernetesVersionToImageTag is helper function that replaces all
 | 
						|
// non-allowed symbols in tag strings with underscores.
 | 
						|
// Image tag can only contain lowercase and uppercase letters, digits,
 | 
						|
// underscores, periods and dashes.
 | 
						|
// Current usage is for CI images where all of symbols except '+' are valid,
 | 
						|
// but function is for generic usage where input can't be always pre-validated.
 | 
						|
func KubernetesVersionToImageTag(version string) string {
 | 
						|
	allowed := regexp.MustCompile(`[^-a-zA-Z0-9_\.]`)
 | 
						|
	return allowed.ReplaceAllString(version, "_")
 | 
						|
}
 | 
						|
 | 
						|
// KubernetesIsCIVersion checks if user requested CI version
 | 
						|
func KubernetesIsCIVersion(version string) bool {
 | 
						|
	subs := kubeBucketPrefixes.FindAllStringSubmatch(version, 1)
 | 
						|
	if len(subs) == 1 && len(subs[0]) == 4 && strings.HasPrefix(subs[0][2], "ci") {
 | 
						|
		return true
 | 
						|
	}
 | 
						|
	return false
 | 
						|
}
 | 
						|
 | 
						|
// Internal helper: returns normalized build version (with "v" prefix if needed)
 | 
						|
// If input doesn't match known version pattern, returns empty string.
 | 
						|
func normalizedBuildVersion(version string) string {
 | 
						|
	if kubeReleaseRegex.MatchString(version) {
 | 
						|
		if strings.HasPrefix(version, "v") {
 | 
						|
			return version
 | 
						|
		}
 | 
						|
		return "v" + version
 | 
						|
	}
 | 
						|
	return ""
 | 
						|
}
 | 
						|
 | 
						|
// Internal helper: split version parts,
 | 
						|
// Return base URL and cleaned-up version
 | 
						|
func splitVersion(version string) (string, string, error) {
 | 
						|
	var urlSuffix string
 | 
						|
	subs := kubeBucketPrefixes.FindAllStringSubmatch(version, 1)
 | 
						|
	if len(subs) != 1 || len(subs[0]) != 4 {
 | 
						|
		return "", "", errors.Errorf("invalid version %q", version)
 | 
						|
	}
 | 
						|
 | 
						|
	switch {
 | 
						|
	case strings.HasPrefix(subs[0][2], "ci"):
 | 
						|
		// Just use whichever the user specified
 | 
						|
		urlSuffix = subs[0][2]
 | 
						|
	default:
 | 
						|
		urlSuffix = "release"
 | 
						|
	}
 | 
						|
	url := fmt.Sprintf("%s/%s", kubeReleaseBucketURL, urlSuffix)
 | 
						|
	return url, subs[0][3], nil
 | 
						|
}
 | 
						|
 | 
						|
// Internal helper: return content of URL
 | 
						|
func fetchFromURL(url string, timeout time.Duration) (string, error) {
 | 
						|
	klog.V(2).Infof("fetching Kubernetes version from URL: %s", url)
 | 
						|
	client := &http.Client{Timeout: timeout, Transport: netutil.SetOldTransportDefaults(&http.Transport{})}
 | 
						|
	resp, err := client.Get(url)
 | 
						|
	if err != nil {
 | 
						|
		return "", errors.Errorf("unable to get URL %q: %s", url, err.Error())
 | 
						|
	}
 | 
						|
	defer resp.Body.Close()
 | 
						|
	body, err := ioutil.ReadAll(resp.Body)
 | 
						|
	if err != nil {
 | 
						|
		return "", errors.Errorf("unable to read content of URL %q: %s", url, err.Error())
 | 
						|
	}
 | 
						|
	bodyString := strings.TrimSpace(string(body))
 | 
						|
 | 
						|
	if resp.StatusCode != http.StatusOK {
 | 
						|
		msg := fmt.Sprintf("unable to fetch file. URL: %q, status: %v", url, resp.Status)
 | 
						|
		return bodyString, errors.New(msg)
 | 
						|
	}
 | 
						|
	return bodyString, nil
 | 
						|
}
 | 
						|
 | 
						|
// kubeadmVersion returns the version of the client without metadata.
 | 
						|
func kubeadmVersion(info string) (string, error) {
 | 
						|
	v, err := versionutil.ParseSemantic(info)
 | 
						|
	if err != nil {
 | 
						|
		return "", errors.Wrap(err, "kubeadm version error")
 | 
						|
	}
 | 
						|
	// There is no utility in versionutil to get the version without the metadata,
 | 
						|
	// so this needs some manual formatting.
 | 
						|
	// Discard offsets after a release label and keep the labels down to e.g. `alpha.0` instead of
 | 
						|
	// including the offset e.g. `alpha.0.206`. This is done to comply with GCR image tags.
 | 
						|
	pre := v.PreRelease()
 | 
						|
	patch := v.Patch()
 | 
						|
	if len(pre) > 0 {
 | 
						|
		if patch > 0 {
 | 
						|
			// If the patch version is more than zero, decrement it and remove the label.
 | 
						|
			// this is done to comply with the latest stable patch release.
 | 
						|
			patch = patch - 1
 | 
						|
			pre = ""
 | 
						|
		} else {
 | 
						|
			split := strings.Split(pre, ".")
 | 
						|
			if len(split) > 2 {
 | 
						|
				pre = split[0] + "." + split[1] // Exclude the third element
 | 
						|
			} else if len(split) < 2 {
 | 
						|
				pre = split[0] + ".0" // Append .0 to a partial label
 | 
						|
			}
 | 
						|
			pre = "-" + pre
 | 
						|
		}
 | 
						|
	}
 | 
						|
	vStr := fmt.Sprintf("v%d.%d.%d%s", v.Major(), v.Minor(), patch, pre)
 | 
						|
	return vStr, nil
 | 
						|
}
 | 
						|
 | 
						|
// Validate if the remote version is one Minor release newer than the client version.
 | 
						|
// This is done to conform with "stable-X" and only allow remote versions from
 | 
						|
// the same Patch level release.
 | 
						|
func validateStableVersion(remoteVersion, clientVersion string) (string, error) {
 | 
						|
	verRemote, err := versionutil.ParseGeneric(remoteVersion)
 | 
						|
	if err != nil {
 | 
						|
		return "", errors.Wrap(err, "remote version error")
 | 
						|
	}
 | 
						|
	verClient, err := versionutil.ParseGeneric(clientVersion)
 | 
						|
	if err != nil {
 | 
						|
		return "", errors.Wrap(err, "client version error")
 | 
						|
	}
 | 
						|
	// If the remote Major version is bigger or if the Major versions are the same,
 | 
						|
	// but the remote Minor is bigger use the client version release. This handles Major bumps too.
 | 
						|
	if verClient.Major() < verRemote.Major() ||
 | 
						|
		(verClient.Major() == verRemote.Major()) && verClient.Minor() < verRemote.Minor() {
 | 
						|
		estimatedRelease := fmt.Sprintf("stable-%d.%d", verClient.Major(), verClient.Minor())
 | 
						|
		klog.Infof("remote version is much newer: %s; falling back to: %s", remoteVersion, estimatedRelease)
 | 
						|
		return estimatedRelease, nil
 | 
						|
	}
 | 
						|
	return remoteVersion, nil
 | 
						|
}
 |