version.go 9.2 KB

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