Skip to content

Commit

Permalink
refactor: consolidate version finding across version strategies
Browse files Browse the repository at this point in the history
Initially there was a single version strategy of 'github' which meant the
location was standardised.  Then came k8s as a version strategy which took
a different approach and so needed a different implementation.  Later came
Gitlab which required another implementation.

This change consolidates the various release finding approaches into a single
function, which varied based on attributes held about the release 'platform'.

As a demonstration of the benefit of the approach GitLab has been added as a
release platform and the GitLab CLI which is hosted on gitlab has been added
as a tool - superseding #1122

Signed-off-by: Richard Gee <richard@technologee.co.uk>
  • Loading branch information
rgee0 authored and alexellis committed Oct 25, 2024
1 parent b1f190b commit 152bacb
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 53 deletions.
120 changes: 67 additions & 53 deletions pkg/get/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,12 @@ import (
)

const GitHubVersionStrategy = "github"
const GitLabVersionStrategy = "gitlab"
const k8sVersionStrategy = "k8s"

var supportedOS = [...]string{"linux", "darwin", "ming"}
var supportedArchitectures = [...]string{"x86_64", "arm", "amd64", "armv6l", "armv7l", "arm64", "aarch64"}

// githubTimeout expanded from the original 5 seconds due to #693
var githubTimeout = time.Second * 10

