1
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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67 func KubernetesReleaseVersion(version string) (string, error) {
68 return kubernetesReleaseVersion(version, fetchFromURL)
69 }
70
71
72
73
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
86 ver = normalizedBuildVersion(versionLabel)
87 if len(ver) != 0 {
88 return ver, nil
89 }
90
91
92 if kubeReleaseLabelRegex.MatchString(versionLabel) {
93
94
95
96
97 clientVersion, clientVersionErr := kubeadmVersion(pkgversion.Get().String())
98
99 url := fmt.Sprintf("%s/%s.txt", bucketURL, versionLabel)
100 body, err := fetcher(url, getReleaseVersionTimeout)
101 if err != nil {
102 if clientVersionErr == nil {
103
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
121 body, err = validateStableVersion(body, clientVersion)
122 if err != nil {
123 return "", err
124 }
125
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
132
133
134
135
136
137 func KubernetesVersionToImageTag(version string) string {
138 allowed := regexp.MustCompile(`[^-\w.]`)
139 return allowed.ReplaceAllString(version, "_")
140 }
141
142
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
152
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
164
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
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
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
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
214
215
216
217 pre := v.PreRelease()
218 patch := v.Patch()
219 if len(pre) > 0 {
220 if patch > 0 {
221
222
223 patch = patch - 1
224 pre = ""
225 } else {
226 split := strings.Split(pre, ".")
227 if len(split) > 2 {
228 pre = split[0] + "." + split[1]
229 } else if len(split) < 2 {
230 pre = split[0] + ".0"
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
240
241
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
252
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