Skip to content

Commit

Permalink
feat: Allow to download binary with custom CPU arch (#532)
Browse files Browse the repository at this point in the history
* feat: Allow to download binary with custom CPU arch

Fixes #318

Caveats:
- Doesn't override if binary had already been downloaded before
  - Remove existing binary from `tfswitch` download dir manually (see
    `--install` option)
- Spits out Warn log message if requested arch doesn't match actual
- Doesn't check arch value validity and just passes it to download
  function directly, which will fail if download file doesn't exist on
  remote
  • Loading branch information
yermulnik authored Jan 22, 2025
1 parent 65c3f53 commit 235ea4e
Show file tree
Hide file tree
Showing 9 changed files with 134 additions and 51 deletions.
65 changes: 34 additions & 31 deletions lib/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ import (
"github.com/hashicorp/go-version"
)

var (
installLocation = "/tmp"
)
var installLocation = "/tmp"

// initialize : removes existing symlink to terraform binary based on provided binPath
func initialize(binPath string) {
Expand Down Expand Up @@ -50,10 +48,10 @@ func GetInstallLocation(installPath string) string {
}

// install : install the provided version in the argument
func install(product Product, tfversion string, binPath string, installPath string, mirrorURL string) error {
func install(product Product, tfversion, binPath, installPath, mirrorURL, goarch string) error {
var wg sync.WaitGroup

//check to see if the requested version has been downloaded before
// check to see if the requested version has been downloaded before
installLocation := GetInstallLocation(installPath)
installFileVersionPath := ConvertExecutableExt(filepath.Join(installLocation, product.GetVersionPrefix()+tfversion))
recentDownloadFile := CheckFileExist(installFileVersionPath)
Expand All @@ -70,7 +68,10 @@ func install(product Product, tfversion string, binPath string, installPath stri
return fmt.Errorf("the provided %s version does not exist: %q.\n Try `tfswitch -l` to see all available versions", product.GetId(), tfversion)
}

goarch := runtime.GOARCH
if goarch != runtime.GOARCH {
logger.Warnf("Installing for %q architecture on %q!", goarch, runtime.GOARCH)
}

goos := runtime.GOOS

// Terraform darwin arm64 comes with 1.0.2 and next version
Expand Down Expand Up @@ -114,7 +115,9 @@ func switchToVersion(product Product, tfversion string, binPath string, installP
}

logger.Infof("Switched %s to version %q", product.GetName(), tfversion)
addRecent(tfversion, installPath, product) //add to recent file for faster lookup

// add to recent file for faster lookup
addRecent(tfversion, installPath, product)
return nil
}

Expand All @@ -135,24 +138,24 @@ func ConvertExecutableExt(fpath string) string {
// If not, create $HOME/bin. Ask users to add $HOME/bin to $PATH and return $HOME/bin as install location
// Deprecated: This function has been deprecated and will be removed in v2.0.0
func installableBinLocation(product Product, userBinPath string) string {
homedir := GetHomeDirectory() //get user's home directory
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
homedir := GetHomeDirectory() // get user's home directory
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 { //if bin path exist - check if we can write to it
if binPathExist { // if bin path exist - check if we can write to it

binPathWritable := false //assume bin path is not writable
binPathWritable := false // assume bin path is not writable
if runtime.GOOS != "windows" {
binPathWritable = CheckDirWritable(binDir) //check if is writable on ( only works on LINUX)
binPathWritable = CheckDirWritable(binDir) // check if is writable on ( only works on LINUX)
}

// IF: "/usr/local/bin" or `custom bin path` provided by user is non-writable, (binPathWritable == false), we will attempt to install terraform at the ~/bin location. See ELSE
if !binPathWritable {
homeBinDir := filepath.Join(homedir, "bin")
if !CheckDirExist(homeBinDir) { //if ~/bin exist, install at ~/bin/terraform
if !CheckDirExist(homeBinDir) { // if ~/bin exist, install at ~/bin/terraform
logger.Noticef("Unable to write to %q", userBinPath)
logger.Infof("Creating bin directory at %q", homeBinDir)
createDirIfNotExist(homeBinDir) //create ~/bin
createDirIfNotExist(homeBinDir) // create ~/bin
logger.Warnf("Run `export PATH=\"$PATH:%s\"` to append bin to $PATH", homeBinDir)
}
logger.Infof("Installing %s at %q", product.GetName(), homeBinDir)
Expand All @@ -171,38 +174,38 @@ func installableBinLocation(product Product, userBinPath string) string {
// InstallLatestVersion install latest stable tf version
//
// Deprecated: This function has been deprecated in favor of InstallLatestProductVersion and will be removed in v2.0.0
func InstallLatestVersion(dryRun bool, customBinaryPath, installPath string, mirrorURL string) {
func InstallLatestVersion(dryRun bool, customBinaryPath, installPath, mirrorURL, arch string) {
product := getLegacyProduct()
InstallLatestProductVersion(product, dryRun, customBinaryPath, installPath, mirrorURL)
InstallLatestProductVersion(product, dryRun, customBinaryPath, installPath, mirrorURL, arch)
}

// InstallLatestProductVersion install latest stable tf version
func InstallLatestProductVersion(product Product, dryRun bool, customBinaryPath, installPath string, mirrorURL string) error {
func InstallLatestProductVersion(product Product, dryRun bool, customBinaryPath, installPath, mirrorURL, arch string) error {
tfversion, _ := getTFLatest(mirrorURL)
if !dryRun {
return install(product, tfversion, customBinaryPath, installPath, mirrorURL)
return install(product, tfversion, customBinaryPath, installPath, mirrorURL, arch)
}
return nil
}

// InstallLatestImplicitVersion install latest - argument (version) must be provided
//
// Deprecated: This function has been deprecated in favor of InstallLatestProductImplicitVersion and will be removed in v2.0.0
func InstallLatestImplicitVersion(dryRun bool, requestedVersion, customBinaryPath, installPath string, mirrorURL string, preRelease bool) {
func InstallLatestImplicitVersion(dryRun bool, requestedVersion, customBinaryPath, installPath, mirrorURL, arch string, preRelease bool) {
product := getLegacyProduct()
InstallLatestProductImplicitVersion(product, dryRun, requestedVersion, customBinaryPath, installPath, mirrorURL, preRelease)
InstallLatestProductImplicitVersion(product, dryRun, requestedVersion, customBinaryPath, installPath, mirrorURL, arch, preRelease)
}

// InstallLatestProductImplicitVersion install latest - argument (version) must be provided
func InstallLatestProductImplicitVersion(product Product, dryRun bool, requestedVersion, customBinaryPath, installPath string, mirrorURL string, preRelease bool) error {
func InstallLatestProductImplicitVersion(product Product, dryRun bool, requestedVersion, customBinaryPath, installPath, mirrorURL, arch string, preRelease bool) error {
_, err := version.NewConstraint(requestedVersion)
if err != nil {
// @TODO Should this return an error?
logger.Errorf("Error parsing constraint %q: %v", requestedVersion, err)
}
tfversion, err := getTFLatestImplicit(mirrorURL, preRelease, requestedVersion)
if err == nil && tfversion != "" && !dryRun {
install(product, tfversion, customBinaryPath, installPath, mirrorURL)
install(product, tfversion, customBinaryPath, installPath, mirrorURL, arch)
return nil
}
PrintInvalidMinorTFVersion()
Expand All @@ -212,18 +215,18 @@ func InstallLatestProductImplicitVersion(product Product, dryRun bool, requested
// InstallVersion install Terraform product
//
// Deprecated: This function has been deprecated in favor of InstallProductVersion and will be removed in v2.0.0
func InstallVersion(dryRun bool, version, customBinaryPath, installPath, mirrorURL string) {
func InstallVersion(dryRun bool, version, customBinaryPath, installPath, mirrorURL, arch string) {
product := getLegacyProduct()
InstallProductVersion(product, dryRun, version, customBinaryPath, installPath, mirrorURL)
InstallProductVersion(product, dryRun, version, customBinaryPath, installPath, mirrorURL, arch)
}

// InstallProductVersion install with provided version as argument
func InstallProductVersion(product Product, dryRun bool, version, customBinaryPath, installPath, mirrorURL string) error {
func InstallProductVersion(product Product, dryRun bool, version, customBinaryPath, installPath, mirrorURL, arch string) error {
logger.Debugf("Install version %s. Dry run: %s", version, strconv.FormatBool(dryRun))
if !dryRun {
if validVersionFormat(version) {
requestedVersion := version
return install(product, requestedVersion, customBinaryPath, installPath, mirrorURL)
return install(product, requestedVersion, customBinaryPath, installPath, mirrorURL, arch)
} else {
PrintInvalidTFVersion()
UsageMessage()
Expand All @@ -238,9 +241,9 @@ func InstallProductVersion(product Product, dryRun bool, version, customBinaryPa
// listAll = false - only official stable release are displayed */
//
// Deprecated: This function has been deprecated in favor of InstallProductOption and will be removed in v2.0.0
func InstallOption(listAll, dryRun bool, customBinaryPath, installPath string, mirrorURL string) {
func InstallOption(listAll, dryRun bool, customBinaryPath, installPath, mirrorURL, arch string) {
product := getLegacyProduct()
InstallProductOption(product, listAll, dryRun, customBinaryPath, installPath, mirrorURL)
InstallProductOption(product, listAll, dryRun, customBinaryPath, installPath, mirrorURL, arch)
}

type VersionSelector struct {
Expand All @@ -251,7 +254,7 @@ type VersionSelector struct {
// InstallProductOption displays & installs tf version
/* listAll = true - all versions including beta and rc will be displayed */
/* listAll = false - only official stable release are displayed */
func InstallProductOption(product Product, listAll, dryRun bool, customBinaryPath, installPath string, mirrorURL string) error {
func InstallProductOption(product Product, listAll, dryRun bool, customBinaryPath, installPath, mirrorURL, arch string) error {
var selectVersions []VersionSelector

var versionMap map[string]bool = make(map[string]bool)
Expand Down Expand Up @@ -306,7 +309,7 @@ func InstallProductOption(product Product, listAll, dryRun bool, customBinaryPat
}
}
if !dryRun {
return install(product, selectVersions[selectedItx].Version, customBinaryPath, installPath, mirrorURL)
return install(product, selectVersions[selectedItx].Version, customBinaryPath, installPath, mirrorURL, arch)
}
return nil
}
4 changes: 4 additions & 0 deletions lib/param_parsing/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ package param_parsing
import "os"

func GetParamsFromEnvironment(params Params) Params {
if envArch := os.Getenv("TF_ARCH"); envArch != "" {
params.Arch = envArch
logger.Debugf("Using architecture from environment variable \"TF_ARCH\": %q", envArch)
}
if envVersion := os.Getenv("TF_VERSION"); envVersion != "" {
params.Version = envVersion
logger.Debugf("Using version from environment variable \"TF_VERSION\": %q", envVersion)
Expand Down
13 changes: 13 additions & 0 deletions lib/param_parsing/environment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ import (
"github.com/warrensbox/terraform-switcher/lib"
)

func TestGetParamsFromEnvironment_arch_from_env(t *testing.T) {
logger = lib.InitLogger("DEBUG")
var params Params
expected := "amd64_from_env"
_ = os.Setenv("TF_ARCH", expected)
params = initParams(params)
params = GetParamsFromEnvironment(params)
_ = os.Unsetenv("TF_ARCH")
if params.Arch != expected {
t.Error("Determined arch is not matching. Got " + params.Arch + ", expected " + expected)
}
}

func TestGetParamsFromEnvironment_version_from_env(t *testing.T) {
logger = lib.InitLogger("DEBUG")
var params Params
Expand Down
3 changes: 3 additions & 0 deletions lib/param_parsing/parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
)

type Params struct {
Arch string
ChDirPath string
CustomBinaryPath string
DefaultVersion string
Expand Down Expand Up @@ -51,6 +52,7 @@ func populateParams(params Params) Params {
defaultMirrors = append(defaultMirrors, fmt.Sprintf("%s: %s", product.GetName(), product.GetDefaultMirrorUrl()))
}

getopt.StringVarLong(&params.Arch, "arch", 'A', fmt.Sprintf("Override CPU architecture type for downloaded binary. Ex: `tfswitch --arch amd64` will attempt to download the amd64 version of the binary. Default: %s", runtime.GOARCH))
getopt.StringVarLong(&params.ChDirPath, "chdir", 'c', "Switch to a different working directory before executing the given command. Ex: tfswitch --chdir terraform_project will run tfswitch in the terraform_project directory")
getopt.StringVarLong(&params.CustomBinaryPath, "bin", 'b', "Custom binary path. Ex: tfswitch -b "+lib.ConvertExecutableExt("/Users/username/bin/terraform"))
getopt.StringVarLong(&params.DefaultVersion, "default", 'd', "Default to this version in case no other versions could be detected. Ex: tfswitch --default 1.2.4")
Expand Down Expand Up @@ -160,6 +162,7 @@ func populateParams(params Params) Params {
}

func initParams(params Params) Params {
params.Arch = runtime.GOARCH
params.ChDirPath = lib.GetCurrentDirectory()
params.CustomBinaryPath = ""
params.DefaultVersion = lib.DefaultLatest
Expand Down
31 changes: 28 additions & 3 deletions lib/param_parsing/parameters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ import (
"github.com/warrensbox/terraform-switcher/lib"
)

func TestGetParameters_arch_from_args(t *testing.T) {
expected := "arch_from_args"
os.Args = []string{"cmd", "--arch=" + expected}
params := GetParameters()
actual := params.Arch
if actual != expected {
t.Error("Arch Param was not parsed correctly. Actual: " + actual + ", Expected: " + expected)
}
t.Cleanup(func() {
getopt.CommandLine = getopt.New()
})
}

func TestGetParameters_version_from_args(t *testing.T) {
expected := "0.13args"
os.Args = []string{"cmd", expected}
Expand Down Expand Up @@ -45,6 +58,12 @@ func TestGetParameters_params_are_overridden_by_toml_file(t *testing.T) {
t.Error("CustomBinaryPath Param was not as expected. Actual: " + actual + ", Expected: " + expected)
}

expected = "amd64"
actual = params.Arch
if actual != expected {
t.Error("Arch Param was not as expected. Actual: " + actual + ", Expected: " + expected)
}

expected = "1.6.2"
actual = params.Version
if actual != expected {
Expand All @@ -64,7 +83,7 @@ func TestGetParameters_params_are_overridden_by_toml_file(t *testing.T) {
func TestGetParameters_toml_params_are_overridden_by_cli(t *testing.T) {
logger = lib.InitLogger("DEBUG")
expected := "../../test-data/integration-tests/test_tfswitchtoml"
os.Args = []string{"cmd", "--chdir=" + expected, "--bin=/usr/test/bin", "--product=terraform", "1.6.0"}
os.Args = []string{"cmd", "--chdir=" + expected, "--bin=/usr/test/bin", "--product=terraform", "--arch=arch_from_args", "1.6.0"}
params := Params{}
params = initParams(params)
params.TomlDir = expected
Expand Down Expand Up @@ -93,6 +112,12 @@ func TestGetParameters_toml_params_are_overridden_by_cli(t *testing.T) {
t.Error("Product Param was not as expected. Actual: " + actual + ", Expected: " + expected)
}

expected = "arch_from_args"
actual = params.Arch
if actual != expected {
t.Error("Arch Param was not as expected. Actual: " + actual + ", Expected: " + expected)
}

t.Cleanup(func() {
getopt.CommandLine = getopt.New()
})
Expand Down Expand Up @@ -137,7 +162,7 @@ func TestGetParameters_dry_run_wont_download_anything(t *testing.T) {
installFileVersionPath := lib.ConvertExecutableExt(filepath.Join(installLocation, product.GetVersionPrefix()+params.Version))
// Make sure the file tfswitch WOULD download is absent
_ = os.Remove(installFileVersionPath)
lib.InstallProductVersion(product, params.DryRun, params.Version, params.CustomBinaryPath, params.InstallPath, params.MirrorURL)
lib.InstallProductVersion(product, params.DryRun, params.Version, params.CustomBinaryPath, params.InstallPath, params.MirrorURL, params.Arch)
if lib.FileExistsAndIsNotDir(installFileVersionPath) {
t.Error("Dry run should NOT download any files.")
}
Expand All @@ -148,7 +173,7 @@ func TestGetParameters_dry_run_wont_download_anything(t *testing.T) {

func writeTestFile(t *testing.T, basePath string, fileName string, fileContent string) {
fullPath := filepath.Join(basePath, fileName)
if err := os.WriteFile(fullPath, []byte(fileContent), 0600); err != nil {
if err := os.WriteFile(fullPath, []byte(fileContent), 0o600); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
Expand Down
4 changes: 4 additions & 0 deletions lib/param_parsing/toml.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ func getParamsTOML(params Params) (Params, error) {
return params, errs
}

if viperParser.Get("arch") != nil {
params.Arch = os.ExpandEnv(viperParser.GetString("arch"))
logger.Debugf("Using \"arch\" from %q: %q", tomlPath, params.Arch)
}
if viperParser.Get("bin") != nil {
params.CustomBinaryPath = os.ExpandEnv(viperParser.GetString("bin"))
logger.Debugf("Using \"bin\" from %q: %q", tomlPath, params.CustomBinaryPath)
Expand Down
Loading

0 comments on commit 235ea4e

Please sign in to comment.