Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: alias command #10

Merged
merged 12 commits into from Mar 5, 2024
13 changes: 13 additions & 0 deletions README.md
Expand Up @@ -78,6 +78,13 @@ tfversion use --latest --pre-release
tfversion use --required
```

### Create and use an alias

```sh
tfversion alias default 1.7.4
tfversion use default
```

### List versions

```sh
Expand All @@ -90,6 +97,12 @@ tfversion list
tfversion list --installed
```

### List aliased versions

```sh
tfversion list --aliases
```

### Uninstall a specific version

```sh
Expand Down
37 changes: 37 additions & 0 deletions cmd/alias.go
@@ -0,0 +1,37 @@
package cmd

import (
"fmt"
"os"

"github.com/fatih/color"
"github.com/spf13/cobra"

"github.com/tfversion/tfversion/pkg/alias"
)

const (
aliasExample = "# Alias a Terraform version\n" +
ChrisTerBeke marked this conversation as resolved.
Show resolved Hide resolved
"tfversion alias default 1.7.4\n" +
"tfversion alias legacy 1.2.4"
)

var (
aliasCmd = &cobra.Command{
Use: "alias",
Short: "Alias a Terraform version",
Example: aliasExample,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
fmt.Println("error: provide an alias name and Terraform version")
fmt.Printf("See %s for help and examples\n", color.CyanString("`tfversion alias -h`"))
os.Exit(1)
}
alias.AliasVersion(args[0], args[1])
},
}
)

func init() {
rootCmd.AddCommand(aliasCmd)
ChrisTerBeke marked this conversation as resolved.
Show resolved Hide resolved
}
39 changes: 21 additions & 18 deletions cmd/list.go
Expand Up @@ -19,36 +19,38 @@ const (
"\n" +
"\n" +
"# List all installed Terraform versions\n" +
"tfversion list --installed"
"tfversion list --installed\n" +
"\n" +
"\n" +
"# List all aliased Terraform versions\n" +
"tfversion list --aliases"
)

