diff --git a/cmd/clusterctl/client/cluster/template.go b/cmd/clusterctl/client/cluster/template.go index 7e3ea76a6ae7..3de5afbf7c9f 100644 --- a/cmd/clusterctl/client/cluster/template.go +++ b/cmd/clusterctl/client/cluster/template.go @@ -19,6 +19,7 @@ package cluster import ( "context" "encoding/base64" + "fmt" "io" "net/http" "net/url" @@ -180,37 +181,58 @@ func (t *templateClient) getGitHubFileContent(rURL *url.URL) ([]byte, error) { urlSplit := strings.Split(strings.TrimPrefix(rURL.Path, "/"), "/") if len(urlSplit) < 5 { return nil, errors.Errorf( - "invalid GitHub url %q: a GitHub url should be in the form https://github.com/{owner}/{repository}/blob/{branch}/{path-to-file}", rURL, + "invalid GitHub url %q: a GitHub url should be in on of these the forms\n"+ + "- https://github.com/{owner}/{repository}/blob/{branch}/{path-to-file}\n"+ + "- https://github.com/{owner}/{repository}/releases/download/{tag}/{asset-file-name}", rURL, ) } // Extract all the info from url split. owner := urlSplit[0] - repository := urlSplit[1] - branch := urlSplit[3] - path := strings.Join(urlSplit[4:], "/") + repo := urlSplit[1] + linkType := urlSplit[2] // gets the GitHub client - client, err := t.gitHubClientFactory(t.configClient.Variables()) + ghClient, err := t.gitHubClientFactory(t.configClient.Variables()) if err != nil { return nil, err } // gets the file from GiHub - fileContent, _, _, err := client.Repositories.GetContents(context.TODO(), owner, repository, path, &github.RepositoryContentGetOptions{Ref: branch}) + switch linkType { + case "blob": // get file from a code in a github repo + branch := urlSplit[3] + path := strings.Join(urlSplit[4:], "/") + + return getGithubFileContentFromCode(ghClient, rURL.Path, owner, repo, path, branch) + + case "releases": // get a github release asset + if urlSplit[3] != "download" { + break + } + tag := urlSplit[4] + assetName := urlSplit[5] + + return getGithubAssetFromRelease(ghClient, rURL.Path, owner, repo, tag, assetName) + } + + return nil, fmt.Errorf("unknown github URL: %v", rURL) +} + +func getGithubFileContentFromCode(ghClient *github.Client, fullPath string, owner string, repo string, path string, branch string) ([]byte, error) { + fileContent, _, _, err := ghClient.Repositories.GetContents(ctx, owner, repo, path, &github.RepositoryContentGetOptions{Ref: branch}) if err != nil { - return nil, handleGithubErr(err, "failed to get %q", rURL.Path) + return nil, handleGithubErr(err, "failed to get %q", fullPath) } if fileContent == nil { - return nil, errors.Errorf("%q does not return a valid file content", rURL.Path) + return nil, errors.Errorf("%q does not return a valid file content", fullPath) } if fileContent.Encoding == nil || *fileContent.Encoding != "base64" { - return nil, errors.Errorf("invalid encoding detected for %q. Only base64 encoding supported", rURL.Path) + return nil, errors.Errorf("invalid encoding detected for %q. Only base64 encoding supported", fullPath) } - content, err := base64.StdEncoding.DecodeString(*fileContent.Content) if err != nil { - return nil, errors.Wrapf(err, "failed to decode file %q", rURL.Path) + return nil, errors.Wrapf(err, "failed to decode file %q", fullPath) } return content, nil } @@ -239,6 +261,36 @@ func (t *templateClient) getRawURLFileContent(rURL string) ([]byte, error) { return content, nil } +func getGithubAssetFromRelease(ghClient *github.Client, path string, owner string, repo string, tag string, assetName string) ([]byte, error) { + release, _, err := ghClient.Repositories.GetReleaseByTag(ctx, owner, repo, tag) + if err != nil { + return nil, handleGithubErr(err, "failed to get release '%s' from %s/%s repository", tag, owner, repo) + } + + if release == nil { + return nil, fmt.Errorf("can't find release '%s' in %s/%s repository", tag, owner, repo) + } + + var rc io.ReadCloser + for _, asset := range release.Assets { + if asset.GetName() == assetName { + rc, _, err = ghClient.Repositories.DownloadReleaseAsset(ctx, owner, repo, asset.GetID(), ghClient.Client()) + if err != nil { + return nil, errors.Wrapf(err, "failed to download file %q", path) + } + break + } + } + + if rc == nil { + return nil, fmt.Errorf("failed to download the file %q", path) + } + + defer func() { _ = rc.Close() }() + + return io.ReadAll(rc) +} + func getGitHubClient(configVariablesClient config.VariablesClient) (*github.Client, error) { var authenticatingHTTPClient *http.Client if token, err := configVariablesClient.Get(config.GitHubTokenVariable); err == nil { diff --git a/cmd/clusterctl/client/cluster/template_test.go b/cmd/clusterctl/client/cluster/template_test.go index 696b7f6b412a..4e77cad946c8 100644 --- a/cmd/clusterctl/client/cluster/template_test.go +++ b/cmd/clusterctl/client/cluster/template_test.go @@ -348,6 +348,52 @@ func Test_templateClient_GetFromURL(t *testing.T) { }`) }) + mux.HandleFunc("/repos/some-owner/some-repo/releases/tags/v1.0.0", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, `{ + "tag_name": "v1.0.0", + "name": "v1.0.0", + "id": 12345678, + "url": "https://api.github.com/repos/some-owner/some-repo/releases/12345678", + "assets": [ + { + "id": 87654321, + "name": "cluster-template.yaml" + } + ] + }`) + }) + + mux.HandleFunc("/repos/some-owner/some-repo/releases/assets/87654321", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, template) + }) + + mux.HandleFunc("/repos/some-owner/some-repo/releases/tags/v2.0.0", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, `{ + "tag_name": "v2.0.0", + "name": "v2.0.0", + "id": 12345678, + "url": "https://api.github.com/repos/some-owner/some-repo/releases/12345678", + "assets": [ + { + "id": 22222222, + "name": "cluster-template.yaml" + } + ] + }`) + }) + + // redirect asset + mux.HandleFunc("/repos/some-owner/some-repo/releases/assets/22222222", func(w http.ResponseWriter, r *http.Request) { + // add the "/api-v3" prefix to match the prefix of the fake github server + w.Header().Add("Location", "/api-v3/redirected/22222222") + w.WriteHeader(http.StatusFound) + }) + + // redirect location + mux.HandleFunc("/redirected/22222222", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, template) + }) + path := filepath.Join(tmpDir, "cluster-template.yaml") g.Expect(os.WriteFile(path, []byte(template), 0600)).To(Succeed()) @@ -388,6 +434,36 @@ func Test_templateClient_GetFromURL(t *testing.T) { want: template, wantErr: false, }, + { + name: "Get asset from GitHub release", + args: args{ + templateURL: "https://github.com/some-owner/some-repo/releases/download/v1.0.0/cluster-template.yaml", + targetNamespace: "", + skipTemplateProcess: false, + }, + want: template, + wantErr: false, + }, + { + name: "Get asset from GitHub release + redirect", + args: args{ + templateURL: "https://github.com/some-owner/some-repo/releases/download/v2.0.0/cluster-template.yaml", + targetNamespace: "", + skipTemplateProcess: false, + }, + want: template, + wantErr: false, + }, + { + name: "Get asset from GitHub release with a wrong URL", + args: args{ + templateURL: "https://github.com/some-owner/some-repo/releases/wrong/v1.0.0/cluster-template.yaml", + targetNamespace: "", + skipTemplateProcess: false, + }, + want: "", + wantErr: true, + }, { name: "Get from stdin", args: args{