diff --git a/lib/download.go b/lib/download.go index a03a6b55..eedc080b 100644 --- a/lib/download.go +++ b/lib/download.go @@ -7,37 +7,40 @@ import ( "os" "path/filepath" "strings" + "sync" ) // DownloadFromURL : Downloads the terraform binary and its hash from the source url -func DownloadFromURL(installLocation string, mirrorURL string, tfversion string, versionPrefix string, goos string, goarch string) (string, error) { +func DownloadFromURL(installLocation, mirrorURL, tfversion, versionPrefix, goos, goarch string) (string, error) { + var wg sync.WaitGroup + defer wg.Done() pubKeyFilename := filepath.Join(installLocation, "/", PubKeyPrefix+PubKeyId+pubKeySuffix) zipUrl := mirrorURL + tfversion + "/" + versionPrefix + tfversion + "_" + goos + "_" + goarch + ".zip" hashUrl := mirrorURL + tfversion + "/" + versionPrefix + tfversion + "_SHA256SUMS" hashSignatureUrl := mirrorURL + tfversion + "/" + versionPrefix + tfversion + "_SHA256SUMS." + PubKeyId + ".sig" - err := downloadPublicKey(installLocation, pubKeyFilename) + err := downloadPublicKey(installLocation, pubKeyFilename, &wg) if err != nil { logger.Error("Could not download public PGP key file.") return "", err } logger.Infof("Downloading %q", zipUrl) - zipFilePath, err := downloadFromURL(installLocation, zipUrl) + zipFilePath, err := downloadFromURL(installLocation, zipUrl, &wg) if err != nil { logger.Error("Could not download zip file.") return "", err } logger.Infof("Downloading %q", hashUrl) - hashFilePath, err := downloadFromURL(installLocation, hashUrl) + hashFilePath, err := downloadFromURL(installLocation, hashUrl, &wg) if err != nil { logger.Error("Could not download hash file.") return "", err } logger.Infof("Downloading %q", hashSignatureUrl) - hashSigFilePath, err := downloadFromURL(installLocation, hashSignatureUrl) + hashSigFilePath, err := downloadFromURL(installLocation, hashSignatureUrl, &wg) if err != nil { logger.Error("Could not download hash signature file.") return "", err @@ -70,7 +73,7 @@ func DownloadFromURL(installLocation string, mirrorURL string, tfversion string, var filesToCleanup []string filesToCleanup = append(filesToCleanup, hashFilePath) filesToCleanup = append(filesToCleanup, hashSigFilePath) - defer cleanup(filesToCleanup) + defer cleanup(filesToCleanup, &wg) verified := checkSignatureOfChecksums(publicKeyFile, hashFile, signatureFile) if !verified { @@ -83,7 +86,8 @@ func DownloadFromURL(installLocation string, mirrorURL string, tfversion string, return zipFilePath, err } -func downloadFromURL(installLocation string, url string) (string, error) { +func downloadFromURL(installLocation string, url string, wg *sync.WaitGroup) (string, error) { + wg.Add(1) tokens := strings.Split(url, "/") fileName := tokens[len(tokens)-1] logger.Infof("Downloading to %q", filepath.Join(installLocation, "/", fileName)) @@ -119,12 +123,12 @@ func downloadFromURL(installLocation string, url string) (string, error) { return filePath, nil } -func downloadPublicKey(installLocation string, targetFileName string) error { +func downloadPublicKey(installLocation string, targetFileName string, wg *sync.WaitGroup) error { logger.Debugf("Looking up public key file at %q", targetFileName) publicKeyFileExists := FileExistsAndIsNotDir(targetFileName) if !publicKeyFileExists { // Public key does not exist. Let's grab it from hashicorp - pubKeyFile, errDl := downloadFromURL(installLocation, PubKeyUri) + pubKeyFile, errDl := downloadFromURL(installLocation, PubKeyUri, wg) if errDl != nil { logger.Errorf("Error fetching public key file from %s", PubKeyUri) return errDl @@ -138,8 +142,9 @@ func downloadPublicKey(installLocation string, targetFileName string) error { return nil } -func cleanup(paths []string) { +func cleanup(paths []string, wg *sync.WaitGroup) { for _, path := range paths { + wg.Add(1) logger.Infof("Deleting %q", path) err := os.Remove(path) if err != nil { diff --git a/lib/download_test.go b/lib/download_test.go index 65ec448f..519917eb 100644 --- a/lib/download_test.go +++ b/lib/download_test.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "runtime" + "sync" "testing" "github.com/mitchellh/go-homedir" @@ -13,7 +14,7 @@ import ( // TestDownloadFromURL_FileNameMatch : Check expected filename exist when downloaded func TestDownloadFromURL_FileNameMatch(t *testing.T) { - + logger = InitLogger("DEBUG") hashiURL := "https://releases.hashicorp.com/terraform/" installVersion := "terraform_" tempDir := t.TempDir() @@ -45,10 +46,11 @@ func TestDownloadFromURL_FileNameMatch(t *testing.T) { /* test download old terraform version */ lowestVersion := "0.11.0" - + var wg sync.WaitGroup + defer wg.Done() urlToDownload := hashiURL + lowestVersion + "/" + installVersion + lowestVersion + macOS expectedFile := filepath.Join(installLocation, installVersion+lowestVersion+macOS) - installedFile, errDownload := downloadFromURL(installLocation, urlToDownload) + installedFile, errDownload := downloadFromURL(installLocation, urlToDownload, &wg) if errDownload != nil { t.Logf("Expected file name %v to be downloaded", expectedFile) diff --git a/lib/files.go b/lib/files.go index c5bcb381..88da02f4 100644 --- a/lib/files.go +++ b/lib/files.go @@ -59,16 +59,18 @@ func Unzip(src string, dest string) ([]string, error) { if err != nil { logger.Fatalf("Could not open destination: %v", err) } - var wg sync.WaitGroup + var unzipWaitGroup sync.WaitGroup for _, f := range reader.File { - wg.Add(1) - unzipErr := unzipFile(f, destination, &wg) + unzipWaitGroup.Add(1) + unzipErr := unzipFile(f, destination, &unzipWaitGroup) if unzipErr != nil { logger.Fatalf("Error unzipping %v", unzipErr) } else { filenames = append(filenames, filepath.Join(destination, f.Name)) } } + logger.Debug("Waiting for deferred functions.") + unzipWaitGroup.Wait() return filenames, nil } diff --git a/lib/install.go b/lib/install.go index 76daa2d3..3155ba4d 100644 --- a/lib/install.go +++ b/lib/install.go @@ -5,7 +5,9 @@ import ( "os" "path/filepath" "runtime" + "strconv" "strings" + "sync" "github.com/manifoldco/promptui" @@ -37,9 +39,9 @@ func initialize(binPath string) { } } -// getInstallLocation : get location where the terraform binary will be installed, +// GetInstallLocation : get location where the terraform binary will be installed, // will create the installDir if it does not exist -func getInstallLocation(installPath string) string { +func GetInstallLocation(installPath string) string { /* set installation location */ installLocation = filepath.Join(installPath, InstallDir) @@ -50,6 +52,7 @@ func getInstallLocation(installPath string) string { // install : install the provided version in the argument func install(tfversion string, binPath string, installPath string, mirrorURL string) { + var wg sync.WaitGroup /* Check to see if user has permission to the default bin location which is "/usr/local/bin/terraform" * If user does not have permission to default bin location, proceed to create $HOME/bin and install the tfswitch there * Inform user that they don't have permission to default location, therefore tfswitch was installed in $HOME/bin @@ -58,7 +61,7 @@ func install(tfversion string, binPath string, installPath string, mirrorURL str binPath = installableBinLocation(binPath) initialize(binPath) //initialize path - installLocation = getInstallLocation(installPath) //get installation location - this is where we will put our terraform binary file + installLocation = GetInstallLocation(installPath) //get installation location - this is where we will put our terraform binary file goarch := runtime.GOARCH goos := runtime.GOOS @@ -88,7 +91,7 @@ func install(tfversion string, binPath string, installPath string, mirrorURL str CreateSymlink(installFileVersionPath, binPath) logger.Infof("Switched terraform to version %q", tfversion) addRecent(tfversion, installPath) //add to recent file for faster lookup - os.Exit(0) + return } //if does not have slash - append slash @@ -112,6 +115,8 @@ func install(tfversion string, binPath string, installPath string, mirrorURL str logger.Fatalf("Unable to unzip %q file: %v", zipFile, errUnzip) } + logger.Debug("Waiting for deferred functions.") + wg.Wait() /* rename unzipped file to terraform version name - terraform_x.x.x */ installFilePath := ConvertExecutableExt(filepath.Join(installLocation, installFile)) RenameFile(installFilePath, installFileVersionPath) @@ -130,13 +135,13 @@ func install(tfversion string, binPath string, installPath string, mirrorURL str CreateSymlink(installFileVersionPath, binPath) logger.Infof("Switched terraform to version %q", tfversion) addRecent(tfversion, installPath) //add to recent file for faster lookup - os.Exit(0) + 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 + installLocation = GetInstallLocation(installPath) //get installation location - this is where we will put our terraform binary file versionFile := filepath.Join(installLocation, recentFile) fileExist := CheckFileExist(versionFile) @@ -179,7 +184,7 @@ func addRecent(requestedVersion string, installPath string) { // 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 + installLocation = GetInstallLocation(installPath) //get installation location - this is where we will put our terraform binary file versionFile := filepath.Join(installLocation, recentFile) fileExist := CheckFileExist(versionFile) @@ -217,7 +222,7 @@ func getRecentVersions(installPath string) ([]string, error) { // 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 + installLocation = GetInstallLocation(installPath) //get installation location - this is where we will put our terraform binary file _ = WriteLines([]string{requestedVersion}, filepath.Join(installLocation, recentFile)) } @@ -274,19 +279,21 @@ func installableBinLocation(userBinPath string) string { } // InstallLatestVersion install latest stable tf version -func InstallLatestVersion(customBinaryPath, installPath string, mirrorURL string) { +func InstallLatestVersion(dryRun bool, customBinaryPath, installPath string, mirrorURL string) { tfversion, _ := getTFLatest(mirrorURL) - install(tfversion, customBinaryPath, installPath, mirrorURL) + if !dryRun { + install(tfversion, customBinaryPath, installPath, mirrorURL) + } } // InstallLatestImplicitVersion install latest - argument (version) must be provided -func InstallLatestImplicitVersion(requestedVersion, customBinaryPath, installPath string, mirrorURL string, preRelease bool) { +func InstallLatestImplicitVersion(dryRun bool, requestedVersion, customBinaryPath, installPath string, mirrorURL string, preRelease bool) { _, err := version.NewConstraint(requestedVersion) if err != nil { logger.Errorf("Error parsing constraint %q: %v", requestedVersion, err) } tfversion, err := getTFLatestImplicit(mirrorURL, preRelease, requestedVersion) - if err == nil && tfversion != "" { + if err == nil && tfversion != "" && !dryRun { install(tfversion, customBinaryPath, installPath, mirrorURL) } logger.Errorf("Error parsing constraint %q: %v", requestedVersion, err) @@ -294,45 +301,46 @@ func InstallLatestImplicitVersion(requestedVersion, customBinaryPath, installPat } // InstallVersion install with provided version as argument -func InstallVersion(arg, customBinaryPath, installPath string, mirrorURL string) { - if validVersionFormat(arg) { - requestedVersion := arg - - //check to see if the requested version has been downloaded before - installLocation := getInstallLocation(installPath) - installFileVersionPath := ConvertExecutableExt(filepath.Join(installLocation, VersionPrefix+requestedVersion)) - recentDownloadFile := CheckFileExist(installFileVersionPath) - if recentDownloadFile { - ChangeSymlink(installFileVersionPath, customBinaryPath) - logger.Infof("Switched terraform to version %q", requestedVersion) - addRecent(requestedVersion, installPath) //add to recent file for faster lookup - os.Exit(0) - } +func InstallVersion(dryRun bool, version, customBinaryPath, installPath, mirrorURL string) { + logger.Debugf("Install version %s. Dry run: %s", version, strconv.FormatBool(dryRun)) + if !dryRun { + if validVersionFormat(version) { + requestedVersion := version + + //check to see if the requested version has been downloaded before + installLocation := GetInstallLocation(installPath) + installFileVersionPath := ConvertExecutableExt(filepath.Join(installLocation, VersionPrefix+requestedVersion)) + recentDownloadFile := CheckFileExist(installFileVersionPath) + if recentDownloadFile { + ChangeSymlink(installFileVersionPath, customBinaryPath) + logger.Infof("Switched terraform to version %q", requestedVersion) + addRecent(requestedVersion, installPath) //add to recent file for faster lookup + return + } - // If the requested version had not been downloaded before - // Set list all true - all versions including beta and rc will be displayed - tflist, _ := getTFList(mirrorURL, true) // Get list of versions - exist := versionExist(requestedVersion, tflist) // Check if version exists before downloading it + // If the requested version had not been downloaded before + // Set list all true - all versions including beta and rc will be displayed + tflist, _ := getTFList(mirrorURL, true) // Get list of versions + exist := versionExist(requestedVersion, tflist) // Check if version exists before downloading it - if exist { - install(requestedVersion, customBinaryPath, installPath, mirrorURL) + if exist { + install(requestedVersion, customBinaryPath, installPath, mirrorURL) + } else { + logger.Fatal("The provided terraform version does not exist.\n Try `tfswitch -l` to see all available versions") + } } else { - logger.Fatal("The provided terraform version does not exist.\n Try `tfswitch -l` to see all available versions") + PrintInvalidTFVersion() + logger.Error("Args must be a valid terraform version") + UsageMessage() os.Exit(1) } - - } else { - PrintInvalidTFVersion() - logger.Error("Args must be a valid terraform version") - UsageMessage() - os.Exit(1) } } // InstallOption displays & installs tf version /* listAll = true - all versions including beta and rc will be displayed */ /* listAll = false - only official stable release are displayed */ -func InstallOption(listAll bool, customBinaryPath, installPath string, mirrorURL string) { +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 @@ -360,7 +368,8 @@ func InstallOption(listAll bool, customBinaryPath, installPath string, mirrorURL logger.Fatalf("Prompt failed %v", errPrompt) } } - - install(tfversion, customBinaryPath, installPath, mirrorURL) + if !dryRun { + install(tfversion, customBinaryPath, installPath, mirrorURL) + } os.Exit(0) } diff --git a/lib/param_parsing/parameters.go b/lib/param_parsing/parameters.go index 1369c196..dc02437b 100644 --- a/lib/param_parsing/parameters.go +++ b/lib/param_parsing/parameters.go @@ -10,6 +10,7 @@ type Params struct { ChDirPath string CustomBinaryPath string DefaultVersion string + DryRun bool HelpFlag bool InstallPath string LatestFlag bool @@ -34,6 +35,7 @@ func GetParameters() Params { getopt.StringVarLong(¶ms.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(¶ms.CustomBinaryPath, "bin", 'b', "Custom binary path. Ex: tfswitch -b "+lib.ConvertExecutableExt("/Users/username/bin/terraform")) getopt.StringVarLong(¶ms.DefaultVersion, "default", 'd', "Default to this version in case no other versions could be detected. Ex: tfswitch --default 1.2.4") + getopt.BoolVarLong(¶ms.DryRun, "dry-run", 'r', "Only show what tfswitch would do. Don't download anything.") getopt.BoolVarLong(¶ms.HelpFlag, "help", 'h', "Displays help message") getopt.StringVarLong(¶ms.InstallPath, "install", 'i', "Custom install path. Ex: tfswitch -i /Users/username. The binaries will be in the sub installDir directory e.g. /Users/username/"+lib.InstallDir) getopt.BoolVarLong(¶ms.LatestFlag, "latest", 'u', "Get latest stable version") @@ -90,6 +92,7 @@ func initParams(params Params) Params { params.ChDirPath = lib.GetCurrentDirectory() params.CustomBinaryPath = lib.ConvertExecutableExt(lib.GetDefaultBin()) params.DefaultVersion = lib.DefaultLatest + params.DryRun = false params.HelpFlag = false params.InstallPath = lib.GetHomeDirectory() params.LatestFlag = false diff --git a/lib/param_parsing/parameters_test.go b/lib/param_parsing/parameters_test.go index 8539539e..79ce8fc4 100644 --- a/lib/param_parsing/parameters_test.go +++ b/lib/param_parsing/parameters_test.go @@ -2,7 +2,9 @@ package param_parsing import ( "github.com/pborman/getopt" + "github.com/warrensbox/terraform-switcher/lib" "os" + "path/filepath" "testing" ) @@ -41,11 +43,13 @@ func TestGetParameters_params_are_overridden_by_toml_file(t *testing.T) { if actual != expected { t.Error("Version Param was not as expected. Actual: " + actual + ", Expected: " + expected) } -} -func TestGetParameters_toml_params_are_overridden_by_cli(t *testing.T) { t.Cleanup(func() { getopt.CommandLine = getopt.New() }) +} + +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"} params := GetParameters() @@ -64,6 +68,27 @@ func TestGetParameters_toml_params_are_overridden_by_cli(t *testing.T) { if actual != expected { t.Error("Version Param was not as expected. Actual: " + actual + ", Expected: " + expected) } + t.Cleanup(func() { + getopt.CommandLine = getopt.New() + }) +} + +func TestGetParameters_dry_run_wont_download_anything(t *testing.T) { + logger = lib.InitLogger("DEBUG") + expected := "../../test-data/integration-tests/test_versiontf" + os.Args = []string{"cmd", "--chdir=" + expected, "--bin=/tmp", "--dry-run"} + params := GetParameters() + installLocation := lib.GetInstallLocation(params.InstallPath) + installFileVersionPath := lib.ConvertExecutableExt(filepath.Join(installLocation, lib.VersionPrefix+params.Version)) + // Make sure the file tfswitch WOULD download is absent + _ = os.Remove(installFileVersionPath) + lib.InstallVersion(params.DryRun, params.Version, params.CustomBinaryPath, params.InstallPath, params.MirrorURL) + if lib.FileExistsAndIsNotDir(installFileVersionPath) { + t.Error("Dry run should NOT download any files.") + } + t.Cleanup(func() { + getopt.CommandLine = getopt.New() + }) } func TestGetParameters_check_config_precedence(t *testing.T) { @@ -76,4 +101,7 @@ func TestGetParameters_check_config_precedence(t *testing.T) { if parameters.Version != expected { t.Error("Version Param was not as expected. Actual: " + parameters.Version + ", Expected: " + expected) } + t.Cleanup(func() { + getopt.CommandLine = getopt.New() + }) } diff --git a/main.go b/main.go index fd004d23..7b7e7a46 100644 --- a/main.go +++ b/main.go @@ -44,32 +44,32 @@ func main() { os.Exit(0) case parameters.ListAllFlag: /* show all terraform version including betas and RCs*/ - lib.InstallOption(true, parameters.CustomBinaryPath, parameters.InstallPath, parameters.MirrorURL) + lib.InstallOption(true, parameters.DryRun, parameters.CustomBinaryPath, parameters.InstallPath, parameters.MirrorURL) case parameters.LatestPre != "": /* latest pre-release implicit version. Ex: tfswitch --latest-pre 0.13 downloads 0.13.0-rc1 (latest) */ - lib.InstallLatestImplicitVersion(parameters.LatestPre, parameters.CustomBinaryPath, parameters.InstallPath, parameters.MirrorURL, true) + lib.InstallLatestImplicitVersion(parameters.DryRun, parameters.LatestPre, parameters.CustomBinaryPath, parameters.InstallPath, parameters.MirrorURL, true) case parameters.ShowLatestPre != "": /* show latest pre-release implicit version. Ex: tfswitch --latest-pre 0.13 downloads 0.13.0-rc1 (latest) */ lib.ShowLatestImplicitVersion(parameters.ShowLatestPre, parameters.MirrorURL, true) case parameters.LatestStable != "": /* latest implicit version. Ex: tfswitch --latest-stable 0.13 downloads 0.13.5 (latest) */ - lib.InstallLatestImplicitVersion(parameters.LatestStable, parameters.CustomBinaryPath, parameters.InstallPath, parameters.MirrorURL, false) + lib.InstallLatestImplicitVersion(parameters.DryRun, parameters.LatestStable, parameters.CustomBinaryPath, parameters.InstallPath, parameters.MirrorURL, false) case parameters.ShowLatestStable != "": /* show latest implicit stable version. Ex: tfswitch --show-latest-stable 0.13 downloads 0.13.5 (latest) */ lib.ShowLatestImplicitVersion(parameters.ShowLatestStable, parameters.MirrorURL, false) case parameters.LatestFlag: /* latest stable version */ - lib.InstallLatestVersion(parameters.CustomBinaryPath, parameters.InstallPath, parameters.MirrorURL) + lib.InstallLatestVersion(parameters.DryRun, parameters.CustomBinaryPath, parameters.InstallPath, parameters.MirrorURL) case parameters.ShowLatestFlag: /* show latest stable version */ lib.ShowLatestVersion(parameters.MirrorURL) case parameters.Version != "": - lib.InstallVersion(parameters.Version, parameters.CustomBinaryPath, parameters.InstallPath, parameters.MirrorURL) + lib.InstallVersion(parameters.DryRun, parameters.Version, parameters.CustomBinaryPath, parameters.InstallPath, parameters.MirrorURL) case parameters.DefaultVersion != "": /* if default version is provided - Pick this instead of going for prompt */ - lib.InstallVersion(parameters.DefaultVersion, parameters.CustomBinaryPath, parameters.InstallPath, parameters.MirrorURL) + lib.InstallVersion(parameters.DryRun, parameters.DefaultVersion, parameters.CustomBinaryPath, parameters.InstallPath, parameters.MirrorURL) default: // Set list all false - only official release will be displayed - lib.InstallOption(false, parameters.CustomBinaryPath, parameters.InstallPath, parameters.MirrorURL) + lib.InstallOption(false, parameters.DryRun, parameters.CustomBinaryPath, parameters.InstallPath, parameters.MirrorURL) } }