Skip to content

Commit

Permalink
Add uninstall command, match Github OS and architecture first against…
Browse files Browse the repository at this point in the history
… aliases (for aliases that contain a substring of an OS name), add `apple-darwin` to OS aliases, strip common file extensions from Github assets if version+extension matching fails
  • Loading branch information
ivanfetch committed Jan 3, 2023
1 parent 9718f35 commit c7d069f
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 7 deletions.
27 changes: 27 additions & 0 deletions cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ func RunCLI(args []string, output, errOutput io.Writer) error {
},
}
rootCmd.AddCommand(versionCmd)

var installCmd = &cobra.Command{
Use: "install <provider>:<source>[:version]",
Short: "Install a command-line tool",
Expand Down Expand Up @@ -84,6 +85,32 @@ Available providers are:
}
rootCmd.AddCommand(installCmd)

var uninstallCmd = &cobra.Command{
Use: "uninstall <tool name>[:version]",
Short: "Uninstall a command-line tool",
Long: `Uninstall a command-line tool managed by JKL.
A tool version must be exact, as shown by: jkl list <tool name>
If no version is specified, all versions of the tool will be uninstalled.`,
Example: ` jkl uninstall rbac-lookup
jkl uninstall rbac-lookup:0.9.0`,
Aliases: []string{"remove", "uninst", "u", "rm"},
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return fmt.Errorf("Please specify which tool JKL should uninstall, using the form: ToolName[:version]\nThe `jkl list` command will show JKL-managed tools. Run %s uninstall -h for more information about uninstallation.", callMeProgName)
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
err := j.Uninstall(args[0])
if err != nil {
return err
}
return nil
},
}
rootCmd.AddCommand(uninstallCmd)

var listCmd = &cobra.Command{
Use: "list [<tool name>]",
Short: "List installed command-line tools or installed versions for a specific tool",
Expand Down
9 changes: 7 additions & 2 deletions github.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ type GithubAsset struct {
URL string `json:"url"`
}

var versionRE *regexp.Regexp = regexp.MustCompile(`(.+?)[-_]?v?\d+.*`)
var versionRE *regexp.Regexp = regexp.MustCompile(`(.+?)[-_]?v?[-_]?\d+.*`)

// NameWithoutVersionAndComponents returns the asset name minus its version
// and any specified components. A component is matched with a preseeding
Expand All @@ -94,9 +94,14 @@ func (g GithubAsset) NameWithoutVersionAndComponents(components ...string) strin
// the name.
withoutVersionMatches := versionRE.FindStringSubmatch(strippedName)
if withoutVersionMatches != nil || len(withoutVersionMatches) >= 2 {
debugLog.Printf("the stripped name after matching a version number is %q", withoutVersionMatches[1])
debugLog.Printf("the stripped name after matching a version number and extension is %q", withoutVersionMatches[1])
return withoutVersionMatches[1]
}
// Attemptto strip a file extension because the above regular expression
// failed.
for _, ext := range []string{".tar.gz", ".tar", ".tgz", ".tar.bz2", ".zip"} {
strippedName = strings.Replace(strippedName, ext, "", -1)
}
debugLog.Printf("the stripped name is %q", strippedName)
return strippedName
}
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ require (
)

