sigs.k8s.io/cluster-api@v1.7.1/internal/goproxy/goproxy.go (about) 1 /* 2 Copyright 2022 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 goproxy 18 19 import ( 20 "context" 21 "fmt" 22 "io" 23 "net/http" 24 "net/url" 25 "path" 26 "path/filepath" 27 "sort" 28 "strings" 29 "time" 30 31 "github.com/blang/semver/v4" 32 "github.com/pkg/errors" 33 "k8s.io/apimachinery/pkg/util/wait" 34 ) 35 36 const ( 37 defaultGoProxyHost = "proxy.golang.org" 38 ) 39 40 var ( 41 retryableOperationInterval = 10 * time.Second 42 retryableOperationTimeout = 1 * time.Minute 43 ) 44 45 // Client is a client to query versions from a goproxy instance. 46 type Client struct { 47 scheme string 48 host string 49 } 50 51 // NewClient returns a new goproxyClient instance. 52 func NewClient(scheme, host string) *Client { 53 return &Client{ 54 scheme: scheme, 55 host: host, 56 } 57 } 58 59 // GetVersions returns the a sorted list of semantical versions which exist for a go module. 60 func (g *Client) GetVersions(ctx context.Context, gomodulePath string) (semver.Versions, error) { 61 parsedVersions := semver.Versions{} 62 63 majorVersionNumber := 1 64 var majorVersion string 65 for { 66 if majorVersionNumber > 1 { 67 majorVersion = fmt.Sprintf("v%d", majorVersionNumber) 68 } 69 rawURL := url.URL{ 70 Scheme: g.scheme, 71 Host: g.host, 72 Path: path.Join(gomodulePath, majorVersion, "@v", "/list"), 73 } 74 majorVersionNumber++ 75 76 req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL.String(), http.NoBody) 77 if err != nil { 78 return nil, errors.Wrapf(err, "failed to get versions: failed to create request") 79 } 80 81 var rawResponse []byte 82 var responseStatusCode int 83 var retryError error 84 _ = wait.PollUntilContextTimeout(ctx, retryableOperationInterval, retryableOperationTimeout, true, func(context.Context) (bool, error) { 85 retryError = nil 86 87 resp, err := http.DefaultClient.Do(req) 88 if err != nil { 89 retryError = errors.Wrapf(err, "failed to get versions: failed to do request") 90 return false, nil 91 } 92 defer resp.Body.Close() 93 94 responseStatusCode = resp.StatusCode 95 96 // Status codes OK and NotFound are expected results: 97 // * OK indicates that we got a list of versions to read. 98 // * NotFound indicates that there are no versions for this module / modules major version. 99 if responseStatusCode != http.StatusOK && responseStatusCode != http.StatusNotFound { 100 retryError = errors.Errorf("failed to get versions: response status code %d", resp.StatusCode) 101 return false, nil 102 } 103 104 // only read the response for http.StatusOK 105 if responseStatusCode == http.StatusOK { 106 rawResponse, err = io.ReadAll(resp.Body) 107 if err != nil { 108 retryError = errors.Wrap(err, "failed to get versions: error reading goproxy response body") 109 return false, nil 110 } 111 } 112 return true, nil 113 }) 114 if retryError != nil { 115 return nil, retryError 116 } 117 118 // Don't try to read the versions if status was not found. 119 if responseStatusCode == http.StatusNotFound { 120 break 121 } 122 123 for _, s := range strings.Split(string(rawResponse), "\n") { 124 if s == "" { 125 continue 126 } 127 parsedVersion, err := semver.ParseTolerant(s) 128 if err != nil { 129 // Discard releases with tags that are not a valid semantic versions (the user can point explicitly to such releases). 130 continue 131 } 132 parsedVersions = append(parsedVersions, parsedVersion) 133 } 134 } 135 136 if len(parsedVersions) == 0 { 137 return nil, fmt.Errorf("no versions found for go module %q", gomodulePath) 138 } 139 140 sort.Sort(parsedVersions) 141 142 return parsedVersions, nil 143 } 144 145 // GetSchemeAndHost detects and returns the scheme and host for goproxy requests. 146 // It returns empty strings if goproxy is disabled via `off` or `direct` values. 147 func GetSchemeAndHost(goproxy string) (string, string, error) { 148 // Fallback to default 149 if goproxy == "" { 150 return "https", defaultGoProxyHost, nil 151 } 152 153 var goproxyHost, goproxyScheme string 154 // xref https://github.com/golang/go/blob/master/src/cmd/go/internal/modfetch/proxy.go 155 for goproxy != "" { 156 var rawURL string 157 if i := strings.IndexAny(goproxy, ",|"); i >= 0 { 158 rawURL = goproxy[:i] 159 goproxy = goproxy[i+1:] 160 } else { 161 rawURL = goproxy 162 goproxy = "" 163 } 164 165 rawURL = strings.TrimSpace(rawURL) 166 if rawURL == "" { 167 continue 168 } 169 if rawURL == "off" || rawURL == "direct" { 170 // Return nothing to fallback to github repository client without an error. 171 return "", "", nil 172 } 173 174 // Single-word tokens are reserved for built-in behaviors, and anything 175 // containing the string ":/" or matching an absolute file path must be a 176 // complete URL. For all other paths, implicitly add "https://". 177 if strings.ContainsAny(rawURL, ".:/") && !strings.Contains(rawURL, ":/") && !filepath.IsAbs(rawURL) && !path.IsAbs(rawURL) { 178 rawURL = "https://" + rawURL 179 } 180 181 parsedURL, err := url.Parse(rawURL) 182 if err != nil { 183 return "", "", errors.Wrapf(err, "parse GOPROXY url %q", rawURL) 184 } 185 goproxyHost = parsedURL.Host 186 goproxyScheme = parsedURL.Scheme 187 // A host was found so no need to continue. 188 break 189 } 190 191 return goproxyScheme, goproxyHost, nil 192 }