diff --git a/go.mod b/go.mod index d33a583e..08eb6133 100644 --- a/go.mod +++ b/go.mod @@ -22,4 +22,5 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/viper v1.4.0 github.com/zclconf/go-cty v1.8.0 // indirect + golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect ) diff --git a/go.sum b/go.sum index 6f2d87a2..9211d0f0 100644 --- a/go.sum +++ b/go.sum @@ -200,6 +200,8 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82 h1:vsphBvatvfbhlb4PO1BYSr9dzugGxJ/SQHoNufZJq1w= golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= diff --git a/lib/download.go b/lib/download.go index bb81c03c..348373f1 100644 --- a/lib/download.go +++ b/lib/download.go @@ -13,13 +13,11 @@ import ( func DownloadFromURL(installLocation string, url string) (string, error) { tokens := strings.Split(url, "/") fileName := tokens[len(tokens)-1] - fmt.Println("Downloading", url, "to", fileName) - fmt.Println("Downloading ...") + fmt.Printf("Downloading to: %s\n", installLocation) response, err := http.Get(url) - if err != nil { - fmt.Println("Error while downloading", url, "-", err) + fmt.Println("[Error] : Error while downloading", url, "-", err) return "", err } defer response.Body.Close() @@ -27,23 +25,23 @@ func DownloadFromURL(installLocation string, url string) (string, error) { if response.StatusCode != 200 { //Sometimes hashicorp terraform file names are not consistent //For example 0.12.0-alpha4 naming convention in the release repo is not consistent - return "", fmt.Errorf("Unable to download from %s\nPlease download manually from https://releases.hashicorp.com/terraform/", url) + return "", fmt.Errorf("[Error] : Unable to download from %s", url) } zipFile := filepath.Join(installLocation, fileName) output, err := os.Create(zipFile) if err != nil { - fmt.Println("Error while creating", zipFile, "-", err) + fmt.Println("[Error] : Error while creating", zipFile, "-", err) return "", err } defer output.Close() n, err := io.Copy(output, response.Body) if err != nil { - fmt.Println("Error while downloading", url, "-", err) + fmt.Println("[Error] : Error while downloading", url, "-", err) return "", err } - fmt.Println(n, "bytes downloaded.") + fmt.Println(n, "bytes downloaded") return zipFile, nil } diff --git a/lib/files.go b/lib/files.go index 1ca70a48..5e9ecb55 100644 --- a/lib/files.go +++ b/lib/files.go @@ -11,6 +11,8 @@ import ( "os" "path/filepath" "strings" + + "golang.org/x/sys/unix" ) // RenameFile : rename file name @@ -103,10 +105,10 @@ func Unzip(src string, dest string) ([]string, error) { //CreateDirIfNotExist : create directory if directory does not exist func CreateDirIfNotExist(dir string) { if _, err := os.Stat(dir); os.IsNotExist(err) { - log.Printf("Creating directory for terraform: %v", dir) + fmt.Printf("Creating directory for terraform binary at: %v\n", dir) err = os.MkdirAll(dir, 0755) if err != nil { - fmt.Printf("Unable to create directory for terraform: %v", dir) + fmt.Printf("Unable to create directory for terraform binary at: %v", dir) panic(err) } } @@ -204,7 +206,7 @@ func CheckDirHasTGBin(dir, prefix string) bool { //CheckDirExist : check if directory exist //dir=path to file -//return path to directory +//return bool func CheckDirExist(dir string) bool { if _, err := os.Stat(dir); os.IsNotExist(err) { return false @@ -220,6 +222,35 @@ func Path(value string) string { // GetFileName : remove file ext. .tfswitch.config returns .tfswitch func GetFileName(configfile string) string { - return strings.TrimSuffix(configfile, filepath.Ext(configfile)) } + +//Check if user has permission to directory : +//dir=path to file +//return bool +func CheckDirWritable(dir string) bool { + return unix.Access(dir, unix.W_OK) == nil +} + +// func WindowsCheckDirWritable(path string) bool { + +// info, err := os.Stat(path) +// if err != nil { +// fmt.Println("Path doesn't exist") +// return false +// } + +// err = nil +// if !info.IsDir() { +// fmt.Println("Path isn't a directory") +// return false +// } + +// // Check if the user bit is enabled in file permission +// if info.Mode().Perm()&(1<<(uint(7))) == 0 { +// fmt.Println("Write permission bit is not set on this file for user") +// return false +// } + +// return true +// } diff --git a/lib/install.go b/lib/install.go index 12ad8959..0b45dca0 100644 --- a/lib/install.go +++ b/lib/install.go @@ -7,6 +7,7 @@ import ( "os/user" "path/filepath" "runtime" + "strings" ) const ( @@ -14,13 +15,14 @@ const ( installVersion = "terraform_" installPath = ".terraform.versions" recentFile = "RECENT" + defaultBin = "/usr/local/bin/terraform" //default bin installation dir ) var ( installLocation = "/tmp" ) -// initialize : removes existing symlink to terraform binary +// initialize : removes existing symlink to terraform binary// I Don't think this is needed func initialize() { /* Step 1 */ @@ -69,7 +71,6 @@ func getInstallLocation() string { /* set installation location */ installLocation = filepath.Join(userCommon, installPath) - fmt.Printf("installLocation: %s", installLocation) /* Create local installation directory if it does not exist */ CreateDirIfNotExist(installLocation) @@ -86,14 +87,14 @@ func Install(tfversion string, binPath string, mirrorURL string) { os.Exit(1) } - pathDir := Path(binPath) //get path directory from binary path - binDirExist := CheckDirExist(pathDir) //check bin path exist - - if !binDirExist { - fmt.Printf("Error - Binary path does not exist: %s\n", pathDir) - fmt.Printf("Create binary path: %s for terraform installation\n", pathDir) - os.Exit(1) - } + pathDir := Path(binPath) //get path directory from binary path + //binDirExist := CheckDirExist(pathDir) //check bin path exist + /* Check to see if user has permission to the default bin location which is "/usr/local/bin/terraform" + * If user does not have permission to default bin location, proceed to create $HOME/bin and install the tfswitch there + * Inform user that they dont have permission to default location, therefore tfswitch was installed in $HOME/bin + * Tell users to add $HOME/bin to their path + */ + binPath = InstallableBinLocation(pathDir) initialize() //initialize path installLocation = getInstallLocation() //get installation location - this is where we will put our terraform binary file @@ -127,6 +128,12 @@ func Install(tfversion string, binPath string, mirrorURL string) { os.Exit(0) } + //if does not have slash - append slash + hasSlash := strings.HasSuffix(mirrorURL, "/") + if !hasSlash { + mirrorURL = fmt.Sprintf("%s/", mirrorURL) + } + /* if selected version already exist, */ /* proceed to download it from the hashicorp release page */ url := mirrorURL + tfversion + "/" + installVersion + tfversion + "_" + goos + "_" + goarch + ".zip" @@ -141,7 +148,7 @@ func Install(tfversion string, binPath string, mirrorURL string) { /* unzip the downloaded zipfile */ _, errUnzip := Unzip(zipFile, installLocation) if errUnzip != nil { - fmt.Println("Unable to unzip downloaded zip file") + fmt.Println("[Error] : Unable to unzip downloaded zip file") log.Fatal(errUnzip) os.Exit(1) } @@ -178,7 +185,7 @@ func AddRecent(requestedVersion string) { lines, errRead := ReadLines(versionFile) if errRead != nil { - fmt.Printf("Error: %s\n", errRead) + fmt.Printf("[Error] : %s\n", errRead) return } @@ -269,3 +276,46 @@ func ConvertExecutableExt(fpath string) string { return fpath } } + +//InstallableBinLocation : Checks if terraform is installable in the location provided by the user. +//If not, create $HOME/bin. Ask users to add $HOME/bin to $PATH +//Return $HOME/bin as install location +func InstallableBinLocation(userBin string) string { + + usr, errCurr := user.Current() + if errCurr != nil { + log.Fatal(errCurr) + } + + existDefaultBin := CheckDirExist(userBin) + + if existDefaultBin { //if exist - now see if we can write to to it + + writableToDefault := false + if runtime.GOOS != "windows" { + writableToDefault = CheckDirWritable(userBin) //check if is writable on ( only works on LINUX) + } + + if !writableToDefault { + exisHomeBin := CheckDirExist(filepath.Join(usr.HomeDir, "bin")) + if exisHomeBin { + fmt.Printf("Installing terraform at %s\n", filepath.Join(usr.HomeDir, "bin")) + return filepath.Join(usr.HomeDir, "bin", "terraform") + } + PrintCreateDirStmt(userBin, filepath.Join(usr.HomeDir, "bin")) + CreateDirIfNotExist(filepath.Join(usr.HomeDir, "bin")) + return filepath.Join(usr.HomeDir, "bin", "terraform") + } + return filepath.Join(userBin, "terraform") + } + fmt.Printf("[Error] : Binary path does not exist: %s\n", userBin) + fmt.Printf("[Error] : Manually create bin directory at: %s and try again.\n", userBin) + os.Exit(1) + return "" +} + +func PrintCreateDirStmt(unableDir string, writable string) { + fmt.Printf("Unable to write to: %s\n", unableDir) + fmt.Printf("Creating bin directory at: %s\n", writable) + fmt.Printf("RUN `export PATH=$PATH:%s` to append bin to $PATH\n", writable) +} diff --git a/lib/list_versions.go b/lib/list_versions.go index 9708091d..0053e688 100644 --- a/lib/list_versions.go +++ b/lib/list_versions.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "log" "net/http" + "os" "reflect" "regexp" "strings" @@ -40,6 +41,10 @@ func GetTFList(mirrorURL string, preRelease bool) ([]string, error) { } } + if len(tfVersionList.tflist) == 0 { + fmt.Printf("Cannot get list from mirror: %s\n", mirrorURL) + } + return tfVersionList.tflist, nil } @@ -95,20 +100,27 @@ func GetTFLatestImplicit(mirrorURL string, preRelease bool, version string) (str //GetTFURLBody : Get list of terraform versions from hashicorp releases func GetTFURLBody(mirrorURL string) ([]string, error) { + hasSlash := strings.HasSuffix(mirrorURL, "/") + if !hasSlash { //if does not have slash - append slash + mirrorURL = fmt.Sprintf("%s/", mirrorURL) + } resp, errURL := http.Get(mirrorURL) if errURL != nil { - log.Printf("Error getting url: %v", errURL) + log.Printf("[Error] : Getting url: %v", errURL) + os.Exit(1) return nil, errURL } defer resp.Body.Close() if resp.StatusCode != 200 { - log.Printf("Error retrieving contents from url: %s", mirrorURL) + log.Printf("[Error] : Retrieving contents from url: %s", mirrorURL) + os.Exit(1) } body, errBody := ioutil.ReadAll(resp.Body) if errBody != nil { - log.Printf("Error reading body: %v", errBody) + log.Printf("[Error] : reading body: %v", errBody) + os.Exit(1) return nil, errBody } diff --git a/main.go b/main.go index 11b84991..1053ccd5 100644 --- a/main.go +++ b/main.go @@ -44,7 +44,7 @@ import ( ) const ( - defaultMirror = "https://releases.hashicorp.com/terraform/" + defaultMirror = "https://releases.hashicorp.com/terraform" defaultBin = "/usr/local/bin/terraform" //default bin installation dir defaultLatest = "" tfvFilename = ".terraform-version" @@ -366,6 +366,10 @@ func installOption(listAll bool, custBinPath, mirrorURL *string) { tflist = append(recentVersions, tflist...) //append recent versions to the top of the list tflist = lib.RemoveDuplicateVersions(tflist) //remove duplicate version + if len(tflist) == 0 { + fmt.Println("[ERROR] : List is empty") + os.Exit(1) + } /* prompt user to select version of terraform */ prompt := promptui.Select{ Label: "Select Terraform version",