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 }