Skip to content

Commit

Permalink
Merge pull request #123 from warrensbox/develop
Browse files Browse the repository at this point in the history
Develop - support implicit latest version
  • Loading branch information
warrensbox authored Jan 13, 2021
2 parents 44315ba + 69c438d commit 738ff96
Show file tree
Hide file tree
Showing 8 changed files with 402 additions and 58 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ The most recently selected versions are presented at the top of the dropdown.
2. For example, `tfswitch -l` or `tfswitch --list-all` to see all versions.
3. Hit **Enter** to select the desired version.

### Install latest version only
1. Install the latest stable version only.
2. Run `tfswitch -u` or `tfswitch --latest` to see all versions.
3. Hit **Enter** to install.
### Install latest implicit version for stable releases
1. Install the latest implicit stable version.
2. Ex: `tfswitch -s 0.13` or `tfswitch --latest-stable 0.13` downloads 0.13.6 (latest) version.
3. Hit **Enter** to install.
### Install latest implicit version for beta, alpha and release candidates(rc)
1. Install the latest implicit pre-release version.
2. Ex: `tfswitch -p 0.13` or `tfswitch --latest-pre 0.13` downloads 0.13.0-rc1 (latest) version.
3. Hit **Enter** to install.
### Use version.tf file
If a .tf file with the terraform constrain is included in the current directory, it should automatically download or switch to that terraform version. For example, the following should automatically switch terraform to the lastest version:
```ruby
Expand Down
4 changes: 3 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE=
github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=
github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0=
Expand Down Expand Up @@ -112,6 +114,7 @@ github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30 h1:BHT1/DKsYDGkUgQ2jmMaozVcdk+sVfz0+1ZJq4zkWgw=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
github.com/pborman/getopt v1.1.0 h1:eJ3aFZroQqq0bWmraivjQNt6Dmm5M0h2JcDW38/Azb0=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.4.0 h1:u3Z1r+oOXJIkxqw34zVhyPgjBsm6X2wn21NWs/HfSeg=
github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
Expand Down Expand Up @@ -205,7 +208,6 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
221 changes: 221 additions & 0 deletions go.sum538389371.tmp

Large diffs are not rendered by default.

118 changes: 94 additions & 24 deletions lib/list_versions.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package lib

import (
"fmt"
"io/ioutil"
"log"
"net/http"
Expand All @@ -14,47 +15,103 @@ type tfVersionList struct {
}

//GetTFList : Get the list of available terraform version given the hashicorp url
func GetTFList(hashiURL string, listAll bool) ([]string, error) {
func GetTFList(hashiURL string, preRelease bool) ([]string, error) {

/* Get list of terraform versions from hashicorp releases */
resp, errURL := http.Get(hashiURL)
if errURL != nil {
log.Printf("Error getting url: %v", errURL)
return nil, errURL
result, error := GetTFURLBody(hashiURL)
if error != nil {
return nil, error
}
defer resp.Body.Close()

body, errBody := ioutil.ReadAll(resp.Body)
if errBody != nil {
log.Printf("Error reading body: %v", errBody)
return nil, errBody
var tfVersionList tfVersionList
var semver string
if preRelease == true {
// 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
semver = `\/(\d+\.\d+\.\d+)(-[a-zA-z]+\d*)?\/`
} else if preRelease == false {
// Getting versions from body; should return match /X.X.X/ where X is a number
semver = `\/(\d+\.\d+\.\d+)\/`
}
r, _ := regexp.Compile(semver)
for i := range result {
if r.MatchString(result[i]) {
str := r.FindString(result[i])
trimstr := strings.Trim(str, "/") //remove "/" from /X.X.X/
tfVersionList.tflist = append(tfVersionList.tflist, trimstr)
}
}

bodyString := string(body)
result := strings.Split(bodyString, "\n")
return tfVersionList.tflist, nil

var tfVersionList tfVersionList
}

//GetTFLatest : Get the latest terraform version given the hashicorp url
func GetTFLatest(hashiURL string) (string, error) {

result, error := GetTFURLBody(hashiURL)
if error != nil {
return "", error
}
// Getting versions from body; should return match /X.X.X/ where X is a number
semver := `\/(\d+\.\d+\.\d+)\/`
r, _ := regexp.Compile(semver)
for i := range result {
// Getting versions from body; should return match /X.X.X/ where X is a number
// Follow https://semver.org/spec/v2.0.0.html
r, _ := regexp.Compile(`\/(\d+\.\d+\.\d+)\/`)
if listAll {
// 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
r, _ = regexp.Compile(`\/(\d+\.\d+\.\d+)(-[a-zA-z]+\d*)?\/`)
if r.MatchString(result[i]) {
str := r.FindString(result[i])
trimstr := strings.Trim(str, "/") //remove "/" from /X.X.X/
return trimstr, nil
}
}

return "", nil
}

//GetTFLatestImplicit : Get the latest implicit terraform version given the hashicorp url
func GetTFLatestImplicit(hashiURL string, preRelease bool, version string) (string, error) {

result, error := GetTFURLBody(hashiURL)
if error != nil {
return "", error
}
var semver string
if preRelease == true {
// 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
semver = fmt.Sprintf(`\/(%s{1}\.\d+\-[a-zA-z]+\d*)?\/`, version)
} else if preRelease == false {
semver = fmt.Sprintf(`\/(%s{1}\.\d+)\/`, version)
}
r, _ := regexp.Compile(semver)
for i := range result {
if r.MatchString(result[i]) {
str := r.FindString(result[i])
trimstr := strings.Trim(str, "/") //remove "/" from /X.X.X/
tfVersionList.tflist = append(tfVersionList.tflist, trimstr)
return trimstr, nil
}
}

return tfVersionList.tflist, nil
return "", nil

}

//GetTFURLBody : Get list of terraform versions from hashicorp releases
func GetTFURLBody(hashiURL string) ([]string, error) {

resp, errURL := http.Get(hashiURL)
if errURL != nil {
log.Printf("Error getting url: %v", errURL)
return nil, errURL
}
defer resp.Body.Close()

body, errBody := ioutil.ReadAll(resp.Body)
if errBody != nil {
log.Printf("Error reading body: %v", errBody)
return nil, errBody
}

bodyString := string(body)
result := strings.Split(bodyString, "\n")

return result, nil
}

//VersionExist : check if requested version exist
Expand Down Expand Up @@ -113,3 +170,16 @@ func ValidVersionFormat(version string) bool {

return semverRegex.MatchString(version)
}

// ValidMinorVersionFormat : returns valid MINOR version format
/* For example: 0.1 = valid
// For example: a.1.2 = invalid
// For example: 0.1.2 = invalid
*/
func ValidMinorVersionFormat(version string) bool {

// Getting versions from body; should return match /X.X./ where X is a number
semverRegex := regexp.MustCompile(`^(\d+\.\d+)$`)

return semverRegex.MatchString(version)
}
96 changes: 71 additions & 25 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,22 @@ import (
)

const (
hashiURL = "https://releases.hashicorp.com/terraform/"
defaultBin = "/usr/local/bin/terraform" //default bin installation dir
tfvFilename = ".terraform-version"
rcFilename = ".tfswitchrc"
tomlFilename = ".tfswitch.toml"
hashiURL = "https://releases.hashicorp.com/terraform/"
defaultBin = "/usr/local/bin/terraform" //default bin installation dir
defaultLatest = ""
tfvFilename = ".terraform-version"
rcFilename = ".tfswitchrc"
tomlFilename = ".tfswitch.toml"
)

var version = "0.9.0\n"
var version = "0.10.0\n"

func main() {

custBinPath := getopt.StringLong("bin", 'b', defaultBin, "Custom binary path. For example: /Users/username/bin/terraform")
custBinPath := getopt.StringLong("bin", 'b', defaultBin, "Custom binary path. Ex: /Users/username/bin/terraform")
listAllFlag := getopt.BoolLong("list-all", 'l', "List all versions of terraform - including beta and rc")
latestPre := getopt.StringLong("latest-pre", 'p', defaultLatest, "Latest pre-release implicit version. Ex: tfswitch --latest-pre 0.13 downloads 0.13.0-rc1 (latest)")
latestStable := getopt.StringLong("latest-stable", 's', defaultLatest, "Latest implicit version. Ex: tfswitch --latest 0.13 downloads 0.13.5 (latest)")
latestFlag := getopt.BoolLong("latest", 'u', "Get latest stable version")
versionFlag := getopt.BoolLong("version", 'v', "Displays the version of tfswitch")
helpFlag := getopt.BoolLong("help", 'h', "Displays help message")
_ = versionFlag
Expand All @@ -73,10 +76,10 @@ func main() {
os.Exit(1)
}

curr_tfvfile := dir + fmt.Sprintf("/%s", tfvFilename) //settings for .terraform-version file in current directory (tfenv compatible)
curr_rcfile := dir + fmt.Sprintf("/%s", rcFilename) //settings for .tfswitchrc file in current directory (backward compatible purpose)
curr_tomlconfigfile := dir + fmt.Sprintf("/%s", tomlFilename) //settings for .tfswitch.toml file in current directory (option to specify bin directory)
home_tomlconfigfile := homedir + fmt.Sprintf("/%s", tomlFilename) //settings for .tfswitch.toml file in home directory (option to specify bin directory)
TFVersionFile := dir + fmt.Sprintf("/%s", tfvFilename) //settings for .terraform-version file in current directory (tfenv compatible)
RCFile := dir + fmt.Sprintf("/%s", rcFilename) //settings for .tfswitchrc file in current directory (backward compatible purpose)
TOMLConfigFile := dir + fmt.Sprintf("/%s", tomlFilename) //settings for .tfswitch.toml file in current directory (option to specify bin directory)
HomeTOMLConfigFile := homedir + fmt.Sprintf("/%s", tomlFilename) //settings for .tfswitch.toml file in home directory (option to specify bin directory)

switch {
case *versionFlag:
Expand All @@ -92,11 +95,11 @@ func main() {
* If you provide a custom binary path with the -b option, this will override the bin value in the toml file
* If you provide a version on the command line, this will override the version value in the toml file
*/
case fileExists(curr_tomlconfigfile) || fileExists(home_tomlconfigfile):
case fileExists(TOMLConfigFile) || fileExists(HomeTOMLConfigFile):

version := ""
binPath := *custBinPath
if fileExists(curr_tomlconfigfile) { //read from toml from current directory
if fileExists(TOMLConfigFile) { //read from toml from current directory
version, binPath = getParamsTOML(binPath, dir)
} else { // else read from toml from home directory
version, binPath = getParamsTOML(binPath, homedir)
Expand All @@ -106,15 +109,23 @@ func main() {
case *listAllFlag:
listAll := true //set list all true - all versions including beta and rc will be displayed
installOption(listAll, &binPath)
case *latestPre != "":
preRelease := true
installLatestImplicitVersion(*latestPre, custBinPath, preRelease)
case *latestStable != "":
preRelease := false
installLatestImplicitVersion(*latestStable, custBinPath, preRelease)
case *latestFlag:
installLatestVersion(custBinPath)
case len(args) == 1:
installVersion(args[0], &binPath)
case fileExists(curr_rcfile) && len(args) == 0:
case fileExists(RCFile) && len(args) == 0:
readingFileMsg(rcFilename)
tfversion := retrieveFileContents(curr_rcfile)
tfversion := retrieveFileContents(RCFile)
installVersion(tfversion, &binPath)
case fileExists(curr_tfvfile) && len(args) == 0:
case fileExists(TFVersionFile) && len(args) == 0:
readingFileMsg(tfvFilename)
tfversion := retrieveFileContents(curr_tfvfile)
tfversion := retrieveFileContents(TFVersionFile)
installVersion(tfversion, &binPath)
case checkTFModuleFileExist(dir) && len(args) == 0:
installTFProvidedModule(dir, &binPath)
Expand All @@ -125,24 +136,38 @@ func main() {
installOption(listAll, &binPath)
}

/* list all versions, //show all terraform version including betas and RCs*/
/* show all terraform version including betas and RCs*/
case *listAllFlag:
installWithListAll(custBinPath)

/* latest pre-release implicit version. Ex: tfswitch --latest-pre 0.13 downloads 0.13.0-rc1 (latest) */
case *latestPre != "":
preRelease := true
installLatestImplicitVersion(*latestPre, custBinPath, preRelease)

/* latest implicit version. Ex: tfswitch --latest 0.13 downloads 0.13.5 (latest) */
case *latestStable != "":
preRelease := false
installLatestImplicitVersion(*latestStable, custBinPath, preRelease)

/* latest stable version */
case *latestFlag:
installLatestVersion(custBinPath)

/* version provided on command line as arg */
case len(args) == 1:
installVersion(args[0], custBinPath)

/* provide an tfswitchrc file */
case fileExists(curr_rcfile) && len(args) == 0:
case fileExists(RCFile) && len(args) == 0:
readingFileMsg(rcFilename)
tfversion := retrieveFileContents(curr_rcfile)
tfversion := retrieveFileContents(RCFile)
installVersion(tfversion, custBinPath)

/* if .terraform-version file found */
case fileExists(curr_tfvfile) && len(args) == 0:
case fileExists(TFVersionFile) && len(args) == 0:
readingFileMsg(tfvFilename)
tfversion := retrieveFileContents(curr_tfvfile)
tfversion := retrieveFileContents(TFVersionFile)
installVersion(tfversion, custBinPath)

/* if versions.tf file found */
Expand All @@ -164,6 +189,22 @@ func installWithListAll(custBinPath *string) {
installOption(listAll, custBinPath)
}

// install latest stable tf version
func installLatestVersion(custBinPath *string) {
tfversion, _ := lib.GetTFLatest(hashiURL)
lib.Install(tfversion, *custBinPath)
}

// install latest - argument (version) must be provided
func installLatestImplicitVersion(requestedVersion string, custBinPath *string, preRelease bool) {
if lib.ValidMinorVersionFormat(requestedVersion) {
tfversion, _ := lib.GetTFLatestImplicit(hashiURL, preRelease, requestedVersion)
lib.Install(tfversion, *custBinPath)
} else {
printInvalidMinorTFVersion()
}
}

// install with provided version as argument
func installVersion(arg string, custBinPath *string) {
if lib.ValidVersionFormat(arg) {
Expand All @@ -187,7 +228,12 @@ func installVersion(arg string, custBinPath *string) {

// Print invalid TF version
func printInvalidTFVersion() {
fmt.Println("Invalid terraform 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")
fmt.Println("Invalid terraform version format. Format should be #.#.# or #.#.#-@# where # are numbers and @ are word characters. For example, 0.11.7 and 0.11.9-beta1 are valid versions")
}

// Print invalid TF version
func printInvalidMinorTFVersion() {
fmt.Println("Invalid minor terraform version format. Format should be #.# where # are numbers. For example, 0.11 is valid version")
}

//retrive file content of regular file
Expand Down Expand Up @@ -295,7 +341,7 @@ func installOption(listAll bool, custBinPath *string) {
os.Exit(0)
}

// installation when
// install when tf file is provided
func installTFProvidedModule(dir string, custBinPath *string) {
tfversion := ""
module, _ := tfconfig.LoadModule(dir)
Expand Down
6 changes: 0 additions & 6 deletions vendor/github.com/manifoldco/promptui/Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion vendor/github.com/spf13/viper/go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 738ff96

Please sign in to comment.