diff --git a/cmd/clusterctl/client/repository/goproxy.go b/cmd/clusterctl/client/repository/goproxy.go index 7be56e342c20..bd0f415b7db8 100644 --- a/cmd/clusterctl/client/repository/goproxy.go +++ b/cmd/clusterctl/client/repository/goproxy.go @@ -18,6 +18,7 @@ package repository import ( "context" + "fmt" "io" "net/http" "net/url" @@ -50,57 +51,83 @@ func newGoproxyClient(scheme, host string) *goproxyClient { func (g *goproxyClient) getVersions(ctx context.Context, base, owner, repository string) ([]string, error) { // A goproxy is also able to handle the github repository path instead of the actual go module name. gomodulePath := path.Join(base, owner, repository) + parsedVersions := semver.Versions{} - rawURL := url.URL{ - Scheme: g.scheme, - Host: g.host, - Path: path.Join(gomodulePath, "@v", "/list"), - } - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL.String(), http.NoBody) - if err != nil { - return nil, errors.Wrapf(err, "failed to get versions: failed to create request") - } - - var rawResponse []byte - var retryError error - _ = wait.PollImmediateWithContext(ctx, retryableOperationInterval, retryableOperationTimeout, func(ctx context.Context) (bool, error) { - retryError = nil + majorVersionNumber := 1 + var majorVersion string + for { + if majorVersionNumber > 1 { + majorVersion = fmt.Sprintf("v%d", majorVersionNumber) + } + rawURL := url.URL{ + Scheme: g.scheme, + Host: g.host, + Path: path.Join(gomodulePath, majorVersion, "@v", "/list"), + } + majorVersionNumber++ - resp, err := http.DefaultClient.Do(req) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL.String(), http.NoBody) if err != nil { - retryError = errors.Wrapf(err, "failed to get versions: failed to do request") - return false, nil + return nil, errors.Wrapf(err, "failed to get versions: failed to create request") } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - retryError = errors.Errorf("failed to get versions: response status code %d", resp.StatusCode) - return false, nil + var rawResponse []byte + var responseStatusCode int + var retryError error + _ = wait.PollImmediateWithContext(ctx, retryableOperationInterval, retryableOperationTimeout, func(ctx context.Context) (bool, error) { + retryError = nil + + resp, err := http.DefaultClient.Do(req) + if err != nil { + retryError = errors.Wrapf(err, "failed to get versions: failed to do request") + return false, nil + } + defer resp.Body.Close() + + responseStatusCode = resp.StatusCode + + // Status codes OK and NotFound are expected results: + // * OK indicates that we got a list of versions to read. + // * NotFound indicates that there are no versions for this module / modules major version. + if responseStatusCode != http.StatusOK && responseStatusCode != http.StatusNotFound { + retryError = errors.Errorf("failed to get versions: response status code %d", resp.StatusCode) + return false, nil + } + + // only read the response for http.StatusOK + if responseStatusCode == http.StatusOK { + rawResponse, err = io.ReadAll(resp.Body) + if err != nil { + retryError = errors.Wrap(err, "failed to get versions: error reading goproxy response body") + return false, nil + } + } + return true, nil + }) + if retryError != nil { + return nil, retryError } - rawResponse, err = io.ReadAll(resp.Body) - if err != nil { - retryError = errors.Wrap(err, "failed to get versions: error reading goproxy response body") - return false, nil + // Don't try to read the versions if status was not found. + if responseStatusCode == http.StatusNotFound { + break } - return true, nil - }) - if retryError != nil { - return nil, retryError - } - parsedVersions := semver.Versions{} - for _, s := range strings.Split(string(rawResponse), "\n") { - if s == "" { - continue - } - parsedVersion, err := semver.ParseTolerant(s) - if err != nil { - // Discard releases with tags that are not a valid semantic versions (the user can point explicitly to such releases). - continue + for _, s := range strings.Split(string(rawResponse), "\n") { + if s == "" { + continue + } + parsedVersion, err := semver.ParseTolerant(s) + if err != nil { + // Discard releases with tags that are not a valid semantic versions (the user can point explicitly to such releases). + continue + } + parsedVersions = append(parsedVersions, parsedVersion) } - parsedVersions = append(parsedVersions, parsedVersion) + } + + if len(parsedVersions) == 0 { + return nil, fmt.Errorf("no versions found for go module %q", gomodulePath) } sort.Sort(parsedVersions) diff --git a/cmd/clusterctl/client/repository/repository_github_test.go b/cmd/clusterctl/client/repository/repository_github_test.go index 356523891e83..417115081dae 100644 --- a/cmd/clusterctl/client/repository/repository_github_test.go +++ b/cmd/clusterctl/client/repository/repository_github_test.go @@ -63,6 +63,21 @@ func Test_gitHubRepository_GetVersions(t *testing.T) { fmt.Fprint(w, "v0.3.1\n") }) + // setup an handler for returning 3 different major fake releases + muxGoproxy.HandleFunc("/github.com/o/r3/@v/list", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, "v1.0.0\n") + fmt.Fprint(w, "v0.1.0\n") + }) + muxGoproxy.HandleFunc("/github.com/o/r3/v2/@v/list", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, "v2.0.0\n") + }) + muxGoproxy.HandleFunc("/github.com/o/r3/v3/@v/list", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, "v3.0.0\n") + }) + configVariablesClient := test.NewFakeVariableClient() tests := []struct { @@ -83,6 +98,12 @@ func Test_gitHubRepository_GetVersions(t *testing.T) { want: []string{"v0.3.1", "v0.3.2", "v0.4.0", "v0.5.0"}, wantErr: false, }, + { + name: "use goproxy having multiple majors", + providerConfig: config.NewProvider("test", "https://github.com/o/r3/releases/v3.0.0/path", clusterctlv1.CoreProviderType), + want: []string{"v0.1.0", "v1.0.0", "v2.0.0", "v3.0.0"}, + wantErr: false, + }, { name: "failure", providerConfig: config.NewProvider("test", "https://github.com/o/unknown/releases/v0.4.0/path", clusterctlv1.CoreProviderType),