// Tool describes how to download a CLI tool from a binary
// release - whether a single binary, or an archive.
type Tool struct {
Expand Down Expand Up @@ -66,6 +64,30 @@ type Tool struct {
NoExtension bool
}

type ReleaseLocation struct {
Url string
Timeout time.Duration
Method string
}

var releaseLocations = map[string]ReleaseLocation{
GitHubVersionStrategy: {
Url: "https://github.com/%s/%s/releases/latest",
Timeout: time.Second * 10,
Method: http.MethodHead,
},
GitLabVersionStrategy: {
Url: "https://gitlab.com/%s/%s/-/releases/permalink/latest",
Timeout: time.Second * 5,
Method: http.MethodHead,
},
k8sVersionStrategy: {
Url: "https://cdn.dl.k8s.io/release/stable.txt",
Timeout: time.Second * 5,
Method: http.MethodGet,
},
}

type ToolLocal struct {
Name string
Path string
Expand Down Expand Up @@ -110,12 +132,12 @@ func GetDownloadURL(tool *Tool, os, arch, version string, quiet bool) (string, e
}

func GetToolVersion(tool *Tool, version string) string {
ver := tool.Version

if len(version) > 0 {
ver = version
return version
}

return ver
return tool.Version
}

func (tool Tool) Head(uri string) (int, string, http.Header, error) {
Expand Down Expand Up @@ -148,19 +170,21 @@ func (tool Tool) GetURL(os, arch, version string, quiet bool) (string, error) {
log.Printf("Looking up version for %s", tool.Name)
}

var releaseType string
if len(tool.URLTemplate) == 0 ||
strings.Contains(tool.URLTemplate, "https://github.com/") ||
tool.VersionStrategy == GitHubVersionStrategy {
strings.Contains(tool.URLTemplate, "https://github.com/") {

releaseType = GitHubVersionStrategy

v, err := FindGitHubRelease(tool.Owner, tool.Repo)
if err != nil {
return "", err
}
version = v
}

if tool.VersionStrategy == k8sVersionStrategy {
v, err := FindK8sRelease()
if len(tool.VersionStrategy) > 0 {
releaseType = tool.VersionStrategy
}

if _, supported := releaseLocations[releaseType]; supported {

v, err := FindRelease(releaseType, tool.Owner, tool.Repo)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -209,14 +233,19 @@ func getURLByGithubTemplate(tool Tool, os, arch, version string) (string, error)
}

func FindGitHubRelease(owner, repo string) (string, error) {
url := fmt.Sprintf("https://github.com/%s/%s/releases/latest", owner, repo)
return FindRelease(GitHubVersionStrategy, owner, repo)
}

func FindRelease(location, owner, repo string) (string, error) {
url := formatUrl(releaseLocations[location].Url, owner, repo)

client := makeHTTPClient(&githubTimeout, false)
clientTimeout := releaseLocations[location].Timeout
client := makeHTTPClient(&clientTimeout, false)
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}

req, err := http.NewRequest(http.MethodHead, url, nil)
req, err := http.NewRequest(releaseLocations[location].Method, url, nil)
if err != nil {
return "", err
}
Expand All @@ -228,49 +257,27 @@ func FindGitHubRelease(owner, repo string) (string, error) {
return "", err
}

if res.Body != nil {
defer res.Body.Close()
}

if res.StatusCode != http.StatusMovedPermanently && res.StatusCode != http.StatusFound {
return "", fmt.Errorf("server returned status: %d", res.StatusCode)
}

loc := res.Header.Get("Location")
if len(loc) == 0 {
return "", fmt.Errorf("unable to determine release of tool")
}

version := loc[strings.LastIndex(loc, "/")+1:]
return version, nil
}
defer res.Body.Close()

func FindK8sRelease() (string, error) {
url := "https://cdn.dl.k8s.io/release/stable.txt"
if releaseLocations[location].Method == http.MethodHead {

timeout := time.Second * 5
client := makeHTTPClient(&timeout, false)
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}

req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return "", err
}
if res.StatusCode != http.StatusMovedPermanently && res.StatusCode != http.StatusFound {
return "", fmt.Errorf("server returned status: %d", res.StatusCode)
}

req.Header.Set("User-Agent", pkg.UserAgent())
loc := res.Header.Get("Location")
if len(loc) == 0 {
return "", fmt.Errorf("unable to determine release of tool")
}

res, err := client.Do(req)
if err != nil {
return "", err
version := loc[strings.LastIndex(loc, "/")+1:]
return version, nil
}

if res.Body == nil {
return "", fmt.Errorf("unable to determine release of tool")
}

defer res.Body.Close()
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
return "", err
Expand All @@ -280,6 +287,13 @@ func FindK8sRelease() (string, error) {
return version, nil
}

func formatUrl(url, owner, repo string) string {
if strings.Contains(url, "%s") {
return fmt.Sprintf(url, owner, repo)
}
return url
}

func getBinaryURL(owner, repo, version, downloadName string) string {
if in := strings.Index(downloadName, "/"); in > -1 {
return fmt.Sprintf(
Expand Down Expand Up @@ -481,7 +495,7 @@ func ValidateOS(name string) error {
}
}

return fmt.Errorf("operating system %q is not supported. Available prefixes: %s.",
return fmt.Errorf("operating system %q is not supported. Available prefixes: %s",
name, strings.Join(supportedOS[:], ", "))
}

Expand All @@ -492,6 +506,6 @@ func ValidateArch(name string) error {
return nil
}
}
return fmt.Errorf("cpu architecture %q is not supported. Available: %s.",
return fmt.Errorf("cpu architecture %q is not supported. Available: %s",
name, strings.Join(supportedArchitectures[:], ", "))
}
84 changes: 84 additions & 0 deletions pkg/get/get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,40 @@ func TestGetToolVersion(t *testing.T) {
}
}

func Test_FormatUrl(t *testing.T) {
tests := []struct {
name string
url string
owner string
repo string
expected string
}{
{
name: "URL with placeholders",
url: "https://github.com/%s/%s",
owner: "ownerName",
repo: "repoName",
expected: "https://github.com/ownerName/repoName",
},
{
name: "URL without placeholders",
url: "https://github.com/example",
owner: "ownerName",
repo: "repoName",
expected: "https://github.com/example",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := formatUrl(tc.url, tc.owner, tc.repo)
if result != tc.expected {
t.Fatalf("\nwant: %s\ngot: %s", tc.expected, result)
}
})
}
}

func Test_MakeSureNoDuplicates(t *testing.T) {
count := map[string]int{}
tools := MakeTools()
Expand Down Expand Up @@ -7821,3 +7855,53 @@ func Test_Download_labctl(t *testing.T) {
}
}
}

func Test_Download_glab(t *testing.T) {
tools := MakeTools()
name := "glab"
const toolVersion = "v1.48.0"

tool := getTool(name, tools)

tests := []test{
{
os: "darwin",
arch: arch64bit,
version: toolVersion,
url: "https://gitlab.com/gitlab-org/cli/-/releases/v1.48.0/downloads/glab_1.48.0_darwin_amd64.tar.gz",
},
{
os: "darwin",
arch: archDarwinARM64,
version: toolVersion,
url: "https://gitlab.com/gitlab-org/cli/-/releases/v1.48.0/downloads/glab_1.48.0_darwin_arm64.tar.gz",
},
{
os: "linux",
arch: arch64bit,
version: toolVersion,
url: "https://gitlab.com/gitlab-org/cli/-/releases/v1.48.0/downloads/glab_1.48.0_linux_amd64.tar.gz",
},
{
os: "linux",
arch: archARM64,
version: toolVersion,
url: "https://gitlab.com/gitlab-org/cli/-/releases/v1.48.0/downloads/glab_1.48.0_linux_arm64.tar.gz",
},
{
os: "mingw64_nt-10.0-18362",
arch: arch64bit,
version: toolVersion,
url: "https://gitlab.com/gitlab-org/cli/-/releases/v1.48.0/downloads/glab_1.48.0_windows_amd64.zip",
},
}
for _, tc := range tests {
got, err := tool.GetURL(tc.os, tc.arch, tc.version, false)
if err != nil {
t.Fatal(err)
}
if got != tc.url {
t.Fatalf("\nwant: %s\ngot: %s", tc.url, got)
}
}
}
28 changes: 28 additions & 0 deletions pkg/get/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -4269,5 +4269,33 @@ https://github.com/{{.Owner}}/{{.Repo}}/releases/download/{{.Version}}/{{.Name}}
labctl_{{$os}}_{{$arch}}.{{$ext}}
`,
})

tools = append(tools,
Tool{
Owner: "gitlab-org",
Repo: "cli",
Name: "glab",
Description: "A GitLab CLI tool bringing GitLab to your command line.",
VersionStrategy: GitLabVersionStrategy,
URLTemplate: `
{{ $osStr := .OS }}
{{ $arch := .Arch }}
{{ $extStr := "tar.gz" }}
{{- if eq .Arch "x86_64" -}}
{{$arch = "amd64"}}
{{- else if eq .Arch "armv6l" -}}
{{$arch = "armv6"}}
{{- else if (or (eq .Arch "aarch64") (eq .Arch "arm64")) -}}
{{$arch = "arm64"}}
{{- end -}}
{{- if HasPrefix .OS "ming" -}}
{{$osStr = "windows"}}
{{$extStr = "zip"}}
{{- end -}}
https://gitlab.com/{{.Owner}}/{{.Repo}}/-/releases/{{.Version}}/downloads/{{.Name}}_{{.VersionNumber}}_{{$osStr}}_{{$arch}}.{{$extStr}}`,
})
return tools
}

0 comments on commit 152bacb

Please sign in to comment.