mirror of
https://github.com/alexellis/arkade.git
synced 2022-05-07 18:22:49 +03:00
Fixes: #557 Closes: #628 - thanks to @yankeexe for his suggestions Runs every day at midnight, or upon new commits / PRs. Signed-off-by: Alex Ellis (OpenFaaS Ltd) <alexellis2@gmail.com>
421 lines
10 KiB
Go
421 lines
10 KiB
Go
package get
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
"github.com/alexellis/arkade/pkg/env"
|
|
)
|
|
|
|
var supportedOS = [...]string{"linux", "darwin", "ming"}
|
|
var supportedArchitectures = [...]string{"x86_64", "arm", "amd64", "armv6l", "armv7l", "arm64", "aarch64"}
|
|
|
|
// Tool describes how to download a CLI tool from a binary
|
|
// release - whether a single binary, or an archive.
|
|
type Tool struct {
|
|
// The name of the tool for download
|
|
Name string
|
|
|
|
// Repo is a GitHub repo, when no repo exists, use the same
|
|
// as the name.
|
|
Repo string
|
|
|
|
// Owner is the name of the GitHub account, when no account
|
|
// exists, use the vendor name lowercase.
|
|
Owner string
|
|
|
|
// Version pinned or left empty to pull the latest release
|
|
// if any only if only BinaryTemplate is specified.
|
|
Version string
|
|
|
|
// Description of what the tool is used for.
|
|
Description string
|
|
|
|
// URLTemplate specifies a Go template for the download URL
|
|
// override the OS, architecture and extension
|
|
// All whitespace will be trimmed/
|
|
URLTemplate string
|
|
|
|
// The binary template can be used when downloading GitHub
|
|
// It assumes that the only part of the URL needing to be
|
|
// templated is the binary name on a standard GitHub download
|
|
// URL.
|
|
BinaryTemplate string
|
|
|
|
// NoExtension is required for tooling such as kubectx
|
|
// which at time of writing is a bash script.
|
|
NoExtension bool
|
|
}
|
|
|
|
type ToolLocal struct {
|
|
Name string
|
|
Path string
|
|
}
|
|
|
|
var templateFuncs = map[string]interface{}{
|
|
"HasPrefix": func(s, prefix string) bool { return strings.HasPrefix(s, prefix) },
|
|
}
|
|
|
|
func (tool Tool) IsArchive() (bool, error) {
|
|
arch, operatingSystem := env.GetClientArch()
|
|
version := ""
|
|
|
|
downloadURL, err := GetDownloadURL(&tool, strings.ToLower(operatingSystem), strings.ToLower(arch), version)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return strings.HasSuffix(downloadURL, "tar.gz") ||
|
|
strings.HasSuffix(downloadURL, "zip") ||
|
|
strings.HasSuffix(downloadURL, "tgz"), nil
|
|
}
|
|
|
|
// GetDownloadURL fetches the download URL for a release of a tool
|
|
// for a given os, architecture and version
|
|
func GetDownloadURL(tool *Tool, os, arch, version string) (string, error) {
|
|
ver := getToolVersion(tool, version)
|
|
|
|
dlURL, err := tool.GetURL(os, arch, ver)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return dlURL, nil
|
|
}
|
|
|
|
func getToolVersion(tool *Tool, version string) string {
|
|
ver := tool.Version
|
|
if len(version) > 0 {
|
|
ver = version
|
|
}
|
|
|
|
return ver
|
|
}
|
|
|
|
func (tool Tool) Head(uri string) (int, string, http.Header, error) {
|
|
req, err := http.NewRequest(http.MethodHead, uri, nil)
|
|
if err != nil {
|
|
return http.StatusBadRequest, "", nil, err
|
|
}
|
|
|
|
res, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return http.StatusBadRequest, "", nil, err
|
|
}
|
|
|
|
var body string
|
|
if res.Body != nil {
|
|
b, _ := ioutil.ReadAll(res.Body)
|
|
body = string(b)
|
|
}
|
|
|
|
return res.StatusCode, body, res.Header, nil
|
|
}
|
|
|
|
func (tool Tool) GetURL(os, arch, version string) (string, error) {
|
|
|
|
if len(version) == 0 &&
|
|
(len(tool.URLTemplate) == 0 || strings.Contains(tool.URLTemplate, "https://github.com/")) {
|
|
log.Printf("Looking up version for %s", tool.Name)
|
|
v, err := findGitHubRelease(tool.Owner, tool.Repo)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
log.Printf("Found: %s", v)
|
|
version = v
|
|
}
|
|
|
|
if len(tool.URLTemplate) > 0 {
|
|
return getByDownloadTemplate(tool, os, arch, version)
|
|
}
|
|
|
|
return getURLByGithubTemplate(tool, os, arch, version)
|
|
}
|
|
|
|
func getURLByGithubTemplate(tool Tool, os, arch, version string) (string, error) {
|
|
|
|
var err error
|
|
t := template.New(tool.Name + "binary")
|
|
t = t.Funcs(templateFuncs)
|
|
t, err = t.Parse(tool.BinaryTemplate)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
pref := map[string]string{
|
|
"OS": os,
|
|
"Arch": arch,
|
|
"Name": tool.Name,
|
|
"Version": version,
|
|
"VersionNumber": strings.TrimPrefix(version, "v"),
|
|
}
|
|
|
|
err = t.Execute(&buf, pref)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
downloadName := strings.TrimSpace(buf.String())
|
|
|
|
return getBinaryURL(tool.Owner, tool.Repo, version, downloadName), nil
|
|
}
|
|
|
|
func findGitHubRelease(owner, repo string) (string, error) {
|
|
url := fmt.Sprintf("https://github.com/%s/%s/releases/latest", owner, repo)
|
|
|
|
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.MethodHead, url, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if res.Body != nil {
|
|
defer res.Body.Close()
|
|
}
|
|
|
|
if res.StatusCode != 302 {
|
|
return "", fmt.Errorf("incorrect status code: %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
|
|
}
|
|
|
|
func getBinaryURL(owner, repo, version, downloadName string) string {
|
|
if in := strings.Index(downloadName, "/"); in > -1 {
|
|
return fmt.Sprintf(
|
|
"https://github.com/%s/%s/releases/download/%s",
|
|
owner, repo, downloadName)
|
|
}
|
|
return fmt.Sprintf(
|
|
"https://github.com/%s/%s/releases/download/%s/%s",
|
|
owner, repo, version, downloadName)
|
|
}
|
|
|
|
func getByDownloadTemplate(tool Tool, os, arch, version string) (string, error) {
|
|
var err error
|
|
t := template.New(tool.Name)
|
|
t = t.Funcs(templateFuncs)
|
|
t, err = t.Parse(tool.URLTemplate)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
inputs := map[string]string{
|
|
"OS": os,
|
|
"Arch": arch,
|
|
"Version": version,
|
|
"VersionNumber": strings.TrimPrefix(version, "v"),
|
|
"Repo": tool.Repo,
|
|
"Owner": tool.Owner,
|
|
"Name": tool.Name,
|
|
}
|
|
|
|
if err := t.Execute(&buf, inputs); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
res := strings.TrimSpace(buf.String())
|
|
return res, nil
|
|
}
|
|
|
|
// makeHTTPClient makes a HTTP client with good defaults for timeouts.
|
|
func makeHTTPClient(timeout *time.Duration, tlsInsecure bool) http.Client {
|
|
return makeHTTPClientWithDisableKeepAlives(timeout, tlsInsecure, false)
|
|
}
|
|
|
|
// makeHTTPClientWithDisableKeepAlives makes a HTTP client with good defaults for timeouts.
|
|
func makeHTTPClientWithDisableKeepAlives(timeout *time.Duration, tlsInsecure bool, disableKeepAlives bool) http.Client {
|
|
client := http.Client{}
|
|
|
|
if timeout != nil || tlsInsecure {
|
|
tr := &http.Transport{
|
|
Proxy: http.ProxyFromEnvironment,
|
|
DisableKeepAlives: disableKeepAlives,
|
|
}
|
|
|
|
if timeout != nil {
|
|
client.Timeout = *timeout
|
|
tr.DialContext = (&net.Dialer{
|
|
Timeout: *timeout,
|
|
}).DialContext
|
|
|
|
tr.IdleConnTimeout = 120 * time.Millisecond
|
|
tr.ExpectContinueTimeout = 1500 * time.Millisecond
|
|
}
|
|
|
|
if tlsInsecure {
|
|
tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: tlsInsecure}
|
|
}
|
|
|
|
tr.DisableKeepAlives = disableKeepAlives
|
|
|
|
client.Transport = tr
|
|
}
|
|
|
|
return client
|
|
}
|
|
|
|
// GetBinaryName returns the name of a binary for the given tool or an
|
|
// error if the tool's template cannot be parsed or executed.
|
|
func GetBinaryName(tool *Tool, os, arch, version string) (string, error) {
|
|
if len(tool.BinaryTemplate) > 0 {
|
|
var err error
|
|
t := template.New(tool.Name + "_binaryname")
|
|
t = t.Funcs(templateFuncs)
|
|
|
|
t, err = t.Parse(tool.BinaryTemplate)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
ver := getToolVersion(tool, version)
|
|
if err := t.Execute(&buf, map[string]string{
|
|
"OS": os,
|
|
"Arch": arch,
|
|
"Name": tool.Name,
|
|
"Version": ver,
|
|
"VersionNumber": strings.TrimPrefix(ver, "v"),
|
|
}); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
res := strings.TrimSpace(buf.String())
|
|
return res, nil
|
|
}
|
|
|
|
return "", errors.New("BinaryTemplate is not set")
|
|
}
|
|
|
|
// GetDownloadURLs generates a list of URL for each tool, for download
|
|
func GetDownloadURLs(tools Tools, toolArgs []string, version string) (Tools, error) {
|
|
arkadeTools := []Tool{}
|
|
|
|
for _, arg := range toolArgs {
|
|
name := arg
|
|
|
|
// Handle version specified tool name
|
|
if i := strings.LastIndex(arg, "@"); i > -1 {
|
|
name = arg[:i]
|
|
if len(version) > 0 {
|
|
return nil, fmt.Errorf("cannot specify --version flag and @ syntax at the same time for %s", name)
|
|
}
|
|
version = arg[i+1:]
|
|
}
|
|
|
|
err := toolExists(&arkadeTools, tools, name, version)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// unset value for the next iteration of versioned tool
|
|
version = ""
|
|
}
|
|
|
|
return arkadeTools, nil
|
|
}
|
|
|
|
// toolExists checks if user provided tool exists on arkade
|
|
func toolExists(arkadeTools *[]Tool, tools Tools, name, version string) error {
|
|
for _, tool := range tools {
|
|
if name == tool.Name {
|
|
if len(version) > 0 {
|
|
tool.Version = version
|
|
}
|
|
*arkadeTools = append(*arkadeTools, tool)
|
|
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("tool %s not found", name)
|
|
}
|
|
|
|
// PostInstallationMsg generates installation message after tool has been downloaded
|
|
func PostInstallationMsg(dlMode int, localToolsStore []ToolLocal) ([]byte, error) {
|
|
|
|
t := template.New("Installation Instructions")
|
|
|
|
if dlMode == DownloadTempDir {
|
|
t.Parse(`Run the following to copy to install the tool:
|
|
|
|
chmod +x {{range .}}{{.Path}} {{end}}
|
|
{{- range . }}
|
|
sudo install -m 755 {{.Path}} /usr/local/bin/{{.Name}}
|
|
{{- end}}`)
|
|
|
|
} else {
|
|
t.Parse(`# Add arkade binary directory to your PATH variable
|
|
export PATH=$PATH:$HOME/.arkade/bin/
|
|
|
|
# Test the binary:
|
|
{{- range . }}
|
|
{{.Path}}
|
|
{{- end }}
|
|
|
|
# Or install with:
|
|
{{- range . }}
|
|
sudo mv {{.Path}} /usr/local/bin/
|
|
|
|
{{- end}}`)
|
|
}
|
|
|
|
var tpl bytes.Buffer
|
|
|
|
err := t.Execute(&tpl, localToolsStore)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return tpl.Bytes(), err
|
|
}
|
|
|
|
// ValidateOS returns whether a given operating system is supported
|
|
func ValidateOS(name string) error {
|
|
for _, os := range supportedOS {
|
|
if strings.HasPrefix(strings.ToLower(name), os) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("operating system %q is not supported. Available prefixes: %s.",
|
|
name, strings.Join(supportedOS[:], ", "))
|
|
}
|
|
|
|
// ValidateArch returns whether a given cpu architecture is supported
|
|
func ValidateArch(name string) error {
|
|
for _, arch := range supportedArchitectures {
|
|
if arch == strings.ToLower(name) {
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("cpu architecture %q is not supported. Available: %s.",
|
|
name, strings.Join(supportedArchitectures[:], ", "))
|
|
}
|