From 770ea4c0d4fdc47c146425b5b99bd5c93448e9cd Mon Sep 17 00:00:00 2001 From: Johannes Brunswicker Date: Mon, 22 Apr 2024 13:32:21 +0200 Subject: [PATCH] #154 implement dry-run --- lib/download.go | 25 ++++--- lib/download_test.go | 2 +- lib/files.go | 8 ++- lib/install.go | 98 +++++++++++++++------------- lib/param_parsing/parameters.go | 3 + lib/param_parsing/parameters_test.go | 23 ++++++- lib/symlink.go | 2 +- main.go | 14 ++-- 8 files changed, 106 insertions(+), 69 deletions(-) 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 863ac16b..543e3b1d 100644 --- a/lib/download_test.go +++ b/lib/download_test.go @@ -49,7 +49,7 @@ func TestDownloadFromURL_FileNameMatch(t *testing.T) { urlToDownload := hashiURL + lowestVersion + "/" + installVersion + lowestVersion + macOS expectedFile := filepath.Join(installLocation, installVersion+lowestVersion+macOS) - installedFile, errDownload := downloadFromURL(installLocation, urlToDownload) + installedFile, errDownload := downloadFromURL(installLocation, urlToDownload, nil) if errDownload != nil { t.Logf("Expected file name %v to be downloaded", expectedFile) diff --git a/lib/files.go b/lib/files.go index e8992364..ef77b6eb 100644 --- a/lib/files.go +++ b/lib/files.go @@ -60,16 +60,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 e23098b7..1ebf84b2 100644 --- a/lib/install.go +++ b/lib/install.go @@ -6,7 +6,9 @@ import ( "os" "path/filepath" "runtime" + "strconv" "strings" + "sync" "github.com/hashicorp/go-version" "github.com/mitchellh/go-homedir" @@ -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 a directory in the home location if it does not exist -func getInstallLocation() string { +func GetInstallLocation() string { /* get current user */ homeDir, errCurr := homedir.Dir() if errCurr != nil { @@ -59,6 +61,7 @@ func getInstallLocation() string { // install : install the provided version in the argument func install(tfversion string, binPath 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 @@ -67,7 +70,7 @@ func install(tfversion string, binPath string, mirrorURL string) { binPath = installableBinLocation(binPath) initialize(binPath) //initialize path - installLocation = getInstallLocation() //get installation location - this is where we will put our terraform binary file + installLocation = GetInstallLocation() //get installation location - this is where we will put our terraform binary file goarch := runtime.GOARCH goos := runtime.GOOS @@ -97,7 +100,7 @@ func install(tfversion string, binPath string, mirrorURL string) { CreateSymlink(installFileVersionPath, binPath) logger.Infof("Switched terraform to version %q", tfversion) addRecent(tfversion) //add to recent file for faster lookup - os.Exit(0) + return } //if does not have slash - append slash @@ -121,6 +124,8 @@ func install(tfversion string, binPath string, mirrorURL string) { 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) @@ -139,13 +144,13 @@ func install(tfversion string, binPath string, mirrorURL string) { CreateSymlink(installFileVersionPath, binPath) logger.Infof("Switched terraform to version %q", tfversion) addRecent(tfversion) //add to recent file for faster lookup - os.Exit(0) + return } // addRecent : add to recent file func addRecent(requestedVersion string) { - installLocation = getInstallLocation() //get installation location - this is where we will put our terraform binary file + installLocation = GetInstallLocation() //get installation location - this is where we will put our terraform binary file versionFile := filepath.Join(installLocation, recentFile) fileExist := CheckFileExist(versionFile) @@ -188,7 +193,7 @@ func addRecent(requestedVersion string) { // getRecentVersions : get recent version from file func getRecentVersions() ([]string, error) { - installLocation = getInstallLocation() //get installation location - this is where we will put our terraform binary file + installLocation = GetInstallLocation() //get installation location - this is where we will put our terraform binary file versionFile := filepath.Join(installLocation, recentFile) fileExist := CheckFileExist(versionFile) @@ -226,7 +231,7 @@ func getRecentVersions() ([]string, error) { // CreateRecentFile : create RECENT file func CreateRecentFile(requestedVersion string) { - installLocation = getInstallLocation() //get installation location - this is where we will put our terraform binary file + installLocation = GetInstallLocation() //get installation location - this is where we will put our terraform binary file _ = WriteLines([]string{requestedVersion}, filepath.Join(installLocation, recentFile)) } @@ -288,19 +293,22 @@ func installableBinLocation(userBinPath string) string { } // InstallLatestVersion install latest stable tf version -func InstallLatestVersion(customBinaryPath, mirrorURL string) { +func InstallLatestVersion(dryRun bool, customBinaryPath, mirrorURL string) { + logger.Debugf("Install latest version. Dry run: %s", strconv.FormatBool(dryRun)) tfversion, _ := getTFLatest(mirrorURL) - install(tfversion, customBinaryPath, mirrorURL) + if !dryRun { + install(tfversion, customBinaryPath, mirrorURL) + } } // InstallLatestImplicitVersion install latest - argument (version) must be provided -func InstallLatestImplicitVersion(requestedVersion, customBinaryPath, mirrorURL string, preRelease bool) { +func InstallLatestImplicitVersion(dryRun bool, requestedVersion, customBinaryPath, 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, mirrorURL) } logger.Errorf("Error parsing constraint %q: %v", requestedVersion, err) @@ -308,45 +316,46 @@ func InstallLatestImplicitVersion(requestedVersion, customBinaryPath, mirrorURL } // InstallVersion install with provided version as argument -func InstallVersion(arg, customBinaryPath, mirrorURL string) { - if validVersionFormat(arg) { - requestedVersion := arg - - //check to see if the requested version has been downloaded before - installLocation := getInstallLocation() - 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) //add to recent file for faster lookup - os.Exit(0) - } - - // 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, mirrorURL) +func InstallVersion(dryRun bool, version, customBinaryPath, 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() + 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) //add to recent file for faster lookup + return + } else { + // 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, 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, mirrorURL string) { +func InstallOption(listAll, dryRun bool, customBinaryPath, mirrorURL string) { tflist, _ := getTFList(mirrorURL, listAll) // Get list of versions recentVersions, _ := getRecentVersions() // Get recent versions from RECENT file tflist = append(recentVersions, tflist...) // Append recent versions to the top of the list @@ -374,7 +383,8 @@ func InstallOption(listAll bool, customBinaryPath, mirrorURL string) { logger.Fatalf("Prompt failed %v", errPrompt) } } - - install(tfversion, customBinaryPath, mirrorURL) + if !dryRun { + install(tfversion, customBinaryPath, mirrorURL) + } os.Exit(0) } diff --git a/lib/param_parsing/parameters.go b/lib/param_parsing/parameters.go index e211f922..122cfeb6 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 LatestFlag bool LatestPre string @@ -33,6 +34,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.BoolVarLong(¶ms.LatestFlag, "latest", 'u', "Get latest stable version") getopt.StringVarLong(¶ms.LatestPre, "latest-pre", 'p', "Latest pre-release implicit version. Ex: tfswitch --latest-pre 0.13 downloads 0.13.0-rc1 (latest)") @@ -82,6 +84,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.LatestFlag = false params.LatestPre = lib.DefaultLatest diff --git a/lib/param_parsing/parameters_test.go b/lib/param_parsing/parameters_test.go index b36cdf06..bfedb5d3 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" ) @@ -42,10 +44,9 @@ func TestGetParameters_params_are_overridden_by_toml_file(t *testing.T) { 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() - }) + logger = lib.InitLogger("DEBUG") expected := "../../test-data/integration-tests/test_tfswitchtoml" os.Args = []string{"cmd", "--chdir=" + expected, "--bin=/usr/test/bin"} params := GetParameters() @@ -66,6 +67,22 @@ func TestGetParameters_toml_params_are_overridden_by_cli(t *testing.T) { } } +func TestGetParameters_dry_run_wont_download_anything(t *testing.T) { + logger = lib.InitLogger("DEBUG") + installLocation := lib.GetInstallLocation() + expected := "../../test-data/integration-tests/test_versiontf" + //os.Args = []string{"cmd", "--chdir=" + expected, "--bin=/tmp", "--dry-run"} + os.Args = []string{"cmd", "--log-level=DEBUG", "--chdir=" + expected, "--bin=/tmp"} + params := GetParameters() + 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.MirrorURL) + if lib.FileExistsAndIsNotDir(installFileVersionPath) { + t.Error("Dry run should NOT download any files.") + } +} + func TestGetParameters_check_config_precedence(t *testing.T) { t.Cleanup(func() { getopt.CommandLine = getopt.New() diff --git a/lib/symlink.go b/lib/symlink.go index 91bd9052..4e0da1b4 100644 --- a/lib/symlink.go +++ b/lib/symlink.go @@ -91,7 +91,7 @@ func CheckSymlink(symlinkPath string) bool { // ChangeSymlink : move symlink to existing binary func ChangeSymlink(binVersionPath string, binPath string) { - //installLocation = getInstallLocation() //get installation location - this is where we will put our terraform binary file + //installLocation = GetInstallLocation() //get installation location - this is where we will put our terraform binary file binPath = installableBinLocation(binPath) /* remove current symlink if exist*/ diff --git a/main.go b/main.go index 3d69004c..3aa4ef6a 100644 --- a/main.go +++ b/main.go @@ -43,32 +43,32 @@ func main() { os.Exit(0) case parameters.ListAllFlag: /* show all terraform version including betas and RCs*/ - lib.InstallOption(true, parameters.CustomBinaryPath, parameters.MirrorURL) + lib.InstallOption(true, parameters.DryRun, parameters.CustomBinaryPath, 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.MirrorURL, true) + lib.InstallLatestImplicitVersion(parameters.DryRun, parameters.LatestPre, parameters.CustomBinaryPath, 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.MirrorURL, false) + lib.InstallLatestImplicitVersion(parameters.DryRun, parameters.LatestStable, parameters.CustomBinaryPath, 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.MirrorURL) + lib.InstallLatestVersion(parameters.DryRun, parameters.CustomBinaryPath, parameters.MirrorURL) case parameters.ShowLatestFlag: /* show latest stable version */ lib.ShowLatestVersion(parameters.MirrorURL) case parameters.Version != "": - lib.InstallVersion(parameters.Version, parameters.CustomBinaryPath, parameters.MirrorURL) + lib.InstallVersion(parameters.DryRun, parameters.Version, parameters.CustomBinaryPath, parameters.MirrorURL) case parameters.DefaultVersion != "": /* if default version is provided - Pick this instead of going for prompt */ - lib.InstallVersion(parameters.DefaultVersion, parameters.CustomBinaryPath, parameters.MirrorURL) + lib.InstallVersion(parameters.DryRun, parameters.DefaultVersion, parameters.CustomBinaryPath, parameters.MirrorURL) default: // Set list all false - only official release will be displayed - lib.InstallOption(false, parameters.CustomBinaryPath, parameters.MirrorURL) + lib.InstallOption(false, parameters.DryRun, parameters.CustomBinaryPath, parameters.MirrorURL) } }