diff --git a/cmd/clusterctl/client/repository/repository_github.go b/cmd/clusterctl/client/repository/repository_github.go index 38f4b12cdcb8..ea8999946f18 100644 --- a/cmd/clusterctl/client/repository/repository_github.go +++ b/cmd/clusterctl/client/repository/repository_github.go @@ -49,6 +49,8 @@ const ( ) var ( + errNotFound = errors.New("404 Not Found") + // Caches used to limit the number of GitHub API calls. cacheVersions = map[string][]string{} @@ -226,7 +228,7 @@ func NewGitHubRepository(providerConfig config.Provider, configVariablesClient c if defaultVersion == githubLatestReleaseLabel { repo.defaultVersion, err = latestContractRelease(repo, clusterv1.GroupVersion.Version) if err != nil { - return nil, errors.Wrap(err, "failed to get GitHub latest version") + return nil, errors.Wrap(err, "failed to get latest release") } } @@ -332,6 +334,10 @@ func (g *gitHubRepository) getReleaseByTag(tag string) (*github.RepositoryReleas release, _, getReleasesErr = client.Repositories.GetReleaseByTag(context.TODO(), g.owner, g.repository, tag) if getReleasesErr != nil { retryError = g.handleGithubErr(getReleasesErr, "failed to read release %q", tag) + // return immediately if not found + if errors.Is(retryError, errNotFound) { + return false, retryError + } // return immediately if we are rate limited if _, ok := getReleasesErr.(*github.RateLimitError); ok { return false, retryError @@ -418,5 +424,10 @@ func (g *gitHubRepository) handleGithubErr(err error, message string, args ...in if _, ok := err.(*github.RateLimitError); ok { return errors.New("rate limit for github api has been reached. Please wait one hour or get a personal API token and assign it to the GITHUB_TOKEN environment variable") } + if ghErr, ok := err.(*github.ErrorResponse); ok { + if ghErr.Response.StatusCode == http.StatusNotFound { + return errNotFound + } + } return errors.Wrapf(err, message, args...) } diff --git a/cmd/clusterctl/client/repository/repository_github_test.go b/cmd/clusterctl/client/repository/repository_github_test.go index a24631102704..2aebf07c88d8 100644 --- a/cmd/clusterctl/client/repository/repository_github_test.go +++ b/cmd/clusterctl/client/repository/repository_github_test.go @@ -421,6 +421,15 @@ func Test_gitHubRepository_getLatestContractRelease(t *testing.T) { fmt.Fprint(w, "v0.3.1\n") }) + // setup an handler for returning 4 fake releases but no actual tagged release + muxGoproxy.HandleFunc("/github.com/o/r2/@v/list", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, "v0.5.0\n") + fmt.Fprint(w, "v0.4.0\n") + fmt.Fprint(w, "v0.3.2\n") + fmt.Fprint(w, "v0.3.1\n") + }) + configVariablesClient := test.NewFakeVariableClient() type field struct { @@ -460,6 +469,15 @@ func Test_gitHubRepository_getLatestContractRelease(t *testing.T) { contract: "foo", wantErr: false, }, + { + name: "Return 404 if there is no release for the tag", + field: field{ + providerConfig: config.NewProvider("test", "https://github.com/o/r2/releases/v0.99.0/path", clusterctlv1.CoreProviderType), + }, + want: "0.99.0", + contract: "v1alpha4", + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -494,6 +512,11 @@ func Test_gitHubRepository_getLatestRelease(t *testing.T) { fmt.Fprint(w, "v0.4.3-alpha\n") // prerelease fmt.Fprint(w, "foo\n") // no semantic version tag }) + // And also expose a release for them + muxGoproxy.HandleFunc("/api.github.com/repos/o/r1/releases/tags/v0.4.2", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, "{}\n") + }) // setup an handler for returning no releases muxGoproxy.HandleFunc("/github.com/o/r2/@v/list", func(w http.ResponseWriter, r *http.Request) { diff --git a/cmd/clusterctl/client/repository/repository_versions.go b/cmd/clusterctl/client/repository/repository_versions.go index f4b710408d47..bd5ab56121c3 100644 --- a/cmd/clusterctl/client/repository/repository_versions.go +++ b/cmd/clusterctl/client/repository/repository_versions.go @@ -39,11 +39,16 @@ func latestContractRelease(repo Repository, contract string) (string, error) { } // Attempt to check if the latest release satisfies the API Contract // This is a best-effort attempt to find the latest release for an older API contract if it's not the latest release. - // If an error occurs, we just return the latest release. file, err := repo.GetFile(latest, metadataFile) + // If an error occurs, we just return the latest release. if err != nil { + if errors.Is(err, errNotFound) { + // If it was ErrNotFound, then there is no release yet for the resolved tag. + // Ref: https://github.com/kubernetes-sigs/cluster-api/issues/7889 + return "", errors.Wrap(err, "release does not exist yet, consider setting \"GOPROXY=off\" as workaround") + } // if we can't get the metadata file from the release, we return latest. - return latest, nil //nolint:nilerr + return latest, nil } latestMetadata := &clusterctlv1.Metadata{} codecFactory := serializer.NewCodecFactory(scheme.Scheme)