require (
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
Expand Down
21 changes: 21 additions & 0 deletions jkl.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,27 @@ func (j JKL) Install(specStr string) (installedVersion string, err error) {
return toolSpec.version, nil
}

// Uninstall uninsalls the specified managedTool. All versions will be
// uninstalled unless a version is specified.
func (j JKL) Uninstall(toolNameAndVersion string) error {
toolFields := strings.Split(toolNameAndVersion, ":")
toolName := toolFields[0]
var toolVersion string
if len(toolFields) == 2 {
toolVersion = toolFields[1]
}
tool := j.getManagedTool(toolName)
if toolVersion == "" {
debugLog.Printf("uninstalling all versions of %s", toolName)
return tool.uninstallAllVersions()
}
err := tool.uninstallVersion(toolVersion)
if err != nil {
return err
}
return nil
}

// CreateShim creates a symbolic link for the specified tool name, pointing to
// the JKL binary.
func (j JKL) createShim(binaryName string) error {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ exec jkl install hashi:terraform:1.0.0
! stderr .
exec terraform version
stdout 'Terraform v1.0.0'
# This version is used below, for uninstallation
exec jkl install hashi:terraform:1.0.2
! stdout .
! stderr .
env JKL_TERRAFORM=1.0.2
exec terraform version
stdout 'Terraform v1.0.2'
# This version requires jkl to fetch jultiple pages of Hashicorp releases
exec jkl install hashicorp:terraform:0.11.15
! stdout .
Expand All @@ -17,3 +24,19 @@ stdout 'Terraform v0.11.15'
! exec jkl install hashicorp:terraform:10.2
! stdout .
stderr 'Error: no version found to match "10.2"'
exec jkl uninstall terraform:1.0.0
! stdout .
! stderr .
exec jkl list terraform
! stdout 1.0.0
# These versions should still be installed
stdout 0.11.15
stdout 1.0.2
exec jkl uninstall terraform
! stdout .
! stderr .
! exec jkl list terraform
! stdout .
stderr 'no such file or directory'
# The shim should have also ben removed
! exists $HOME/.jkl/bin/terraform
67 changes: 66 additions & 1 deletion toolmanaged.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"path/filepath"
"sort"
"strings"

"github.com/hashicorp/go-multierror"
)

// managedTool represents a tool that JKL has already installed.
Expand Down Expand Up @@ -67,7 +69,7 @@ func (t managedTool) path(version string) (installedPath string, versionWasFound
installedPath = fmt.Sprintf("%[1]s/%[2]s/%[3]s/%[2]s", t.jkl.installsDir, t.name, possibleVersion)
_, err = os.Stat(installedPath)
if err == nil {
debugLog.Printf("installed path for %s is %q\n", t.name, installedPath)
debugLog.Printf("found installed path for %s %s: %q\n", t.name, version, installedPath)
return installedPath, true, nil
}
if err != nil && !errors.Is(err, fs.ErrNotExist) {
Expand All @@ -81,6 +83,32 @@ func (t managedTool) path(version string) (installedPath string, versionWasFound
return "", false, fmt.Errorf("unexpected loop fall-through finding the path for %q version %q", t.name, version)
}

// uninstallVersion removes the specified version of the managed tool,
// including it's containing directory which is named after the version.
// No error is returned if the specified version is not found.
func (t managedTool) uninstallVersion(version string) error {
binaryPath, versionFound, err := t.path(version)
if err != nil {
return err
}
if !versionFound {
debugLog.Printf("version %s of %s is not found and cannot be uninstalled", version, t.name)
return nil
}
debugLog.Printf("removing tool binary %s", binaryPath)
err = os.Remove(binaryPath)
if err != nil {
return err
}
parentPath := filepath.Dir(binaryPath)
debugLog.Printf("removing the versioned directory %q", parentPath)
err = os.Remove(parentPath)
if err != nil {
return err
}
return nil
}

// desiredVersion returns the version of the specified tool desired by
// configuration files or an environment variable. IF the version is `latest`, the latest installed version will be returned.
func (t managedTool) desiredVersion() (desiredVersion string, found bool, err error) {
Expand Down Expand Up @@ -140,6 +168,43 @@ func (t managedTool) listInstalledVersions() (versions []string, found bool, err
return sortedVersions, true, nil
}

func (t managedTool) uninstallAllVersions() error {
uninstallErrs := new(multierror.Error)
allVersions, foundAnyVersions, err := t.listInstalledVersions()
if err != nil {
return err
}
if !foundAnyVersions {
debugLog.Printf("no versions of %s are installed, nothing to uninstall", t.name)
return nil
}
for _, ver := range allVersions {
err := t.uninstallVersion(ver)
if err != nil {
debugLog.Printf("error uninstalling %s version %s: %v\n", t.name, ver, err)
uninstallErrs = multierror.Append(uninstallErrs, fmt.Errorf("uninstalling %s %s: %v", t.name, ver, err))
}
}
if len(uninstallErrs.Errors) > 0 {
return uninstallErrs
}
topLevelToolDir := filepath.Join(t.jkl.installsDir, t.name)
debugLog.Printf("removing top-level directory %s\n", topLevelToolDir)
err = os.Remove(topLevelToolDir)
if err != nil {
// Do not return an error if non-jkl-managed files are present, but make
// the condition discoverable if debug logging is enabled.
debugLog.Printf("cannot remove directory %q after having removed %s: %v\n", topLevelToolDir, t.name, err)
}
shim := filepath.Join(t.jkl.shimsDir, t.name)
debugLog.Printf("removing shim %s\n", shim)
err = os.Remove(shim)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("unable to remove shim %q while uninstalling all versions of %s: %v", shim, t.name, err)
}
return nil
}

// latestInstalledVersion returns the latest version number that is installed
// of the specified tool.
func (t managedTool) latestInstalledVersion() (latestVersion string, found bool, err error) {
Expand Down
9 changes: 5 additions & 4 deletions util.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ import (
// true if a match is found, and the original substring that matched.
// Substrings are matched using lower-case.
func stringContainsOneOfLowerCase(s, firstSubstr string, additionalSubstrs ...string) (match string, found bool) {
for _, substr := range append([]string{firstSubstr}, additionalSubstrs...) {
if strings.Contains(strings.ToLower(s), strings.ToLower(substr)) {
return substr, true
allSubStrings := append([]string{firstSubstr}, additionalSubstrs...)
for i := len(allSubStrings) - 1; i >= 0; i-- {
if strings.Contains(strings.ToLower(s), strings.ToLower(allSubStrings[i])) {
return allSubStrings[i], true
}
}
return "", false
Expand Down Expand Up @@ -214,7 +215,7 @@ func getAliasesForArchitecture(arch string) []string {

func getAliasesForOperatingSystem(OS string) []string {
OSAliases := map[string][]string{
"darwin": {"macos", "osx"},
"darwin": {"macos", "osx", "apple-darwin"},
}
return OSAliases[strings.ToLower(OS)]
}
Expand Down

0 comments on commit c7d069f

Please sign in to comment.