var (
installed bool
aliases bool
maxResults int
listCmd = &cobra.Command{
Use: "list",
Short: "Lists all Terraform versions",
Example: listExample,
Run: func(cmd *cobra.Command, args []string) {

var versions []string
if installed {
installedVersions := list.GetInstalledVersions()
limit := min(maxResults, len(installedVersions))
for _, version := range installedVersions[:limit] {
if helpers.IsPreReleaseVersion(version) {
fmt.Println(color.YellowString(version))
} else {
fmt.Println(color.CyanString(version))
}
}
versions = list.GetInstalledVersions()
} else if aliases {
versions = list.GetAliasedVersions()
} else {
availableVersions := list.GetAvailableVersions()
limit := min(maxResults, len(availableVersions))
for _, version := range availableVersions[:limit] {
if helpers.IsPreReleaseVersion(version) {
fmt.Println(color.YellowString(version))
} else {
fmt.Println(color.CyanString(version))
}
versions = list.GetAvailableVersions()
}

limit := min(maxResults, len(versions))
for _, version := range versions[:limit] {
if helpers.IsPreReleaseVersion(version) {
fmt.Println(color.YellowString(version))
} else {
fmt.Println(color.CyanString(version))
}
}
},
Expand All @@ -58,5 +60,6 @@ var (
func init() {
rootCmd.AddCommand(listCmd)
listCmd.Flags().BoolVar(&installed, "installed", false, "list the installed Terraform versions")
listCmd.Flags().BoolVar(&aliases, "aliases", false, "list the aliased Terraform versions")
listCmd.Flags().IntVar(&maxResults, "max-results", 500, "maximum number of versions to list")
}
85 changes: 85 additions & 0 deletions pkg/alias/alias.go
@@ -0,0 +1,85 @@
package alias

import (
"fmt"
"os"
"path/filepath"

"github.com/fatih/color"
"github.com/tfversion/tfversion/pkg/download"
"github.com/tfversion/tfversion/pkg/helpers"
)

// AliasVersion creates a symlink to the specified Terraform version.
func AliasVersion(alias string, version string) {
if !download.IsAlreadyDownloaded(version) {
if helpers.IsPreReleaseVersion(version) {
fmt.Printf("Terraform version %s not found, run %s to install\n", color.YellowString(version), color.CyanString(fmt.Sprintf("`tfversion install %s`", version)))
} else {
fmt.Printf("Terraform version %s not found, run %s to install\n", color.CyanString(version), color.CyanString(fmt.Sprintf("`tfversion install %s`", version)))
}
os.Exit(0)
}

aliasLocation := GetAliasLocation()

// delete existing alias symlink, we consider it non-destructive anyways since you can easily restore it
aliasPath := filepath.Join(aliasLocation, alias)
_, err := os.Lstat(aliasPath)
if err == nil {
err = os.RemoveAll(aliasPath)
if err != nil {
fmt.Printf("error removing symlink: %v\n", err)
os.Exit(1)
}
}

// create the symlink
binaryVersionPath := download.GetInstallLocation(version)
err = os.Symlink(binaryVersionPath, aliasPath)
if err != nil {
fmt.Printf("error creating symlink: %v\n", err)
os.Exit(1)
}

if helpers.IsPreReleaseVersion(version) {
fmt.Printf("Aliased Terraform version %s as %s\n", color.YellowString(version), color.YellowString(alias))
} else {
fmt.Printf("Aliased Terraform version %s as %s\n", color.CyanString(version), color.CyanString(alias))
}
}

// GetAliasLocation returns the directory where tfversion stores the aliases.
func GetAliasLocation() string {
user, err := os.UserHomeDir()
if err != nil {
fmt.Printf("error getting user home directory: %s", err)
os.Exit(1)
}

aliasLocation := filepath.Join(user, download.ApplicationDir, download.AliasesDir)
if _, err := os.Stat(aliasLocation); os.IsNotExist(err) {
err := os.Mkdir(aliasLocation, 0755)
if err != nil {
fmt.Printf("error creating alias directory: %s", err)
os.Exit(1)
}
}

return aliasLocation
}

// IsAlias checks if the given alias is valid.
func IsAlias(alias string) bool {
aliasPath := filepath.Join(GetAliasLocation(), alias)
_, err := os.Stat(aliasPath)
return !os.IsNotExist(err)
}

// GetVersion returns the Terraform version for the given alias.
func GetVersion(alias string) string {
aliasLocation := GetAliasLocation()
resolvePath, _ := filepath.EvalSymlinks(filepath.Join(aliasLocation, alias))
_, targetVersion := filepath.Split(resolvePath)
return targetVersion
}
12 changes: 9 additions & 3 deletions pkg/download/const.go
Expand Up @@ -7,8 +7,14 @@ const (
MaxRetries = 3
// RetryTimeInSeconds is the time to wait before retrying a download.
RetryTimeInSeconds = 2
// DownloadDir is the directory where tfversion downloads Terraform releases.
DownloadDir = ".tfversion"
// ApplicationDir is the directory where tfversion downloads Terraform releases.
ApplicationDir = ".tfversion"
// UseDir is the directory where tfversion puts the symlink to the active version.
UseDir = "bin"
// VersionsDir is the directory where tfversion installs all versions.
VersionsDir = "versions"
// AliasesDir is the directory where tfversion stores the aliases.
AliasesDir = "aliases"
// TerraformBinaryName is the name of the Terraform binary.
TerraformBinaryName = "terraform"
BinaryDir = "bin"
)
25 changes: 13 additions & 12 deletions pkg/download/download.go
Expand Up @@ -24,33 +24,34 @@ func GetDownloadLocation() string {
user, err := os.UserHomeDir()
if err != nil {
fmt.Printf("error getting user home directory: %s", err)
os.Exit(1)
}
downloadLocation := filepath.Join(user, DownloadDir)
ensureDownloadDirectoryExists(downloadLocation)

downloadLocation := filepath.Join(user, ApplicationDir, VersionsDir)
if _, err := os.Stat(downloadLocation); os.IsNotExist(err) {
err := os.Mkdir(downloadLocation, 0755)
if err != nil {
fmt.Printf("error creating download directory: %s", err)
os.Exit(1)
}
}

return downloadLocation
}

// GetInstallLocation returns the directory where a specific Terraform version is installed to.
func GetInstallLocation(version string) string {
return filepath.Join(GetDownloadLocation(), version)
}

// GetBinaryLocation returns the path to the Terraform binary for the given version.
func GetBinaryLocation(version string) string {
return filepath.Join(GetInstallLocation(version), TerraformBinaryName)
}

func ensureDownloadDirectoryExists(downloadLocation string) {
if _, err := os.Stat(downloadLocation); os.IsNotExist(err) {
err := os.Mkdir(downloadLocation, 0755)
if err != nil {
fmt.Printf("error creating download directory: %s", err)
}
}
}

// Download downloads the Terraform release zip file for the given version, OS and architecture.
func Download(version, goos, goarch string) (string, error) {
downloadLocation := GetDownloadLocation()
ensureDownloadDirectoryExists(downloadLocation)

// Construct the download URL based on the version and the OS and architecture.
downloadURL := fmt.Sprintf("%s/%s/terraform_%s_%s_%s.zip", TerraformReleasesUrl, version, version, goos, goarch)
Expand Down
40 changes: 34 additions & 6 deletions pkg/list/list.go
Expand Up @@ -4,30 +4,58 @@ import (
"fmt"
"net/http"
"os"
"path/filepath"
"strings"

"github.com/tfversion/tfversion/pkg/alias"
"github.com/tfversion/tfversion/pkg/download"
"github.com/tfversion/tfversion/pkg/helpers"
"golang.org/x/net/html"
)

// GetInstalledVersions returns the installed Terraform versions from the `~/.tfversion` directory
// GetAliasedVersions returns the aliased Terraform versions from the `~/.tfversion/aliases` directory
func GetAliasedVersions() []string {
aliasLocation := alias.GetAliasLocation()

// find all aliases
aliasedVersions, err := os.ReadDir(aliasLocation)
if err != nil {
fmt.Printf("error listing alias directory: %s", err)
os.Exit(1)
}

// resolve the symlinks to get the target versions
var versionNames []string
for _, v := range aliasedVersions {
resolvePath, _ := filepath.EvalSymlinks(filepath.Join(aliasLocation, v.Name()))
_, targetVersion := filepath.Split(resolvePath)
versionNames = append(versionNames, fmt.Sprintf("%s -> %s", v.Name(), targetVersion))
}

// check if there are any versions
if len(versionNames) == 0 {
fmt.Println("error listing installed versions: no versions found")
os.Exit(1)
}

return versionNames
}

// GetInstalledVersions returns the installed Terraform versions from the `~/.tfversion/versions` directory
func GetInstalledVersions() []string {
installLocation := download.GetDownloadLocation()
installedVersions, err := os.ReadDir(installLocation)
if err != nil {
fmt.Printf("error listing installation directory: %s", err)
fmt.Printf("error listing versions directory: %s", err)
os.Exit(1)
}

var versionNames []string
for _, v := range installedVersions {
if v.Name() != download.BinaryDir {
versionNames = append(versionNames, v.Name())
}
versionNames = append(versionNames, v.Name())
}

// Check if there are any versions
// check if there are any versions
if len(versionNames) == 0 {
fmt.Println("error listing installed versions: no versions found")
os.Exit(1)
Expand Down