graphql-engine/cli/plugins/util.go

203 lines
5.8 KiB
Go

package plugins
/*
some of the code here is borrowed from the krew codebse (kubernetes)
and the copyright belongs to the respective authors.
source: https://github.com/kubernetes-sigs/krew/tree/master/internal
*/
import (
"fmt"
"net/url"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"github.com/hasura/graphql-engine/cli/plugins/download"
"github.com/pkg/errors"
)
const (
sha256Pattern = `^[a-f0-9]{64}$`
)
var (
safePluginRegexp = regexp.MustCompile(`^[\w-]+$`)
validSHA256 = regexp.MustCompile(sha256Pattern)
// windowsForbidden is taken from https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file
windowsForbidden = []string{"CON", "PRN", "AUX", "NUL", "COM1", "COM2",
"COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2",
"LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"}
)
func isValidSHA256(s string) bool { return validSHA256.MatchString(s) }
// ensureDirs makes sure the paths created
func ensureDirs(paths ...string) error {
for _, p := range paths {
if err := os.MkdirAll(p, 0755); err != nil {
return errors.Wrapf(err, "failed to ensure create directory %q", p)
}
}
return nil
}
// IsSafePluginName checks if the plugin Name is safe to use.
func IsSafePluginName(name string) bool {
if !safePluginRegexp.MatchString(name) {
return false
}
for _, forbidden := range windowsForbidden {
if strings.EqualFold(forbidden, name) {
return false
}
}
return true
}
// validatePlatform checks Platform for structural validity.
func validatePlatform(p Platform) error {
if p.URI == "" {
return errors.New("`uri` has to be set")
}
if p.Sha256 == "" {
return errors.New("`sha256` sum has to be set")
}
if !isValidSHA256(p.Sha256) {
return errors.Errorf("`sha256` value %s is not valid, must match pattern %s", p.Sha256, sha256Pattern)
}
if p.Bin == "" {
return errors.New("`bin` has to be set")
}
if err := validateFiles(p.Files); err != nil {
return errors.Wrap(err, "`files` is invalid")
}
return nil
}
func validateFiles(fops []FileOperation) error {
if fops == nil {
return nil
}
if len(fops) == 0 {
return errors.New("`files` has to be unspecified or non-empty")
}
for _, op := range fops {
if op.From == "" {
return errors.New("`from` field has to be set")
} else if op.To == "" {
return errors.New("`to` field has to be set")
}
}
return nil
}
// matchPlatform returns the first matching platform to given os/arch.
func MatchPlatform(platforms []Platform) (Platform, bool, error) {
currSelector := fmt.Sprintf("%s-%s", runtime.GOOS, runtime.GOARCH)
for _, platform := range platforms {
if platform.Selector == currSelector {
return platform, true, nil
}
}
return Platform{}, false, nil
}
// downloadAndExtract downloads the specified archive uri (or uses the provided overrideFile, if a non-empty value)
// while validating its checksum with the provided sha256sum, and extracts its contents to extractDir that must be.
// created.
func downloadAndExtract(extractDir, uri, sha256sum string) error {
nurl, err := url.Parse(uri)
if err != nil {
return errors.Wrap(err, "unable to parse uri")
}
var fetcher download.Fetcher
if nurl.Scheme == "file" {
fetcher = download.NewFileFetcher(nurl.Path)
} else {
fetcher = download.HTTPFetcher{}
}
verifier := download.NewSha256Verifier(sha256sum)
err = download.NewDownloader(verifier, fetcher).Get(uri, extractDir)
return errors.Wrap(err, "failed to unpack the plugin archive")
}
// IsWindows sees runtime.GOOS to find out if current execution mode is win32.
func IsWindows() bool {
goos := runtime.GOOS
return goos == "windows"
}
func createOrUpdateLink(binDir, binary, plugin string) error {
dst := filepath.Join(binDir, PluginNameToBin(plugin, IsWindows()))
if err := removeLink(dst); err != nil {
return errors.Wrap(err, "failed to remove old symlink")
}
if _, err := os.Stat(binary); os.IsNotExist(err) {
return errors.Wrapf(err, "can't create symbolic link, source binary (%q) cannot be found in extracted archive", binary)
}
// Create new
if err := os.Symlink(binary, dst); err != nil {
return errors.Wrapf(err, "failed to create a symlink from %q to %q", binary, dst)
}
return nil
}
// removeLink removes a symlink reference if exists.
func removeLink(path string) error {
fi, err := os.Lstat(path)
if os.IsNotExist(err) {
return nil
} else if err != nil {
return errors.Wrapf(err, "failed to read the symlink in %q", path)
}
if fi.Mode()&os.ModeSymlink == 0 {
return errors.Errorf("file %q is not a symlink (mode=%s)", path, fi.Mode())
}
if err := os.Remove(path); err != nil {
return errors.Wrapf(err, "failed to remove the symlink in %q", path)
}
return nil
}
// PluginNameToBin creates the name of the symlink file for the plugin name.
// It converts dashes to underscores.
func PluginNameToBin(name string, isWindows bool) string {
name = strings.ReplaceAll(name, "-", "_")
name = "hasura-" + name
if isWindows {
name += ".exe"
}
return name
}
// IsSubPath checks if the extending path is an extension of the basePath, it will return the extending path
// elements. Both paths have to be absolute or have the same root directory. The remaining path elements
func IsSubPath(basePath, subPath string) (string, bool) {
extendingPath, err := filepath.Rel(basePath, subPath)
if err != nil {
return "", false
}
if strings.HasPrefix(extendingPath, "..") {
return "", false
}
return extendingPath, true
}
// ReplaceBase will return a replacement path with replacement as a base of the path instead of the old base. a/b/c, a, d -> d/b/c
func ReplaceBase(path, old, replacement string) (string, error) {
extendingPath, ok := IsSubPath(old, path)
if !ok {
return "", errors.Errorf("can't replace %q in %q, it is not a subpath", old, path)
}
return filepath.Join(replacement, extendingPath), nil
}