feat(dependencies): Implement base of the dependency system
This commit is contained in:
3
helpers/constant.go
Normal file
3
helpers/constant.go
Normal file
@ -0,0 +1,3 @@
|
||||
package helpers
|
||||
|
||||
const configFile = ".strap.yaml"
|
214
helpers/dependencies/golang.go
Normal file
214
helpers/dependencies/golang.go
Normal file
@ -0,0 +1,214 @@
|
||||
package dependencies
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/blang/semver"
|
||||
"github.com/juju/errors"
|
||||
|
||||
"git.home.m-and-m.ovh/mderasse/boot/helpers"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// minimum minor required for the app to work
|
||||
const minimumGolangVersion = "1.18.4"
|
||||
|
||||
// installation directory for fresh install.
|
||||
// will be prefixed by $HOME
|
||||
const defaultGolangInstallDir = "/local/go"
|
||||
|
||||
var regexGolangVersion = regexp.MustCompile(`^go version go(\d+\.\d+\.\d+)`)
|
||||
|
||||
type Golang struct{}
|
||||
|
||||
// regroup all golang dependencies function
|
||||
|
||||
// GetName
|
||||
func (g Golang) GetName() string {
|
||||
return "Golang"
|
||||
}
|
||||
|
||||
// GetMinimumVersion
|
||||
func (g Golang) GetMinimumVersion() string {
|
||||
return minimumGolangVersion
|
||||
}
|
||||
|
||||
// IsInstalled
|
||||
func (g Golang) IsInstalled() (bool, error) {
|
||||
|
||||
_, err := g.GetBinaryPath()
|
||||
if err != nil && !errors.Is(err, exec.ErrNotFound) {
|
||||
return false, errors.Trace(err)
|
||||
} else if err != nil && errors.Is(err, exec.ErrNotFound) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// GetBinaryPath
|
||||
func (g Golang) GetBinaryPath() (string, error) {
|
||||
log.Debug("looking for golang binary")
|
||||
|
||||
path, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
return "", errors.Trace(err)
|
||||
}
|
||||
|
||||
log.Debug("found golang binary in", path)
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// GetVersion return the major, minor and patch version of Golang
|
||||
func (g Golang) GetVersion() (string, error) {
|
||||
|
||||
isInstalled, err := g.IsInstalled()
|
||||
if err != nil {
|
||||
return "", errors.Trace(err)
|
||||
}
|
||||
|
||||
if !isInstalled {
|
||||
return "", errors.NotFoundf("golang is not installed on the system")
|
||||
}
|
||||
|
||||
golangPath, err := g.GetBinaryPath()
|
||||
if err != nil {
|
||||
return "", errors.Trace(err)
|
||||
}
|
||||
|
||||
log.Debug("executing go version command")
|
||||
|
||||
cmd := exec.Command(golangPath, "version")
|
||||
stdout, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", errors.Trace(err)
|
||||
}
|
||||
cleanOut := strings.TrimSpace(string(stdout))
|
||||
|
||||
log.Debugf("go version returned %s", cleanOut)
|
||||
|
||||
parseOutput := regexGolangVersion.FindStringSubmatch(cleanOut)
|
||||
if len(parseOutput) != 2 {
|
||||
return "", errors.NotSupportedf("failed to parse golang version output: %s", cleanOut)
|
||||
}
|
||||
|
||||
return parseOutput[1], nil
|
||||
}
|
||||
|
||||
// IsVersionSupported
|
||||
func (g Golang) IsVersionSupported() (bool, error) {
|
||||
|
||||
version, err := g.GetVersion()
|
||||
if err != nil {
|
||||
return false, errors.Trace(err)
|
||||
}
|
||||
|
||||
installedVersion, err := semver.Make(version)
|
||||
if err != nil {
|
||||
return false, errors.Trace(err)
|
||||
}
|
||||
|
||||
requiredVersion, _ := semver.Make(minimumGolangVersion)
|
||||
if err != nil {
|
||||
return false, errors.Trace(err)
|
||||
}
|
||||
|
||||
if installedVersion.LT(*&requiredVersion) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// DescribeInstall
|
||||
func (g Golang) DescribeInstall(path string) string {
|
||||
|
||||
description := fmt.Sprintf("rm -rf %s/* ", path)
|
||||
description = fmt.Sprintf("%s\ncurl %s | tar --strip-components=1 -C %s -zxf -", description, getDownloadUrl(), path)
|
||||
return description
|
||||
}
|
||||
|
||||
// Install
|
||||
func (g Golang) Install(path string) error {
|
||||
|
||||
err := helpers.RemoveDirectoryContent(path)
|
||||
if err != nil {
|
||||
log.Warnf("fail to delete content of directory %s", path)
|
||||
}
|
||||
|
||||
downloadUrl := getDownloadUrl()
|
||||
content, err := downloadFile(downloadUrl)
|
||||
if err != nil {
|
||||
log.Warnf("fail to download file from %s", downloadUrl)
|
||||
return errors.Trace(err)
|
||||
}
|
||||
|
||||
gzipReader, err := unGzip(content)
|
||||
if err != nil {
|
||||
log.Warnf("fail to un-gzip downloaded file from %s, error:", downloadUrl)
|
||||
return errors.Trace(err)
|
||||
}
|
||||
|
||||
// XXX: unTar should take a subdir
|
||||
err = unTar(gzipReader, "go/", path)
|
||||
if err != nil {
|
||||
log.Warnf("fail to un-tar downloaded file from %s", downloadUrl)
|
||||
return errors.Trace(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetInstallDirectory will try to find the current golang directory. If it doesn't exist or if it's in a
|
||||
// not userspace directory, it will provide the "default"
|
||||
// It doesn't mean that the directory is "writable"
|
||||
func (g Golang) GetInstallDirectory() (string, error) {
|
||||
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", errors.Trace(err)
|
||||
}
|
||||
|
||||
isInstalled, err := g.IsInstalled()
|
||||
if err != nil {
|
||||
return "", errors.Trace(err)
|
||||
}
|
||||
|
||||
// concat default install dir with home and we have our path
|
||||
if !isInstalled {
|
||||
return fmt.Sprintf("%s/%s", homeDir, defaultGolangInstallDir), nil
|
||||
}
|
||||
|
||||
// now let's play and find the current install path
|
||||
golangPath, err := g.GetBinaryPath()
|
||||
if err != nil {
|
||||
return "", errors.Trace(err)
|
||||
}
|
||||
|
||||
log.Debug("executing go env GOROOT command")
|
||||
|
||||
cmd := exec.Command(golangPath, "env", "GOROOT")
|
||||
stdout, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", errors.Trace(err)
|
||||
}
|
||||
cleanOut := strings.TrimSpace(string(stdout))
|
||||
|
||||
if !strings.Contains(cleanOut, homeDir) {
|
||||
return fmt.Sprintf("%s/%s", homeDir, defaultGolangInstallDir), nil
|
||||
}
|
||||
|
||||
return cleanOut, nil
|
||||
}
|
||||
|
||||
func getDownloadUrl() string {
|
||||
|
||||
return fmt.Sprintf("https://dl.google.com/go/go%s.%s-%s.tar.gz", minimumGolangVersion, runtime.GOOS, runtime.GOARCH)
|
||||
}
|
14
helpers/dependencies/interface.go
Normal file
14
helpers/dependencies/interface.go
Normal file
@ -0,0 +1,14 @@
|
||||
package dependencies
|
||||
|
||||
// Dependency
|
||||
type Dependency interface {
|
||||
DescribeInstall(path string) string
|
||||
GetBinaryPath() (string, error)
|
||||
GetInstallDirectory() (string, error)
|
||||
GetName() string
|
||||
GetMinimumVersion() string
|
||||
GetVersion() (string, error)
|
||||
Install(path string) error
|
||||
IsInstalled() (bool, error)
|
||||
IsVersionSupported() (bool, error)
|
||||
}
|
106
helpers/dependencies/utils.go
Normal file
106
helpers/dependencies/utils.go
Normal file
@ -0,0 +1,106 @@
|
||||
package dependencies
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/juju/errors"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// downloadFile will download file from a given url and store in memory the content
|
||||
// content will probably be untar, ungzip, ....
|
||||
func downloadFile(url string) (io.Reader, error) {
|
||||
|
||||
// Get the data
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, errors.Trace(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
_, err = buf.ReadFrom(resp.Body)
|
||||
if err != nil {
|
||||
return nil, errors.Trace(err)
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func unGzip(reader io.Reader) (io.Reader, error) {
|
||||
|
||||
gzipReader, err := gzip.NewReader(reader)
|
||||
if err != nil {
|
||||
return nil, errors.Trace(err)
|
||||
}
|
||||
defer gzipReader.Close()
|
||||
|
||||
return gzipReader, nil
|
||||
}
|
||||
|
||||
func unTar(reader io.Reader, subdir string, dest string) error {
|
||||
tr := tar.NewReader(reader)
|
||||
|
||||
if subdir != "" && !strings.HasSuffix(subdir, "/") {
|
||||
subdir = fmt.Sprintf("%s/", subdir)
|
||||
}
|
||||
|
||||
for {
|
||||
header, err := tr.Next()
|
||||
switch {
|
||||
// no more files
|
||||
case err == io.EOF:
|
||||
return nil
|
||||
case err != nil:
|
||||
return errors.Trace(err)
|
||||
case header == nil:
|
||||
continue
|
||||
}
|
||||
|
||||
filename := header.Name
|
||||
|
||||
if subdir != "" && strings.HasPrefix(filename, subdir) {
|
||||
|
||||
filename = strings.Replace(filename, subdir, "", 1)
|
||||
|
||||
if filename == "" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
target := filepath.Join(dest, filename)
|
||||
|
||||
log.Debugf("Extacting %s", target)
|
||||
|
||||
switch header.Typeflag {
|
||||
// create directory if doesn't exit
|
||||
case tar.TypeDir:
|
||||
if _, err := os.Stat(target); err != nil {
|
||||
if err := os.MkdirAll(target, 0755); err != nil {
|
||||
return errors.Trace(err)
|
||||
}
|
||||
}
|
||||
// create file
|
||||
case tar.TypeReg:
|
||||
f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
|
||||
if err != nil {
|
||||
return errors.Trace(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// copy contents to file
|
||||
if _, err := io.Copy(f, tr); err != nil {
|
||||
return errors.Trace(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
140
helpers/file.go
Normal file
140
helpers/file.go
Normal file
@ -0,0 +1,140 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/juju/errors"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func IsStrapProject() (bool, error) {
|
||||
|
||||
currentPath, err := os.Getwd()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if !fileExists(fmt.Sprintf("%s/%s", currentPath, configFile)) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func IsGoProject() (bool, error) {
|
||||
|
||||
currentPath, err := os.Getwd()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if fileExists(fmt.Sprintf("%s/%s", currentPath, "go.mod")) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if fileExists(fmt.Sprintf("%s/%s", currentPath, "go.sum")) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func fileExists(filename string) bool {
|
||||
info, err := os.Stat(filename)
|
||||
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return false
|
||||
}
|
||||
return !info.IsDir()
|
||||
}
|
||||
|
||||
// isDirectoryWritable
|
||||
func isDirectoryWritable(path string) (bool, error) {
|
||||
|
||||
dirInfo, err := os.Stat(path)
|
||||
if err != nil {
|
||||
|
||||
if errors.Is(err, fs.ErrPermission) {
|
||||
return false, errors.NewForbidden(err, "directory is not readeable")
|
||||
} else if errors.Is(err, fs.ErrNotExist) {
|
||||
return false, errors.NewNotFound(err, "directory does not exit")
|
||||
} else {
|
||||
return false, errors.Trace(err)
|
||||
}
|
||||
}
|
||||
|
||||
if !dirInfo.IsDir() {
|
||||
return false, errors.NotSupportedf("given path is not a directory")
|
||||
}
|
||||
|
||||
if dirInfo.Mode().Perm()&(1<<(uint(7))) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// createDirectory
|
||||
func createDirectory(path string) error {
|
||||
|
||||
// no need to check if path exist
|
||||
// MkdirAll will do it for us
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
return errors.Trace(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckAndCreateDir will check if path is writable and create directory if needed
|
||||
func CheckAndCreateDir(path string) error {
|
||||
|
||||
dirWritable, err := isDirectoryWritable(path)
|
||||
if dirWritable {
|
||||
return nil
|
||||
} else if err != nil && !errors.Is(err, errors.NotFound) {
|
||||
log.Warnf("impossible to check if the directory is writable")
|
||||
return err
|
||||
} else if err == nil && !dirWritable {
|
||||
log.Warnf("directory is not writable")
|
||||
return errors.Forbiddenf("directory is not writable")
|
||||
}
|
||||
|
||||
err = createDirectory(path)
|
||||
if err != nil {
|
||||
|
||||
log.Warnf("impossible to create directory (%s), please try again", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func RemoveDirectoryContent(path string) error {
|
||||
|
||||
dir, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
return errors.Trace(err)
|
||||
}
|
||||
|
||||
for _, d := range dir {
|
||||
|
||||
err := os.RemoveAll(
|
||||
filepath.Join(
|
||||
[]string{"path", d.Name()}...,
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Trace(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
1
helpers/http.go
Normal file
1
helpers/http.go
Normal file
@ -0,0 +1 @@
|
||||
package helpers
|
58
helpers/input.go
Normal file
58
helpers/input.go
Normal file
@ -0,0 +1,58 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// YesOrNoInput
|
||||
func YesOrNoInput() bool {
|
||||
|
||||
var userInput string
|
||||
for {
|
||||
|
||||
_, err := fmt.Scanf("%s", &userInput)
|
||||
if err != nil {
|
||||
log.Infof("failed to read input, try again (%s)", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
lUserInput := strings.ToLower(userInput)
|
||||
|
||||
for _, positiveAnswer := range []string{"yes", "y", "1", "true"} {
|
||||
if lUserInput == positiveAnswer {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, negativeAnswer := range []string{"no", "n", "0", "false"} {
|
||||
if lUserInput == negativeAnswer {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("Expecting a yes or no answer, Try again")
|
||||
}
|
||||
}
|
||||
|
||||
// IsValidPathInput
|
||||
func IsValidPathInput() string {
|
||||
var userInput string
|
||||
for {
|
||||
|
||||
_, err := fmt.Scanf("%s", &userInput)
|
||||
if err != nil {
|
||||
log.Infof("failed to read input, try again (%s)", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
err = CheckAndCreateDir(userInput)
|
||||
if err != nil {
|
||||
log.Warnf("please, try again")
|
||||
continue
|
||||
}
|
||||
|
||||
return userInput
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user