Skip to content

Commit

Permalink
RECENT to JSON Proposal (#437)
Browse files Browse the repository at this point in the history
* Convert old RECENT file to JSON Format
* added tests
* updated dependencies
* make sure only the last 5 of the versions are marked with *recent and returned in the list

---------

Co-authored-by: George L. Yermulnik <yz@yz.kiev.ua>
Co-authored-by: Matt John <matthew@dockstudios.co.uk>
  • Loading branch information
3 people committed Jun 4, 2024
1 parent 439cb16 commit d59ef38
Show file tree
Hide file tree
Showing 7 changed files with 265 additions and 112 deletions.
11 changes: 7 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ require (
github.com/gookit/slog v0.5.6
github.com/hashicorp/go-version v1.7.0
github.com/hashicorp/hcl/v2 v2.20.1
github.com/hashicorp/terraform-config-inspect v0.0.0-20231204233900-a34142ec2a72
github.com/hashicorp/terraform-config-inspect v0.0.0-20240509232506-4708120f8f30
github.com/manifoldco/promptui v0.9.0
github.com/mitchellh/go-homedir v1.1.0
github.com/pborman/getopt v1.1.0
github.com/spf13/viper v1.19.0
github.com/stretchr/testify v1.9.0
golang.org/x/crypto v0.23.0
golang.org/x/sys v0.20.0
)
Expand All @@ -19,6 +20,7 @@ require (
github.com/agext/levenshtein v1.2.3 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/chzyer/readline v1.5.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/gookit/color v1.5.4 // indirect
Expand All @@ -29,8 +31,9 @@ require (
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/locafero v0.5.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
Expand All @@ -41,11 +44,11 @@ require (
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/zclconf/go-cty v1.14.4 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/tools v0.20.0 // indirect
golang.org/x/tools v0.21.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
16 changes: 8 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ github.com/hashicorp/hcl v1.0.1-vault-5 h1:kI3hhbbyzr4dldA8UdTb7ZlVVlI2DACdCfz31
github.com/hashicorp/hcl v1.0.1-vault-5/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM=
github.com/hashicorp/hcl/v2 v2.20.1 h1:M6hgdyz7HYt1UN9e61j+qKJBqR3orTWbI1HKBJEdxtc=
github.com/hashicorp/hcl/v2 v2.20.1/go.mod h1:TZDqQ4kNKCbh1iJp99FdPiUaVDDUPivbqxZulxDYqL4=
github.com/hashicorp/terraform-config-inspect v0.0.0-20231204233900-a34142ec2a72 h1:nZ5gGjbe5o7XUu1d7j+Y5Ztcxlp+yaumTKH9i0D3wlg=
github.com/hashicorp/terraform-config-inspect v0.0.0-20231204233900-a34142ec2a72/go.mod h1:l8HcFPm9cQh6Q0KSWoYPiePqMvRFenybP1CH2MjKdlg=
github.com/hashicorp/terraform-config-inspect v0.0.0-20240509232506-4708120f8f30 h1:0qwr2oZy9mIIJMWh7W9NTHLWGMbEF5KEQ+QqM9hym34=
github.com/hashicorp/terraform-config-inspect v0.0.0-20240509232506-4708120f8f30/go.mod h1:Gz/z9Hbn+4KSp8A2FBtNszfLSdT2Tn/uAKGuVqqWmDI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
Expand All @@ -65,8 +65,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/locafero v0.5.0 h1:zXz2JnQDgE5gDg0R9ThkNT0orQzm47i8IuO6hk6XSYY=
github.com/sagikazarmark/locafero v0.5.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
Expand Down Expand Up @@ -102,8 +102,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8=
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc h1:O9NuF4s+E/PvMIy+9IUZB9znFwUIXEWSstNjek6VpVg=
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
Expand All @@ -116,8 +116,8 @@ golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
Expand Down
7 changes: 3 additions & 4 deletions lib/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ var (
PubKeyUri = "https://www.hashicorp.com/.well-known/pgp-key.txt"
)

const (
pubKeySuffix = ".asc"
)

// GetDefaultBin Get default binary path
func GetDefaultBin() string {
var defaultBin = "/usr/local/bin/terraform"
Expand All @@ -27,8 +23,11 @@ func GetDefaultBin() string {
const (
DefaultMirror = "https://releases.hashicorp.com/terraform"
DefaultLatest = ""
distributionTerraform = "terraform"
distributionOpenTofu = "opentofu"
installFile = "terraform"
InstallDir = ".terraform.versions"
pubKeySuffix = ".asc"
recentFile = "RECENT"
tfDarwinArm64StartVersion = "1.0.2"
VersionPrefix = "terraform_"
Expand Down
104 changes: 8 additions & 96 deletions lib/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func install(tfversion string, binPath string, installPath string, mirrorURL str
/* set symlink to desired version */
CreateSymlink(installFileVersionPath, binPath)
logger.Infof("Switched terraform to version %q", tfversion)
addRecent(tfversion, installPath) //add to recent file for faster lookup
addRecent(tfversion, installPath, distributionTerraform) //add to recent file for faster lookup
return
}

Expand Down Expand Up @@ -134,99 +134,11 @@ func install(tfversion string, binPath string, installPath string, mirrorURL str
/* set symlink to desired version */
CreateSymlink(installFileVersionPath, binPath)
logger.Infof("Switched terraform to version %q", tfversion)
addRecent(tfversion, installPath) //add to recent file for faster lookup
addRecent(tfversion, installPath, distributionTerraform) //add to recent file for faster lookup
return
}

// addRecent : add to recent file
func addRecent(requestedVersion string, installPath string) {

installLocation = GetInstallLocation(installPath) //get installation location - this is where we will put our terraform binary file
versionFile := filepath.Join(installLocation, recentFile)

fileExist := CheckFileExist(versionFile)
if fileExist {
lines, errRead := ReadLines(versionFile)

if errRead != nil {
logger.Errorf("Error reading %q file: %v", versionFile, errRead)
return
}

for _, line := range lines {
if !validVersionFormat(line) {
logger.Infof("File %q is dirty (recreating cache file)", versionFile)
RemoveFiles(versionFile)
CreateRecentFile(requestedVersion, installPath)
return
}
}

versionExist := versionExist(requestedVersion, lines)

if !versionExist {
if len(lines) >= 3 {
_, lines = lines[len(lines)-1], lines[:len(lines)-1]

lines = append([]string{requestedVersion}, lines...)
_ = WriteLines(lines, versionFile)
} else {
lines = append([]string{requestedVersion}, lines...)
_ = WriteLines(lines, versionFile)
}
}

} else {
CreateRecentFile(requestedVersion, installPath)
}
}

// getRecentVersions : get recent version from file
func getRecentVersions(installPath string) ([]string, error) {

installLocation = GetInstallLocation(installPath) //get installation location - this is where we will put our terraform binary file
versionFile := filepath.Join(installLocation, recentFile)

fileExist := CheckFileExist(versionFile)
if fileExist {

lines, errRead := ReadLines(versionFile)
var outputRecent []string

if errRead != nil {
logger.Errorf("Error reading %q file: %f", versionFile, errRead)
return nil, errRead
}

for _, line := range lines {
/* checks if versions in the recent file are valid.
If any version is invalid, it will be considered dirty
and the recent file will be removed
*/
if !validVersionFormat(line) {
RemoveFiles(versionFile)
return nil, errRead
}

/* output can be confusing since it displays the 3 most recent used terraform version
append the string *recent to the output to make it more user friendly
*/
outputRecent = append(outputRecent, fmt.Sprintf("%s *recent", line))
}

return outputRecent, nil
}

return nil, nil
}

// CreateRecentFile : create RECENT file
func CreateRecentFile(requestedVersion string, installPath string) {
installLocation = GetInstallLocation(installPath) //get installation location - this is where we will put our terraform binary file
_ = WriteLines([]string{requestedVersion}, filepath.Join(installLocation, recentFile))
}

// ConvertExecutableExt : convert excutable with local OS extension
// ConvertExecutableExt : convert executable with local OS extension
func ConvertExecutableExt(fpath string) string {
switch runtime.GOOS {
case "windows":
Expand Down Expand Up @@ -314,7 +226,7 @@ func InstallVersion(dryRun bool, version, customBinaryPath, installPath, mirrorU
if recentDownloadFile {
ChangeSymlink(installFileVersionPath, customBinaryPath)
logger.Infof("Switched terraform to version %q", requestedVersion)
addRecent(requestedVersion, installPath) //add to recent file for faster lookup
addRecent(requestedVersion, installPath, distributionTerraform) //add to recent file for faster lookup
return
}

Expand All @@ -341,10 +253,10 @@ func InstallVersion(dryRun bool, version, customBinaryPath, installPath, mirrorU
/* listAll = true - all versions including beta and rc will be displayed */
/* listAll = false - only official stable release are displayed */
func InstallOption(listAll, dryRun bool, customBinaryPath, installPath string, mirrorURL string) {
tflist, _ := getTFList(mirrorURL, listAll) // Get list of versions
recentVersions, _ := getRecentVersions(installPath) // Get recent versions from RECENT file
tflist = append(recentVersions, tflist...) // Append recent versions to the top of the list
tflist = removeDuplicateVersions(tflist) // Remove duplicate version
tflist, _ := getTFList(mirrorURL, listAll) // Get list of versions
recentVersions, _ := getRecentVersions(installPath, distributionTerraform) // Get recent versions from RECENT file
tflist = append(recentVersions, tflist...) // Append recent versions to the top of the list
tflist = removeDuplicateVersions(tflist) // Remove duplicate version

if len(tflist) == 0 {
logger.Fatalf("Terraform version list is empty: %s", mirrorURL)
Expand Down
111 changes: 111 additions & 0 deletions lib/recent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package lib

import (
"encoding/json"
"os"
"path/filepath"
"strings"
)

type RecentFiles struct {
Terraform []string `json:"terraform"`
OpenTofu []string `json:"opentofu"`
}

func addRecent(requestedVersion string, installPath string, distribution string) {
if !validVersionFormat(requestedVersion) {
logger.Errorf("The version %q is not a valid version string and won't be stored", requestedVersion)
return
}
installLocation := GetInstallLocation(installPath)
recentFilePath := filepath.Join(installLocation, recentFile)
var recentFileData RecentFiles
if CheckFileExist(recentFilePath) {
unmarshalRecentFileData(recentFilePath, &recentFileData)
}
prependRecentVersionToList(requestedVersion, distribution, &recentFileData)
saveRecentFile(recentFileData, recentFilePath)
}

func prependRecentVersionToList(version, distribution string, r *RecentFiles) {
var sliceToCheck []string
switch distribution {
case distributionTerraform:
sliceToCheck = r.Terraform
case distributionOpenTofu:
sliceToCheck = r.OpenTofu
}
for versionIndex, versionValue := range sliceToCheck {
if versionValue == version {
sliceToCheck = append(sliceToCheck[:versionIndex], sliceToCheck[versionIndex+1:]...)
}
}
sliceToCheck = append([]string{version}, sliceToCheck...)

switch distribution {
case distributionTerraform:
r.Terraform = sliceToCheck
case distributionOpenTofu:
r.OpenTofu = sliceToCheck
}
}

func getRecentVersions(installPath string, dist string) ([]string, error) {
installLocation := GetInstallLocation(installPath)
recentFilePath := filepath.Join(installLocation, recentFile)
var recentFileData RecentFiles
unmarshalRecentFileData(recentFilePath, &recentFileData)
var listOfRecentVersions []string
switch dist {
case distributionTerraform:
listOfRecentVersions = recentFileData.Terraform
case distributionOpenTofu:
listOfRecentVersions = recentFileData.OpenTofu
}
var maxCount int
if len(listOfRecentVersions) >= 5 {
maxCount = 5
} else {
maxCount = len(listOfRecentVersions)
}
var returnedRecentVersions []string
for i := 0; i < maxCount; i++ {
returnedRecentVersions = append(returnedRecentVersions, listOfRecentVersions[i]+" *recent")
}
return returnedRecentVersions, nil
}

func unmarshalRecentFileData(recentFilePath string, recentFileData *RecentFiles) {
recentFileContent, err := os.ReadFile(recentFilePath)
if err != nil {
logger.Errorf("Could not open recent versions file %q", recentFilePath)
}
if string(recentFileContent[0:1]) != "{" {
convertOldRecentFile(recentFileContent, recentFileData)
} else {
err = json.Unmarshal(recentFileContent, &recentFileData)
if err != nil {
logger.Errorf("Could not unmarshal recent versions content from %q file", recentFilePath)
}
}
}

func convertOldRecentFile(content []byte, recentFileData *RecentFiles) {
lines := strings.Split(string(content), "\n")
for _, s := range lines {
if s != "" {
recentFileData.Terraform = append(recentFileData.Terraform, s)
}
}
}

func saveRecentFile(data RecentFiles, path string) {
bytes, err := json.Marshal(data)
if err != nil {
logger.Errorf("Could not marshal data to JSON: %v", err)
}
err = os.WriteFile(path, bytes, 0644)
if err != nil {
logger.Errorf("Could not save file %q: %v", path, err)
}
}
Loading

0 comments on commit d59ef38

Please sign in to comment.