Skip to content

Commit 5ff76fb

Browse files
authored
Merge pull request #7192 from chrischdi/pr-optimize-gh-requests
✨ Reduce github api requests in clusterctl by querying go modules
2 parents 3ad7b0e + f7db0c7 commit 5ff76fb

File tree

9 files changed

+472
-50
lines changed

9 files changed

+472
-50
lines changed

cmd/clusterctl/client/config/cert_manager.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ package config
1919
// CertManager defines cert-manager configuration.
2020
type CertManager interface {
2121
// URL returns the name of the cert-manager repository.
22-
// If empty, "https://github.com/cert-manager/cert-manager/releases/latest/cert-manager.yaml" will be used.
22+
// If empty, "https://github.com/cert-manager/cert-manager/releases/{DefaultVersion}/cert-manager.yaml" will be used.
2323
URL() string
2424

2525
// Version returns the cert-manager version to install.

cmd/clusterctl/client/config/cert_manager_client.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ const (
3232
CertManagerDefaultVersion = "v1.10.0"
3333

3434
// CertManagerDefaultURL defines the default cert-manager repository url to be used by clusterctl.
35-
// NOTE: At runtime /latest will be replaced with the CertManagerDefaultVersion or with the
35+
// NOTE: At runtime CertManagerDefaultVersion may be replaced with the
3636
// version defined by the user in the clusterctl configuration file.
37-
CertManagerDefaultURL = "https://github.com/cert-manager/cert-manager/releases/latest/cert-manager.yaml"
37+
CertManagerDefaultURL = "https://github.com/cert-manager/cert-manager/releases/" + CertManagerDefaultVersion + "/cert-manager.yaml"
3838

3939
// CertManagerDefaultTimeout defines the default cert-manager timeout to be used by clusterctl.
4040
CertManagerDefaultTimeout = 10 * time.Minute
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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 repository
18+
19+
import (
20+
"context"
21+
"io"
22+
"net/http"
23+
"net/url"
24+
"path"
25+
"path/filepath"
26+
"sort"
27+
"strings"
28+
29+
"github.com/blang/semver"
30+
"github.com/pkg/errors"
31+
"k8s.io/apimachinery/pkg/util/wait"
32+
)
33+
34+
const (
35+
defaultGoProxyHost = "proxy.golang.org"
36+
)
37+
38+
type goproxyClient struct {
39+
scheme string
40+
host string
41+
}
42+
43+
func newGoproxyClient(scheme, host string) *goproxyClient {
44+
return &goproxyClient{
45+
scheme: scheme,
46+
host: host,
47+
}
48+
}
49+
50+
func (g *goproxyClient) getVersions(ctx context.Context, base, owner, repository string) ([]string, error) {
51+
// A goproxy is also able to handle the github repository path instead of the actual go module name.
52+
gomodulePath := path.Join(base, owner, repository)
53+
54+
rawURL := url.URL{
55+
Scheme: g.scheme,
56+
Host: g.host,
57+
Path: path.Join(gomodulePath, "@v", "/list"),
58+
}
59+
60+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL.String(), http.NoBody)
61+
if err != nil {
62+
return nil, errors.Wrapf(err, "failed to get versions: failed to create request")
63+
}
64+
65+
var rawResponse []byte
66+
var retryError error
67+
_ = wait.PollImmediateWithContext(ctx, retryableOperationInterval, retryableOperationTimeout, func(ctx context.Context) (bool, error) {
68+
retryError = nil
69+
70+
resp, err := http.DefaultClient.Do(req)
71+
if err != nil {
72+
retryError = errors.Wrapf(err, "failed to get versions: failed to do request")
73+
return false, nil
74+
}
75+
defer resp.Body.Close()
76+
77+
if resp.StatusCode != 200 {
78+
retryError = errors.Errorf("failed to get versions: response status code %d", resp.StatusCode)
79+
return false, nil
80+
}
81+
82+
rawResponse, err = io.ReadAll(resp.Body)
83+
if err != nil {
84+
retryError = errors.Wrap(err, "failed to get versions: error reading goproxy response body")
85+
return false, nil
86+
}
87+
return true, nil
88+
})
89+
if retryError != nil {
90+
return nil, retryError
91+
}
92+
93+
parsedVersions := semver.Versions{}
94+
for _, s := range strings.Split(string(rawResponse), "\n") {
95+
if s == "" {
96+
continue
97+
}
98+
parsedVersion, err := semver.ParseTolerant(s)
99+
if err != nil {
100+
// Discard releases with tags that are not a valid semantic versions (the user can point explicitly to such releases).
101+
continue
102+
}
103+
parsedVersions = append(parsedVersions, parsedVersion)
104+
}
105+
106+
sort.Sort(parsedVersions)
107+
108+
versions := []string{}
109+
for _, v := range parsedVersions {
110+
versions = append(versions, "v"+v.String())
111+
}
112+
113+
return versions, nil
114+
}
115+
116+
// getGoproxyHost detects and returns the scheme and host for goproxy requests.
117+
// It returns empty strings if goproxy is disabled via `off` or `direct` values.
118+
func getGoproxyHost(goproxy string) (string, string, error) {
119+
// Fallback to default
120+
if goproxy == "" {
121+
return "https", defaultGoProxyHost, nil
122+
}
123+
124+
var goproxyHost, goproxyScheme string
125+
// xref https://github.com/golang/go/blob/master/src/cmd/go/internal/modfetch/proxy.go
126+
for goproxy != "" {
127+
var rawURL string
128+
if i := strings.IndexAny(goproxy, ",|"); i >= 0 {
129+
rawURL = goproxy[:i]
130+
goproxy = goproxy[i+1:]
131+
} else {
132+
rawURL = goproxy
133+
goproxy = ""
134+
}
135+
136+
rawURL = strings.TrimSpace(rawURL)
137+
if rawURL == "" {
138+
continue
139+
}
140+
if rawURL == "off" || rawURL == "direct" {
141+
// Return nothing to fallback to github repository client without an error.
142+
return "", "", nil
143+
}
144+
145+
// Single-word tokens are reserved for built-in behaviors, and anything
146+
// containing the string ":/" or matching an absolute file path must be a
147+
// complete URL. For all other paths, implicitly add "https://".
148+
if strings.ContainsAny(rawURL, ".:/") && !strings.Contains(rawURL, ":/") && !filepath.IsAbs(rawURL) && !path.IsAbs(rawURL) {
149+
rawURL = "https://" + rawURL
150+
}
151+
152+
parsedURL, err := url.Parse(rawURL)
153+
if err != nil {
154+
return "", "", errors.Wrapf(err, "parse GOPROXY url %q", rawURL)
155+
}
156+
goproxyHost = parsedURL.Host
157+
goproxyScheme = parsedURL.Scheme
158+
// A host was found so no need to continue.
159+
break
160+
}
161+
162+
return goproxyScheme, goproxyHost, nil
163+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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 repository
18+
19+
import (
20+
"testing"
21+
"time"
22+
)
23+
24+
func Test_getGoproxyHost(t *testing.T) {
25+
retryableOperationInterval = 200 * time.Millisecond
26+
retryableOperationTimeout = 1 * time.Second
27+
28+
tests := []struct {
29+
name string
30+
envvar string
31+
wantScheme string
32+
wantHost string
33+
wantErr bool
34+
}{
35+
{
36+
name: "defaulting",
37+
envvar: "",
38+
wantScheme: "https",
39+
wantHost: "proxy.golang.org",
40+
wantErr: false,
41+
},
42+
{
43+
name: "direct falls back to empty strings",
44+
envvar: "direct",
45+
wantScheme: "",
46+
wantHost: "",
47+
wantErr: false,
48+
},
49+
{
50+
name: "off falls back to empty strings",
51+
envvar: "off",
52+
wantScheme: "",
53+
wantHost: "",
54+
wantErr: false,
55+
},
56+
{
57+
name: "other goproxy",
58+
envvar: "foo.bar.de",
59+
wantScheme: "https",
60+
wantHost: "foo.bar.de",
61+
wantErr: false,
62+
},
63+
{
64+
name: "other goproxy comma separated, return first",
65+
envvar: "foo.bar,foobar.barfoo",
66+
wantScheme: "https",
67+
wantHost: "foo.bar",
68+
wantErr: false,
69+
},
70+
{
71+
name: "other goproxy including https scheme",
72+
envvar: "https://foo.bar",
73+
wantScheme: "https",
74+
wantHost: "foo.bar",
75+
wantErr: false,
76+
},
77+
{
78+
name: "other goproxy including http scheme",
79+
envvar: "http://foo.bar",
80+
wantScheme: "http",
81+
wantHost: "foo.bar",
82+
wantErr: false,
83+
},
84+
}
85+
for _, tt := range tests {
86+
t.Run(tt.name, func(t *testing.T) {
87+
gotScheme, gotHost, err := getGoproxyHost(tt.envvar)
88+
if (err != nil) != tt.wantErr {
89+
t.Errorf("getGoproxyHost() error = %v, wantErr %v", err, tt.wantErr)
90+
return
91+
}
92+
if gotScheme != tt.wantScheme {
93+
t.Errorf("getGoproxyHost() = %v, wantScheme %v", gotScheme, tt.wantScheme)
94+
}
95+
if gotHost != tt.wantHost {
96+
t.Errorf("getGoproxyHost() = %v, wantHost %v", gotHost, tt.wantHost)
97+
}
98+
})
99+
}
100+
}

0 commit comments

Comments
 (0)