From ee5789c5f765b3be0089febb9a2919028f24524b Mon Sep 17 00:00:00 2001 From: Mike Hennessy Date: Wed, 5 Jan 2022 13:49:08 -0500 Subject: [PATCH] feat: mimic how tgenv searches for a version file --- lib/install.go | 34 ++++--- lib/list_versions.go | 19 ++-- lib/list_versions_test.go | 17 +--- main.go | 207 +++++++++++++++++++++----------------- 4 files changed, 146 insertions(+), 131 deletions(-) diff --git a/lib/install.go b/lib/install.go index 7bca0e2..cea93e4 100644 --- a/lib/install.go +++ b/lib/install.go @@ -148,21 +148,31 @@ func CreateRecentFile(requestedVersion string) { } // ValidVersionFormat : returns valid version format -/* For example: 0.1.2 = valid -// For example: 0.1.2-beta1 = valid -// For example: 0.1.2-alpha = valid -// For example: a.1.2 = invalid -// For example: 0.1. 2 = invalid +/* +For example: +- 0.1.2 = valid +- latest = valid +- latest:^0.30 = valid +- a.1.2 = invalid +- 0.1. 2 = invalid */ func ValidVersionFormat(version string) bool { // Getting versions from body; should return match /X.X.X-@/ where X is a number,@ is a word character between a-z or A-Z // Follow https://semver.org/spec/v1.0.0-beta.html // Check regular expression at https://rubular.com/r/ju3PxbaSBALpJB - semverRegex := regexp.MustCompile(`^(\d+\.\d+\.\d+)(-[a-zA-z]+\d*)?$`) - - if !semverRegex.MatchString(version) { - fmt.Println("Invalid terragrunt version format. Format should be #.#.# or #.#.#-@# where # is numbers and @ is word characters. For example, 0.11.7 and 0.11.9-beta1 are valid versions") + semverRegex := regexp.MustCompile(`^(?:\d+\.){2}\d+$`) + latestRegex := regexp.MustCompile(`^latest\:(.*)$`) + + if version == "latest" { + return true + } else if latestRegex.MatchString(version) { + return true + } else if semverRegex.MatchString(version) { + return true + } else { + fmt.Printf("Invalid terragrunt version format %q. Format should be #.#.#, latest, or latest:. For example, 0.11.7 and latest:^0.34 are valid versions", version) + return false } return semverRegex.MatchString(version) @@ -227,7 +237,7 @@ func Install(tgversion string, usrBinPath string, mirrorURL string) string { /* set symlink to desired version */ CreateSymlink(installFileVersionPath, binPath) - fmt.Printf("Switched terragrunt to version %q \n", tgversion) + fmt.Printf("Switched terragrunt to version %q\n", tgversion) //AddRecent(tgversion) //add to recent file for faster lookup os.Exit(0) return "" @@ -246,7 +256,7 @@ func InstallableBinLocation(userBinPath string) string { binDir := Path(userBinPath) //get path directory from binary path binPathExist := CheckDirExist(binDir) //the default is /usr/local/bin but users can provide custom bin locations - if binPathExist == true { //if bin path exist - check if we can write to to it + if binPathExist { //if bin path exist - check if we can write to to it binPathWritable := false //assume bin path is not writable if runtime.GOOS != "windows" { @@ -254,7 +264,7 @@ func InstallableBinLocation(userBinPath string) string { } // IF: "/usr/local/bin" or `custom bin path` provided by user is non-writable, (binPathWritable == false), we will attempt to install terragrunt at the ~/bin location. See ELSE - if binPathWritable == false { + if !binPathWritable { homeBinExist := CheckDirExist(filepath.Join(usr.HomeDir, "bin")) //check to see if ~/bin exist if homeBinExist { //if ~/bin exist, install at ~/bin/terragrunt diff --git a/lib/list_versions.go b/lib/list_versions.go index a8f135e..8ac3dcd 100644 --- a/lib/list_versions.go +++ b/lib/list_versions.go @@ -20,15 +20,14 @@ func VersionExist(val interface{}, array interface{}) (exists bool) { s := reflect.ValueOf(array) for i := 0; i < s.Len(); i++ { - if reflect.DeepEqual(val, s.Index(i).Interface()) == true { + if reflect.DeepEqual(val, s.Index(i).Interface()) { exists = true - return exists } } } if !exists { - fmt.Println("Requested version does not exist") + fmt.Printf("Requested version %q does not exist\n", val) } return exists @@ -41,8 +40,8 @@ func RemoveDuplicateVersions(elements []string) []string { result := []string{} for _, val := range elements { - versionOnly := strings.Trim(val, " *recent") - if encountered[versionOnly] == true { + versionOnly := strings.TrimSuffix(val, " *recent") + if encountered[versionOnly] { // Do not add duplicate. } else { // Record this element as an encountered element. @@ -58,7 +57,7 @@ func RemoveDuplicateVersions(elements []string) []string { func GetAppList(gruntURLPage string) []string { gswitch := http.Client{ - Timeout: time.Second * 10, // Maximum of 10 secs [decresing this seem to fail] + Timeout: time.Second * 10, // Maximum of 10 secs [decreasing this seems to fail] } req, err := http.NewRequest(http.MethodGet, gruntURLPage, nil) @@ -70,20 +69,18 @@ func GetAppList(gruntURLPage string) []string { res, getErr := gswitch.Do(req) if getErr != nil { - log.Fatal("Unable to make request Please try again.") + log.Fatal("Unable to make request. Please try again.") } body, readErr := ioutil.ReadAll(res.Body) if readErr != nil { - log.Println("Unable to get release from repo ", string(body)) - log.Fatal(readErr) + log.Fatalf("Unable to get release from repo %s:\n%s", body, readErr) } var repo ListVersion jsonErr := json.Unmarshal(body, &repo) if jsonErr != nil { - log.Println("Unable to get release from repo ", string(body)) - log.Fatal(jsonErr) + log.Fatalf("Unable to get release from repo %s:\n%s", body, jsonErr) } return repo.Versions diff --git a/lib/list_versions_test.go b/lib/list_versions_test.go index 59646a8..bc8f71d 100644 --- a/lib/list_versions_test.go +++ b/lib/list_versions_test.go @@ -80,7 +80,7 @@ func TestValidVersionFormat(t *testing.T) { log.Fatalf("Failed to verify version format: %s\n", version) } - version = "1.11.9-beta1" + version = "latest" valid = lib.ValidVersionFormat(version) @@ -90,7 +90,7 @@ func TestValidVersionFormat(t *testing.T) { log.Fatalf("Failed to verify version format: %s\n", version) } - version = "0.12.0-rc2" + version = "latest:^0.35" valid = lib.ValidVersionFormat(version) @@ -100,7 +100,7 @@ func TestValidVersionFormat(t *testing.T) { log.Fatalf("Failed to verify version format: %s\n", version) } - version = "1.11.4-boom" + version = "latest:^0.35.13" valid = lib.ValidVersionFormat(version) @@ -109,15 +109,4 @@ func TestValidVersionFormat(t *testing.T) { } else { log.Fatalf("Failed to verify version format: %s\n", version) } - - version = "1.11.4-1" - - valid = lib.ValidVersionFormat(version) - - if valid == false { - t.Logf("Invalid version format : %s (expected)", version) - } else { - log.Fatalf("Failed to verify version format: %s\n", version) - } - } diff --git a/main.go b/main.go index f22197b..b22d36d 100644 --- a/main.go +++ b/main.go @@ -20,6 +20,8 @@ import ( "io/ioutil" "log" "os" + "path/filepath" + "regexp" "strings" "github.com/manifoldco/promptui" @@ -38,24 +40,68 @@ const ( var version = "0.5.0\n" -func main() { +// findVersionFile searches recursively upwards from the current working directory for a terragrunt +// version file (ie. ".tgswitchrc" or ".terragrunt-version"). It then parses this file and returns +// the version string. +func findVersionFile() string { + var tgVersion string + var fileContents []byte + var err error + + cwd, err := os.Getwd() + if err != nil { + log.Printf("Failed to get current directory %v\n", err) + os.Exit(1) + } + + path := strings.Split(cwd, string(filepath.Separator)) + pathSegments := len(path) + + // range over the segemnts of the full cwd path + for i := range path { + // get the current segment position in the path based on loop iterator + currentSegment := pathSegments - i + + // create an array of all path segments based on iterator position + // since we are working backwards, we only want the segments _up to_ the + // current iterator position + var currentPathSegments = path[:currentSegment] + + // check current segment position for either version file type + versionFiles := [2]string{rcFilename, tgvFilename} + for _, fileName := range versionFiles { + // constuct the full file path to check from the array of segments + filePath := append(currentPathSegments, fileName) + absPath := string(filepath.Separator) + filepath.Join(filePath...) + + // check for the version file, if found break the loop + fileContents, _ = ioutil.ReadFile(absPath) + if fileContents != nil { + fmt.Printf("Found version file at %s\n", absPath) + tgVersion = strings.TrimSuffix(string(fileContents), "\n") + break + } + } + + // if found a version break the parent loop + if tgVersion != "" { + break + } + } + + return tgVersion +} +func main() { custBinPath := getopt.StringLong("bin", 'b', defaultBin, "Custom binary path. For example: /Users/username/bin/terragrunt") versionFlag := getopt.BoolLong("version", 'v', "displays the version of tgswitch") helpFlag := getopt.BoolLong("help", 'h', "displays help message") - _ = versionFlag getopt.Parse() args := getopt.Args() - dir, err := os.Getwd() - if err != nil { - log.Printf("Failed to get current directory %v\n", err) - os.Exit(1) - } - - tgvfile := dir + fmt.Sprintf("/%s", tgvFilename) //settings for .terragrunt-version file in current directory (tgenv compatible) - rcfile := dir + fmt.Sprintf("/%s", rcFilename) //settings for .tgswitchrc file in current directory + var requestedVersion string + var listOfVersions []string if *versionFlag { fmt.Printf("\nVersion: %v\n", version) @@ -63,109 +109,82 @@ func main() { usageMessage() } else { installLocation := lib.GetInstallLocation() - if _, err := os.Stat(rcfile); err == nil && len(args) == 0 { //if there is a .tgswitchrc file, and no commmand line arguments - fmt.Printf("Reading required terragrunt version %s \n", rcFilename) - - fileContents, err := ioutil.ReadFile(rcfile) - if err != nil { - fmt.Printf("Failed to read %s file. Follow the README.md instructions for setup. https://github.com/warrensbox/tgswitch/blob/master/README.md\n", rcFilename) - fmt.Printf("Error: %s\n", err) - os.Exit(1) - } - tgversion := strings.TrimSuffix(string(fileContents), "\n") - fileExist := lib.CheckFileExist(installLocation + installVersion + tgversion) - if fileExist { - lib.ChangeSymlink(*custBinPath, string(tgversion)) - os.Exit(0) - } - listOfVersions := lib.GetAppList(proxyUrl) - - if lib.ValidVersionFormat(tgversion) && lib.VersionExist(tgversion, listOfVersions) { //check if version format is correct && if version exist - lib.Install(tgversion, *custBinPath, terragruntURL) - } else { - os.Exit(1) - } - - } else if _, err := os.Stat(tgvfile); err == nil && len(args) == 0 { - fmt.Printf("Reading required terragrunt version %s \n", tgvFilename) + if len(args) == 0 { + requestedVersion = findVersionFile() + } else if len(args) == 1 { + requestedVersion = args[0] + } - fileContents, err := ioutil.ReadFile(tgvfile) - if err != nil { - fmt.Printf("Failed to read %s file. Follow the README.md instructions for setup. https://github.com/warrensbox/tgswitch/blob/master/README.md\n", tgvFilename) - fmt.Printf("Error: %s\n", err) - os.Exit(1) - } - tgversion := strings.TrimSuffix(string(fileContents), "\n") - fileExist := lib.CheckFileExist(installLocation + installVersion + string(tgversion)) + if requestedVersion != "" && lib.ValidVersionFormat(requestedVersion) { + fileExist := lib.CheckFileExist(installLocation + installVersion + requestedVersion) if fileExist { - lib.ChangeSymlink(*custBinPath, string(tgversion)) + lib.ChangeSymlink(*custBinPath, requestedVersion) os.Exit(0) } - listOfVersions := lib.GetAppList(proxyUrl) - - if lib.ValidVersionFormat(tgversion) && lib.VersionExist(tgversion, listOfVersions) { //check if version format is correct && if version exist - lib.Install(tgversion, *custBinPath, terragruntURL) - } else { - os.Exit(1) - } - - } else if len(args) == 1 { - requestedVersion := args[0] - - if lib.ValidVersionFormat(requestedVersion) { - - fileExist := lib.CheckFileExist(installLocation + installVersion + string(requestedVersion)) - if fileExist { - lib.ChangeSymlink(*custBinPath, string(requestedVersion)) - os.Exit(0) - } - //check if version exist before downloading it - listOfVersions := lib.GetAppList(proxyUrl) - exist := lib.VersionExist(requestedVersion, listOfVersions) - - if exist { - installLocation := lib.Install(requestedVersion, *custBinPath, terragruntURL) - fmt.Println("remove later - installLocation:", installLocation) + // check if version exists locally before downloading it + listOfVersions = lib.GetAppList(proxyUrl) + + // create a regex that will match a version string that itself contains a regex + latestRegex := regexp.MustCompile(`^latest\:(.*)$`) + + // first check if the version is simply "latest" and use the first version + // next check if the version is a valid regex that matches one or more versions in + // the list of versions. + if requestedVersion == "latest" { + requestedVersion = listOfVersions[0] + } else if latestRegex.MatchString(requestedVersion) { + versionRegex, err := regexp.Compile(latestRegex.FindStringSubmatch(requestedVersion)[1]) + if err != nil { + fmt.Printf("Version regex %q is not valid\n", requestedVersion) + } else { + for _, v := range listOfVersions { + if versionRegex.MatchString(v) { + requestedVersion = v + break + } + } } + } + exist := lib.VersionExist(requestedVersion, listOfVersions) + if exist { + lib.Install(requestedVersion, *custBinPath, terragruntURL) + os.Exit(0) } else { - fmt.Println("Args must be a valid terragrunt version") usageMessage() } + } - } else if len(args) == 0 { - - listOfVersions := lib.GetAppList(proxyUrl) - recentVersions, _ := lib.GetRecentVersions() //get recent versions from RECENT file - listOfVersions = append(recentVersions, listOfVersions...) //append recent versions to the top of the list - listOfVersions = lib.RemoveDuplicateVersions(listOfVersions) //remove duplicate version - - /* prompt user to select version of terragrunt */ - prompt := promptui.Select{ - Label: "Select terragrunt version", - Items: listOfVersions, - } - - _, tgversion, errPrompt := prompt.Run() - tgversion = strings.Trim(tgversion, " *recent") + if len(listOfVersions) == 0 { + listOfVersions = lib.GetAppList(proxyUrl) + } + recentVersions, _ := lib.GetRecentVersions() //get recent versions from RECENT file + listOfVersions = append(recentVersions, listOfVersions...) //append recent versions to the top of the list + listOfVersions = lib.RemoveDuplicateVersions(listOfVersions) //remove duplicate version + + /* prompt user to select version of terragrunt */ + prompt := promptui.Select{ + Label: "Select terragrunt version", + Items: listOfVersions, + } - if errPrompt != nil { - log.Printf("Prompt failed %v\n", errPrompt) - os.Exit(1) - } + _, requestedVersion, errPrompt := prompt.Run() + requestedVersion = strings.TrimSuffix(requestedVersion, " *recent") - lib.Install(tgversion, *custBinPath, terragruntURL) - os.Exit(0) - } else { - usageMessage() + if errPrompt != nil { + log.Printf("Prompt failed %v\n", errPrompt) + os.Exit(1) } - } + lib.Install(requestedVersion, *custBinPath, terragruntURL) + os.Exit(0) + } } func usageMessage() { fmt.Print("\n\n") getopt.PrintUsage(os.Stderr) fmt.Println("Supply the terragrunt version as an argument, or choose from a menu") + os.Exit(0) }