Skip to content

Commit 686961a

Browse files
authored
Merge pull request #7453 from nunnatsa/fix-from-url
🐛 Allow using the --from flag to get a template from a github release
2 parents ae61579 + 39e99d3 commit 686961a

File tree

2 files changed

+139
-11
lines changed

2 files changed

+139
-11
lines changed

cmd/clusterctl/client/cluster/template.go

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package cluster
1919
import (
2020
"context"
2121
"encoding/base64"
22+
"fmt"
2223
"io"
2324
"net/http"
2425
"net/url"
@@ -180,37 +181,58 @@ func (t *templateClient) getGitHubFileContent(rURL *url.URL) ([]byte, error) {
180181
urlSplit := strings.Split(strings.TrimPrefix(rURL.Path, "/"), "/")
181182
if len(urlSplit) < 5 {
182183
return nil, errors.Errorf(
183-
"invalid GitHub url %q: a GitHub url should be in the form https://github.com/{owner}/{repository}/blob/{branch}/{path-to-file}", rURL,
184+
"invalid GitHub url %q: a GitHub url should be in on of these the forms\n"+
185+
"- https://github.com/{owner}/{repository}/blob/{branch}/{path-to-file}\n"+
186+
"- https://github.com/{owner}/{repository}/releases/download/{tag}/{asset-file-name}", rURL,
184187
)
185188
}
186189

187190
// Extract all the info from url split.
188191
owner := urlSplit[0]
189-
repository := urlSplit[1]
190-
branch := urlSplit[3]
191-
path := strings.Join(urlSplit[4:], "/")
192+
repo := urlSplit[1]
193+
linkType := urlSplit[2]
192194

193195
// gets the GitHub client
194-
client, err := t.gitHubClientFactory(t.configClient.Variables())
196+
ghClient, err := t.gitHubClientFactory(t.configClient.Variables())
195197
if err != nil {
196198
return nil, err
197199
}
198200

199201
// gets the file from GiHub
200-
fileContent, _, _, err := client.Repositories.GetContents(context.TODO(), owner, repository, path, &github.RepositoryContentGetOptions{Ref: branch})
202+
switch linkType {
203+
case "blob": // get file from a code in a github repo
204+
branch := urlSplit[3]
205+
path := strings.Join(urlSplit[4:], "/")
206+
207+
return getGithubFileContentFromCode(ghClient, rURL.Path, owner, repo, path, branch)
208+
209+
case "releases": // get a github release asset
210+
if urlSplit[3] != "download" {
211+
break
212+
}
213+
tag := urlSplit[4]
214+
assetName := urlSplit[5]
215+
216+
return getGithubAssetFromRelease(ghClient, rURL.Path, owner, repo, tag, assetName)
217+
}
218+
219+
return nil, fmt.Errorf("unknown github URL: %v", rURL)
220+
}
221+
222+
func getGithubFileContentFromCode(ghClient *github.Client, fullPath string, owner string, repo string, path string, branch string) ([]byte, error) {
223+
fileContent, _, _, err := ghClient.Repositories.GetContents(ctx, owner, repo, path, &github.RepositoryContentGetOptions{Ref: branch})
201224
if err != nil {
202-
return nil, handleGithubErr(err, "failed to get %q", rURL.Path)
225+
return nil, handleGithubErr(err, "failed to get %q", fullPath)
203226
}
204227
if fileContent == nil {
205-
return nil, errors.Errorf("%q does not return a valid file content", rURL.Path)
228+
return nil, errors.Errorf("%q does not return a valid file content", fullPath)
206229
}
207230
if fileContent.Encoding == nil || *fileContent.Encoding != "base64" {
208-
return nil, errors.Errorf("invalid encoding detected for %q. Only base64 encoding supported", rURL.Path)
231+
return nil, errors.Errorf("invalid encoding detected for %q. Only base64 encoding supported", fullPath)
209232
}
210-
211233
content, err := base64.StdEncoding.DecodeString(*fileContent.Content)
212234
if err != nil {
213-
return nil, errors.Wrapf(err, "failed to decode file %q", rURL.Path)
235+
return nil, errors.Wrapf(err, "failed to decode file %q", fullPath)
214236
}
215237
return content, nil
216238
}
@@ -239,6 +261,36 @@ func (t *templateClient) getRawURLFileContent(rURL string) ([]byte, error) {
239261
return content, nil
240262
}
241263

264+
func getGithubAssetFromRelease(ghClient *github.Client, path string, owner string, repo string, tag string, assetName string) ([]byte, error) {
265+
release, _, err := ghClient.Repositories.GetReleaseByTag(ctx, owner, repo, tag)
266+
if err != nil {
267+
return nil, handleGithubErr(err, "failed to get release '%s' from %s/%s repository", tag, owner, repo)
268+
}
269+
270+
if release == nil {
271+
return nil, fmt.Errorf("can't find release '%s' in %s/%s repository", tag, owner, repo)
272+
}
273+
274+
var rc io.ReadCloser
275+
for _, asset := range release.Assets {
276+
if asset.GetName() == assetName {
277+
rc, _, err = ghClient.Repositories.DownloadReleaseAsset(ctx, owner, repo, asset.GetID(), ghClient.Client())
278+
if err != nil {
279+
return nil, errors.Wrapf(err, "failed to download file %q", path)
280+
}
281+
break
282+
}
283+
}
284+
285+
if rc == nil {
286+
return nil, fmt.Errorf("failed to download the file %q", path)
287+
}
288+
289+
defer func() { _ = rc.Close() }()
290+
291+
return io.ReadAll(rc)
292+
}
293+
242294
func getGitHubClient(configVariablesClient config.VariablesClient) (*github.Client, error) {
243295
var authenticatingHTTPClient *http.Client
244296
if token, err := configVariablesClient.Get(config.GitHubTokenVariable); err == nil {

cmd/clusterctl/client/cluster/template_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,52 @@ func Test_templateClient_GetFromURL(t *testing.T) {
348348
}`)
349349
})
350350

351+
mux.HandleFunc("/repos/some-owner/some-repo/releases/tags/v1.0.0", func(w http.ResponseWriter, r *http.Request) {
352+
fmt.Fprint(w, `{
353+
"tag_name": "v1.0.0",
354+
"name": "v1.0.0",
355+
"id": 12345678,
356+
"url": "https://api.github.com/repos/some-owner/some-repo/releases/12345678",
357+
"assets": [
358+
{
359+
"id": 87654321,
360+
"name": "cluster-template.yaml"
361+
}
362+
]
363+
}`)
364+
})
365+
366+
mux.HandleFunc("/repos/some-owner/some-repo/releases/assets/87654321", func(w http.ResponseWriter, r *http.Request) {
367+
fmt.Fprint(w, template)
368+
})
369+
370+
mux.HandleFunc("/repos/some-owner/some-repo/releases/tags/v2.0.0", func(w http.ResponseWriter, r *http.Request) {
371+
fmt.Fprint(w, `{
372+
"tag_name": "v2.0.0",
373+
"name": "v2.0.0",
374+
"id": 12345678,
375+
"url": "https://api.github.com/repos/some-owner/some-repo/releases/12345678",
376+
"assets": [
377+
{
378+
"id": 22222222,
379+
"name": "cluster-template.yaml"
380+
}
381+
]
382+
}`)
383+
})
384+
385+
// redirect asset
386+
mux.HandleFunc("/repos/some-owner/some-repo/releases/assets/22222222", func(w http.ResponseWriter, r *http.Request) {
387+
// add the "/api-v3" prefix to match the prefix of the fake github server
388+
w.Header().Add("Location", "/api-v3/redirected/22222222")
389+
w.WriteHeader(http.StatusFound)
390+
})
391+
392+
// redirect location
393+
mux.HandleFunc("/redirected/22222222", func(w http.ResponseWriter, r *http.Request) {
394+
fmt.Fprint(w, template)
395+
})
396+
351397
path := filepath.Join(tmpDir, "cluster-template.yaml")
352398
g.Expect(os.WriteFile(path, []byte(template), 0600)).To(Succeed())
353399

@@ -388,6 +434,36 @@ func Test_templateClient_GetFromURL(t *testing.T) {
388434
want: template,
389435
wantErr: false,
390436
},
437+
{
438+
name: "Get asset from GitHub release",
439+
args: args{
440+
templateURL: "https://github.com/some-owner/some-repo/releases/download/v1.0.0/cluster-template.yaml",
441+
targetNamespace: "",
442+
skipTemplateProcess: false,
443+
},
444+
want: template,
445+
wantErr: false,
446+
},
447+
{
448+
name: "Get asset from GitHub release + redirect",
449+
args: args{
450+
templateURL: "https://github.com/some-owner/some-repo/releases/download/v2.0.0/cluster-template.yaml",
451+
targetNamespace: "",
452+
skipTemplateProcess: false,
453+
},
454+
want: template,
455+
wantErr: false,
456+
},
457+
{
458+
name: "Get asset from GitHub release with a wrong URL",
459+
args: args{
460+
templateURL: "https://github.com/some-owner/some-repo/releases/wrong/v1.0.0/cluster-template.yaml",
461+
targetNamespace: "",
462+
skipTemplateProcess: false,
463+
},
464+
want: "",
465+
wantErr: true,
466+
},
391467
{
392468
name: "Get from stdin",
393469
args: args{

0 commit comments

Comments
 (0)