├── .gitignore ├── demo └── demo.gif ├── go.mod ├── lib ├── install_test.go ├── download.go ├── command_test.go ├── common_test.go ├── command.go ├── symlink.go ├── list_versions_test.go ├── symlink_test.go ├── download_test.go ├── files.go ├── install.go ├── files_test.go └── list_versions.go ├── CONTRIBUTING.md ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── build.yml ├── LICENSE ├── README.md ├── go.sum ├── CODE_OF_CONDUCT.md ├── modal └── modal.go └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | helmswitch* 2 | helm-* -------------------------------------------------------------------------------- /demo/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tokiwong/helm-switcher/HEAD/demo/demo.gif -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tokiwong/helm-switcher 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/manifoldco/promptui v0.7.0 7 | github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3 8 | ) 9 | -------------------------------------------------------------------------------- /lib/install_test.go: -------------------------------------------------------------------------------- 1 | package lib_test 2 | 3 | import ( 4 | "os/user" 5 | "testing" 6 | ) 7 | 8 | // TestAddRecent : Create a file, check filename exist, 9 | // rename file, check new filename exit 10 | func TestInstall(t *testing.T) { 11 | 12 | t.Run("User should exist", 13 | func(t *testing.T) { 14 | usr, errCurr := user.Current() 15 | if errCurr != nil { 16 | t.Errorf("Unable to get user %v [unexpected]", errCurr) 17 | } 18 | 19 | if usr != nil { 20 | t.Logf("Current user exist: %v [expected]\n", usr.HomeDir) 21 | } else { 22 | t.Error("Unable to get user [unexpected]") 23 | } 24 | }, 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## File a GitHub issue 2 | 3 | Before starting on any work, file a GitHub issue on the repo. This is a good way to get feedback from maintainers! 4 | 5 | ## File a Pull Request 6 | 7 | Pull requests are always welcome, but please make sure to include the following: 8 | 9 | - A description of the change, and a link to the corresponding GitHub issue 10 | - Output of tests run 11 | - Any notes on backwards compatibility 12 | 13 | ## Merge and release 14 | 15 | The maintainers for the repo will review the code and provide feedback. If everything checks out, they will merge the code and release a new version, available on the releases page. 16 | 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Feature Request]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Environment(please complete the following information):** 27 | - OS: [e.g. Ubuntu, MacOS] 28 | - Version [e.g. 22] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ develop ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, macOS-latest] 17 | steps: 18 | 19 | - name: Set up Go 1.13 20 | uses: actions/setup-go@v1 21 | with: 22 | go-version: 1.13 23 | id: go 24 | 25 | - name: Check out code into the Go module directory 26 | uses: actions/checkout@v2 27 | 28 | - name: Build 29 | run: go build -o helmswitch 30 | 31 | - name: Upload 32 | uses: actions/upload-artifact@v1 33 | with: 34 | name: helmswitch-${{ matrix.os }} 35 | path: ./helmswitch 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /lib/download.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | // DownloadFromURL : Downloads the binary from the source url 12 | func DownloadFromURL(installLocation string, url string) (string, error) { 13 | 14 | tokens := strings.Split(url, "/") 15 | fileName := tokens[len(tokens)-1] 16 | fmt.Println("Downloading", url, "to", fileName) 17 | fmt.Println("Downloading ...") 18 | 19 | output, err := os.Create(installLocation + fileName) 20 | if err != nil { 21 | fmt.Println("Error while creating", installLocation+fileName, "-", err) 22 | return "", err 23 | } 24 | defer output.Close() 25 | 26 | response, err := http.Get(url) 27 | if err != nil { 28 | fmt.Println("Error while downloading", url, "-", err) 29 | return "", err 30 | } 31 | defer response.Body.Close() 32 | 33 | n, errCopy := io.Copy(output, response.Body) 34 | if errCopy != nil { 35 | fmt.Println("Error while downloading", url, "-", errCopy) 36 | return "", errCopy 37 | } 38 | 39 | fmt.Println(n, "bytes downloaded.") 40 | return installLocation + fileName, nil 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alvin Wong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # helm-switcher 2 | 3 | ![Build](https://github.com/tokiwong/helm-switcher/workflows/Build/badge.svg) 4 | 5 | `helmswitch` is a CLI tool to install and switch between different versions of Helm 1, 2 or 3. Once installed, just run the command and use the dropdown to choose the desired version of Helm. 6 | 7 | Available for Linux and MacOS 8 | 9 | ## Why 10 | 11 | Helm is the Kubernetes Package Manager, and Helm 2 will be deprecated at the end of 2020. This tool is meant to help teams have an easier time transitioning between helm 2 and 3. 12 | 13 | 14 | ## Prerequisites 15 | 16 | - Go 1.14 17 | 18 | ## Installation 19 | 20 | - Linux and MacOS Binaries are available as assets in [Releases](https://github.com/tokiwong/helm-switcher/releases) 21 | - `chmod +x` 22 | - Put the binary in your PATH 23 | 24 | ### Homebrew 25 | 26 | MacOS installations via Homebrew will place `helmswitch` into `/usr/local/bin` 27 | ``` 28 | brew install tokiwong/tap/helm-switcher 29 | ``` 30 | 31 | ## Installing from source 32 | 33 | - `go build -o helmswitch` 34 | - `./helmswitch` 35 | 36 | Or just `go run main.go` 37 | 38 | ## How-to 39 | - `helmswitch` to open the menu and select the desired version, navigable with arrow keys 40 | - `helmswitch {{ version_number }}` to download the desired version 41 | - Example: `helmswitch 3.1.1` switches to Helm v3.1.1 42 | 43 | ![helmswitch demo](demo/demo.gif) 44 | -------------------------------------------------------------------------------- /lib/command_test.go: -------------------------------------------------------------------------------- 1 | package lib_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/tokiwong/helm-switcher/lib" 8 | ) 9 | 10 | // TestNewCommand : pass value and check if returned value is a pointer 11 | func TestNewCommand(t *testing.T) { 12 | 13 | testCmd := "helm" 14 | cmd := lib.NewCommand(testCmd) 15 | 16 | if reflect.ValueOf(cmd).Kind() == reflect.Ptr { 17 | t.Logf("Value returned is a pointer %v [expected]", cmd) 18 | } else { 19 | t.Errorf("Value returned is not a pointer %v [expected", cmd) 20 | } 21 | } 22 | 23 | // TestPathList : check if bin path exist 24 | func TestPathList(t *testing.T) { 25 | 26 | testCmd := "" 27 | cmd := lib.NewCommand(testCmd) 28 | listBin := cmd.PathList() 29 | 30 | if listBin == nil { 31 | t.Error("No bin path found [unexpected]") 32 | } else { 33 | t.Logf("Found bin path [expected]") 34 | } 35 | } 36 | 37 | type Command struct { 38 | name string 39 | } 40 | 41 | // TestFind : check common "cd" command exist 42 | // This is assuming that Windows and linux has the "cd" command 43 | func TestFind(t *testing.T) { 44 | 45 | testCmd := "cd" 46 | cmd := lib.NewCommand(testCmd) 47 | 48 | next := cmd.Find() 49 | for path := next(); len(path) > 0; path = next() { 50 | if path != "" { 51 | t.Logf("Found installation path: %v [expected]\n", path) 52 | } else { 53 | t.Errorf("Unable to find '%v' command in this operating system [unexpected]", testCmd) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/common_test.go: -------------------------------------------------------------------------------- 1 | package lib_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | func checkFileExist(file string) bool { 11 | _, err := os.Stat(file) 12 | if err != nil { 13 | return false 14 | } 15 | return true 16 | } 17 | 18 | func createFile(path string) { 19 | // detect if file exists 20 | var _, err = os.Stat(path) 21 | 22 | // create file if not exists 23 | if os.IsNotExist(err) { 24 | file, err := os.Create(path) 25 | if err != nil { 26 | fmt.Printf("%v", err) 27 | return 28 | } 29 | defer file.Close() 30 | } 31 | 32 | fmt.Println("==> done creating file", path) 33 | } 34 | 35 | func createDirIfNotExist(dir string) { 36 | if _, err := os.Stat(dir); os.IsNotExist(err) { 37 | log.Printf("Creating directory for helm: %v", dir) 38 | err = os.MkdirAll(dir, 0755) 39 | if err != nil { 40 | fmt.Printf("Unable to create directory for helm: %v", dir) 41 | panic(err) 42 | } 43 | } 44 | } 45 | 46 | func cleanUp(path string) { 47 | removeContents(path) 48 | removeFiles(path) 49 | } 50 | 51 | func removeFiles(src string) { 52 | files, err := filepath.Glob(src) 53 | if err != nil { 54 | 55 | panic(err) 56 | } 57 | for _, f := range files { 58 | if err := os.Remove(f); err != nil { 59 | panic(err) 60 | } 61 | } 62 | } 63 | 64 | func removeContents(dir string) error { 65 | d, err := os.Open(dir) 66 | if err != nil { 67 | return err 68 | } 69 | defer d.Close() 70 | names, err := d.Readdirnames(-1) 71 | if err != nil { 72 | return err 73 | } 74 | for _, name := range names { 75 | err = os.RemoveAll(filepath.Join(dir, name)) 76 | if err != nil { 77 | return err 78 | } 79 | } 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /lib/command.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "runtime" 8 | "strings" 9 | ) 10 | 11 | // Command : type string 12 | type Command struct { 13 | name string 14 | } 15 | 16 | // NewCommand : get command 17 | func NewCommand(name string) *Command { 18 | return &Command{name: name} 19 | } 20 | 21 | // PathList : get bin path list 22 | func (cmd *Command) PathList() []string { 23 | path := os.Getenv("PATH") 24 | return strings.Split(path, string(os.PathListSeparator)) 25 | } 26 | 27 | func isDir(path string) bool { 28 | fileInfo, err := os.Stat(path) 29 | if err != nil || os.IsNotExist(err) { 30 | return false 31 | } 32 | return fileInfo.IsDir() 33 | } 34 | 35 | func isExecutable(path string) bool { 36 | if isDir(path) { 37 | return false 38 | } 39 | 40 | fileInfo, err := os.Stat(path) 41 | if err != nil || os.IsNotExist(err) { 42 | return false 43 | } 44 | 45 | if runtime.GOOS == "windows" { 46 | return true 47 | } 48 | 49 | if fileInfo.Mode()&0111 != 0 { 50 | return true 51 | } 52 | 53 | return false 54 | } 55 | 56 | // Find : find all bin path 57 | func (cmd *Command) Find() func() string { 58 | pathChan := make(chan string) 59 | go func() { 60 | for _, p := range cmd.PathList() { 61 | if !isDir(p) { 62 | continue 63 | } 64 | fileList, err := ioutil.ReadDir(p) 65 | if err != nil { 66 | continue 67 | } 68 | 69 | for _, f := range fileList { 70 | path := filepath.Join(p, f.Name()) 71 | if isExecutable(path) && f.Name() == cmd.name { 72 | pathChan <- path 73 | } 74 | } 75 | } 76 | pathChan <- "" 77 | }() 78 | 79 | return func() string { 80 | return <-pathChan 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/symlink.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | //CreateSymlink : create symlink 9 | //CreateSymlink : create symlink 10 | func CreateSymlink(cwd string, dir string) { 11 | 12 | err := os.Symlink(cwd, dir) 13 | if err != nil { 14 | log.Fatalf(` 15 | Unable to create new symlink. 16 | Maybe symlink already exist. Try removing existing symlink manually. 17 | Try running "unlink" to remove existing symlink. 18 | If error persist, you may not have the permission to create a symlink at %s. 19 | Error: %s 20 | `, dir, err) 21 | os.Exit(1) 22 | } 23 | } 24 | 25 | //RemoveSymlink : remove symlink 26 | func RemoveSymlink(symlinkPath string) { 27 | 28 | _, err := os.Lstat(symlinkPath) 29 | if err != nil { 30 | log.Fatalf(` 31 | Unable to remove symlink. 32 | Maybe symlink already exist. Try removing existing symlink manually. 33 | Try running "unlink" to remove existing symlink. 34 | If error persist, you may not have the permission to create a symlink at %s. 35 | Error: %s 36 | `, symlinkPath, err) 37 | os.Exit(1) 38 | } else { 39 | errRemove := os.Remove(symlinkPath) 40 | if errRemove != nil { 41 | log.Fatalf(` 42 | Unable to remove symlink. 43 | Maybe symlink already exist. Try removing existing symlink manually. 44 | Try running "unlink" to remove existing symlink. 45 | If error persist, you may not have the permission to create a symlink at %s. 46 | Error: %s 47 | `, symlinkPath, errRemove) 48 | os.Exit(1) 49 | } 50 | } 51 | } 52 | 53 | // CheckSymlink : check file is symlink 54 | func CheckSymlink(symlinkPath string) bool { 55 | 56 | fi, err := os.Lstat(symlinkPath) 57 | if err != nil { 58 | return false 59 | } 60 | 61 | if fi.Mode()&os.ModeSymlink != 0 { 62 | return true 63 | } 64 | 65 | return false 66 | } 67 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 2 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= 3 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 4 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 5 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 6 | github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= 7 | github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= 8 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 9 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 10 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 11 | github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw= 12 | github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= 13 | github.com/manifoldco/promptui v0.7.0 h1:3l11YT8tm9MnwGFQ4kETwkzpAwY2Jt9lCrumCUW4+z4= 14 | github.com/manifoldco/promptui v0.7.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ= 15 | github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= 16 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 17 | github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= 18 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 19 | github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3 h1:YtFkrqsMEj7YqpIhRteVxJxCeC3jJBieuLr0d4C4rSA= 20 | github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= 21 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= 22 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 23 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 24 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 25 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b h1:MQE+LT/ABUuuvEZ+YQAMSXindAdUh7slEmAkup74op4= 26 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 27 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 28 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 29 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 30 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 31 | -------------------------------------------------------------------------------- /lib/list_versions_test.go: -------------------------------------------------------------------------------- 1 | package lib_test 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | 7 | "github.com/tokiwong/helm-switcher/lib" 8 | ) 9 | 10 | const ( 11 | helmURL = "https://api.github.com/repos/helm/helm/releases" 12 | ) 13 | 14 | //TestRemoveDuplicateVersions : test to removed duplicate 15 | func TestRemoveDuplicateVersions(t *testing.T) { 16 | 17 | test_array := []string{"0.0.1", "0.0.2", "0.0.3", "0.0.1"} 18 | 19 | list := lib.RemoveDuplicateVersions(test_array) 20 | 21 | if len(list) == len(test_array) { 22 | log.Fatalf("Not able to remove duplicate: %s\n", test_array) 23 | } else { 24 | t.Log("Write versions exist (expected)") 25 | } 26 | } 27 | 28 | //TestValidVersionFormat : test if func returns valid version format 29 | // more regex testing at https://rubular.com/r/UvWXui7EU2icSb 30 | func TestValidVersionFormat(t *testing.T) { 31 | 32 | var version string 33 | version = "0.11.8" 34 | 35 | valid := lib.ValidVersionFormat(version) 36 | 37 | if valid == true { 38 | t.Logf("Valid version format : %s (expected)", version) 39 | } else { 40 | log.Fatalf("Failed to verify version format: %s\n", version) 41 | } 42 | 43 | version = "1.11.9" 44 | 45 | valid = lib.ValidVersionFormat(version) 46 | 47 | if valid == true { 48 | t.Logf("Valid version format : %s (expected)", version) 49 | } else { 50 | log.Fatalf("Failed to verify version format: %s\n", version) 51 | } 52 | 53 | version = "1.11.a" 54 | 55 | valid = lib.ValidVersionFormat(version) 56 | 57 | if valid == false { 58 | t.Logf("Invalid version format : %s (expected)", version) 59 | } else { 60 | log.Fatalf("Failed to verify version format: %s\n", version) 61 | } 62 | 63 | version = "22323" 64 | 65 | valid = lib.ValidVersionFormat(version) 66 | 67 | if valid == false { 68 | t.Logf("Invalid version format : %s (expected)", version) 69 | } else { 70 | log.Fatalf("Failed to verify version format: %s\n", version) 71 | } 72 | 73 | version = "@^&*!)!" 74 | 75 | valid = lib.ValidVersionFormat(version) 76 | 77 | if valid == false { 78 | t.Logf("Invalid version format : %s (expected)", version) 79 | } else { 80 | log.Fatalf("Failed to verify version format: %s\n", version) 81 | } 82 | 83 | version = "1.11.9-beta1" 84 | 85 | valid = lib.ValidVersionFormat(version) 86 | 87 | if valid == true { 88 | t.Logf("Valid version format : %s (expected)", version) 89 | } else { 90 | log.Fatalf("Failed to verify version format: %s\n", version) 91 | } 92 | 93 | version = "0.12.0-rc2" 94 | 95 | valid = lib.ValidVersionFormat(version) 96 | 97 | if valid == true { 98 | t.Logf("Valid version format : %s (expected)", version) 99 | } else { 100 | log.Fatalf("Failed to verify version format: %s\n", version) 101 | } 102 | 103 | version = "1.11.4-boom" 104 | 105 | valid = lib.ValidVersionFormat(version) 106 | 107 | if valid == true { 108 | t.Logf("Valid version format : %s (expected)", version) 109 | } else { 110 | log.Fatalf("Failed to verify version format: %s\n", version) 111 | } 112 | 113 | version = "1.11.4-1" 114 | 115 | valid = lib.ValidVersionFormat(version) 116 | 117 | if valid == false { 118 | t.Logf("Invalid version format : %s (expected)", version) 119 | } else { 120 | log.Fatalf("Failed to verify version format: %s\n", version) 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /lib/symlink_test.go: -------------------------------------------------------------------------------- 1 | package lib_test 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "os/user" 7 | "testing" 8 | 9 | "github.com/tokiwong/helm-switcher/lib" 10 | ) 11 | 12 | // TestCreateSymlink : check if symlink exist-remove if exist, 13 | // create symlink, check if symlink exist, remove symlink 14 | func TestCreateSymlink(t *testing.T) { 15 | 16 | testSymlinkSrc := "/test-helmswitch-src" 17 | 18 | testSymlinkDest := "/test-helmswitch-dest" 19 | 20 | usr, errCurr := user.Current() 21 | if errCurr != nil { 22 | log.Fatal(errCurr) 23 | } 24 | symlinkPathSrc := usr.HomeDir + testSymlinkSrc 25 | symlinkPathDest := usr.HomeDir + testSymlinkDest 26 | 27 | ln, _ := os.Readlink(symlinkPathSrc) 28 | 29 | if ln != symlinkPathDest { 30 | t.Logf("Symlink does not exist %v [expected]", ln) 31 | } else { 32 | t.Logf("Symlink exist %v [expected]", ln) 33 | os.Remove(symlinkPathSrc) 34 | t.Logf("Removed existing symlink for testing purposes") 35 | } 36 | 37 | lib.CreateSymlink(symlinkPathDest, symlinkPathSrc) 38 | 39 | lnCheck, _ := os.Readlink(symlinkPathSrc) 40 | if lnCheck == symlinkPathDest { 41 | t.Logf("Symlink exist %v [expected]", lnCheck) 42 | } else { 43 | t.Logf("Symlink does not exist %v [unexpected]", lnCheck) 44 | t.Error("Symlink was not created") 45 | } 46 | 47 | os.Remove(symlinkPathSrc) 48 | } 49 | 50 | // TestRemoveSymlink : check if symlink exist-create if does not exist, 51 | // remove symlink, check if symlink exist 52 | func TestRemoveSymlink(t *testing.T) { 53 | 54 | testSymlinkSrc := "/test-helmswitch-src" 55 | 56 | testSymlinkDest := "/test-helmswitch-dest" 57 | 58 | usr, errCurr := user.Current() 59 | if errCurr != nil { 60 | log.Fatal(errCurr) 61 | } 62 | symlinkPathSrc := usr.HomeDir + testSymlinkSrc 63 | symlinkPathDest := usr.HomeDir + testSymlinkDest 64 | 65 | ln, _ := os.Readlink(symlinkPathSrc) 66 | 67 | if ln != symlinkPathDest { 68 | t.Logf("Symlink does exist %v [expected]", ln) 69 | t.Log("Creating symlink") 70 | if err := os.Symlink(symlinkPathDest, symlinkPathSrc); err != nil { 71 | t.Error(err) 72 | } 73 | } 74 | 75 | lib.RemoveSymlink(symlinkPathSrc) 76 | 77 | lnCheck, _ := os.Readlink(symlinkPathSrc) 78 | if lnCheck == symlinkPathDest { 79 | t.Logf("Symlink should not exist %v [unexpected]", lnCheck) 80 | t.Error("Symlink was not removed") 81 | } else { 82 | t.Logf("Symlink was removed %v [expected]", lnCheck) 83 | } 84 | } 85 | 86 | // TestCheckSymlink : Create symlink, test if file is symlink 87 | func TestCheckSymlink(t *testing.T) { 88 | 89 | testSymlinkSrc := "/test-helmswitcher-src" 90 | 91 | testSymlinkDest := "/test-helmswitcher-dest" 92 | 93 | usr, errCurr := user.Current() 94 | if errCurr != nil { 95 | log.Fatal(errCurr) 96 | } 97 | symlinkPathSrc := usr.HomeDir + testSymlinkSrc 98 | symlinkPathDest := usr.HomeDir + testSymlinkDest 99 | 100 | ln, _ := os.Readlink(symlinkPathSrc) 101 | 102 | if ln != symlinkPathDest { 103 | t.Log("Creating symlink") 104 | if err := os.Symlink(symlinkPathDest, symlinkPathSrc); err != nil { 105 | t.Error(err) 106 | } 107 | } 108 | 109 | symlinkExist := lib.CheckSymlink(symlinkPathSrc) 110 | 111 | if symlinkExist { 112 | t.Logf("Symlink does exist %v [expected]", ln) 113 | } else { 114 | t.Logf("Symlink does not exist %v [unexpected]", ln) 115 | } 116 | 117 | os.Remove(symlinkPathSrc) 118 | } 119 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at a.tokiwong@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /modal/modal.go: -------------------------------------------------------------------------------- 1 | package modal 2 | 3 | import "time" 4 | 5 | type Repo struct { 6 | URL string `json:"url"` 7 | AssetsURL string `json:"assets_url"` 8 | UploadURL string `json:"upload_url"` 9 | HTMLURL string `json:"html_url"` 10 | ID int `json:"id"` 11 | NodeID string `json:"node_id"` 12 | TagName string `json:"tag_name"` 13 | TargetCommitish string `json:"target_commitish"` 14 | Name string `json:"name"` 15 | Draft bool `json:"draft"` 16 | Prerelease bool `json:"prerelease"` 17 | CreatedAt time.Time `json:"created_at"` 18 | PublishedAt time.Time `json:"published_at"` 19 | TarballURL string `json:"tarball_url"` 20 | ZipballURL string `json:"zipball_url"` 21 | Body string `json:"body"` 22 | Author Author `json:"author"` 23 | Assets []Assets `json:"assets"` 24 | } 25 | 26 | //Author : git owner properties 27 | type Author struct { 28 | Login string `json:"login"` 29 | ID int `json:"id"` 30 | NodeID string `json:"node_id"` 31 | AvatarURL string `json:"avatar_url"` 32 | GravatarID string `json:"gravatar_id"` 33 | URL string `json:"url"` 34 | HTMLURL string `json:"html_url"` 35 | FollowersURL string `json:"followers_url"` 36 | FollowingURL string `json:"following_url"` 37 | GistsURL string `json:"gists_url"` 38 | StarredURL string `json:"starred_url"` 39 | SubscriptionsURL string `json:"subscriptions_url"` 40 | OrganizationsURL string `json:"organizations_url"` 41 | ReposURL string `json:"repos_url"` 42 | EventsURL string `json:"events_url"` 43 | ReceivedEventsURL string `json:"received_events_url"` 44 | Type string `json:"type"` 45 | SiteAdmin bool `json:"site_admin"` 46 | } 47 | 48 | //Author : author properties 49 | type Assets struct { 50 | URL string `json:"url"` 51 | ID int `json:"id"` 52 | NodeID string `json:"node_id"` 53 | Name string `json:"name"` 54 | Label string `json:"label"` 55 | ContentType string `json:"content_type"` 56 | State string `json:"state"` 57 | Size int `json:"size"` 58 | DownloadCount int `json:"download_count"` 59 | CreatedAt time.Time `json:"created_at"` 60 | UpdatedAt time.Time `json:"updated_at"` 61 | BrowserDownloadURL string `json:"browser_download_url"` 62 | Uploader Uploader `json:"uploader"` 63 | } 64 | 65 | //Uploader : repo uploader properties 66 | type Uploader struct { 67 | Login string `json:"login"` 68 | ID int `json:"id"` 69 | NodeID string `json:"node_id"` 70 | AvatarURL string `json:"avatar_url"` 71 | GravatarID string `json:"gravatar_id"` 72 | URL string `json:"url"` 73 | HTMLURL string `json:"html_url"` 74 | FollowersURL string `json:"followers_url"` 75 | FollowingURL string `json:"following_url"` 76 | GistsURL string `json:"gists_url"` 77 | StarredURL string `json:"starred_url"` 78 | SubscriptionsURL string `json:"subscriptions_url"` 79 | OrganizationsURL string `json:"organizations_url"` 80 | ReposURL string `json:"repos_url"` 81 | EventsURL string `json:"events_url"` 82 | ReceivedEventsURL string `json:"received_events_url"` 83 | Type string `json:"type"` 84 | SiteAdmin bool `json:"site_admin"` 85 | } 86 | 87 | type Client struct { 88 | ClientID string 89 | ClientSecret string 90 | } 91 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "os/user" 8 | "regexp" 9 | 10 | "github.com/manifoldco/promptui" 11 | "github.com/pborman/getopt" 12 | lib "github.com/tokiwong/helm-switcher/lib" 13 | "github.com/tokiwong/helm-switcher/modal" 14 | ) 15 | 16 | const ( 17 | helmURL = "https://api.github.com/repos/helm/helm/releases?" 18 | defaultBin = "/usr/local/bin/helm" 19 | installFile = "helm" 20 | installVersion = "helm_" 21 | installPath = "/.helm.versions/" 22 | ) 23 | 24 | var version = "0.0.5\n" 25 | 26 | var clientID = "xxx" 27 | var clientSecret = "xxx" 28 | 29 | func main() { 30 | 31 | var client modal.Client 32 | 33 | client.ClientID = clientID 34 | client.ClientSecret = clientSecret 35 | 36 | custBinPath := getopt.StringLong("bin", 'b', defaultBin, "Custom binary path. For example: /Users/username/bin/helm") 37 | helpFlag := getopt.BoolLong("help", 'h', "displays help message") 38 | versionFlag := getopt.BoolLong("version", 'v', "displays the version of helmswitch") 39 | _ = versionFlag 40 | 41 | getopt.Parse() 42 | args := getopt.Args() 43 | 44 | if *helpFlag { 45 | usageMessage() 46 | } else if *versionFlag { 47 | fmt.Printf("Version: %v\n", version) 48 | } else { 49 | if len(args) == 0 { 50 | helmList, assets := lib.GetAppList(helmURL, &client) 51 | recentVersions, _ := lib.GetRecentVersions() //get recent versions from RECENT file 52 | helmList = append(recentVersions, helmList...) //append recent versions to the top of the list 53 | helmList = lib.RemoveDuplicateVersions(helmList) //remove duplicate version 54 | 55 | /* prompt user to select version of helm */ 56 | prompt := promptui.Select{ 57 | Label: "Select helm version", 58 | Items: helmList, 59 | } 60 | 61 | _, helmVersion, errPrompt := prompt.Run() 62 | 63 | if errPrompt != nil { 64 | log.Printf("Prompt failed %v\n", errPrompt) 65 | os.Exit(1) 66 | } 67 | 68 | installLocation := lib.Install(helmURL, helmVersion, assets, custBinPath) 69 | lib.AddRecent(helmVersion, installLocation) //add to recent file for faster lookup 70 | os.Exit(0) 71 | 72 | fmt.Println(helmList) 73 | 74 | } else if len(args) == 1 { 75 | semverRegex := regexp.MustCompile(`\A\d+(\.\d+){2}\z`) 76 | if semverRegex.MatchString(args[0]) { 77 | requestedVersion := args[0] 78 | 79 | //check if version is already downloaded before checking if it exists 80 | /* get current user */ 81 | usr, errCurr := user.Current() 82 | if errCurr != nil { 83 | log.Fatal(errCurr) 84 | } 85 | /* set installation location */ 86 | installLocation := usr.HomeDir + installPath 87 | 88 | fileInstalled := lib.CheckFileExist(installLocation + installVersion + requestedVersion) 89 | 90 | if fileInstalled { 91 | 92 | /* remove current symlink if exist*/ 93 | symlinkExist := lib.CheckSymlink(*custBinPath) 94 | 95 | if symlinkExist { 96 | lib.RemoveSymlink(*custBinPath) 97 | } 98 | /* set symlink to desired version */ 99 | lib.CreateSymlink(installLocation+installVersion+requestedVersion, *custBinPath) 100 | fmt.Printf("Switched helm to version %q \n", requestedVersion) 101 | } else { 102 | //check if version exist before downloading it 103 | fmt.Println(requestedVersion + " not found in install path " + installPath) 104 | fmt.Println("Checking if the version exists...") 105 | 106 | helmList, assets := lib.GetAppList(helmURL, &client) 107 | exist := lib.VersionExist(requestedVersion, helmList) 108 | 109 | if exist { 110 | installLocation := lib.Install(helmURL, requestedVersion, assets, custBinPath) 111 | lib.AddRecent(requestedVersion, installLocation) //add to recent file for faster lookup 112 | } else { 113 | fmt.Println("Not a valid helm version") 114 | } 115 | 116 | } 117 | 118 | } else { 119 | usageMessage() 120 | } 121 | 122 | } 123 | 124 | } 125 | 126 | } 127 | 128 | func usageMessage() { 129 | fmt.Print("\n\n") 130 | getopt.PrintUsage(os.Stderr) 131 | fmt.Println("Supply the helm version as an argument (ex: helmswitch 2.4.13 ), or choose from a menu") 132 | } 133 | -------------------------------------------------------------------------------- /lib/download_test.go: -------------------------------------------------------------------------------- 1 | package lib_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/url" 7 | "os" 8 | "os/user" 9 | "runtime" 10 | "testing" 11 | 12 | lib "github.com/tokiwong/helm-switcher/lib" 13 | ) 14 | 15 | // TestDownloadFromURL_FileNameMatch : Check expected filename exist when downloaded 16 | func TestDownloadFromURL_FileNameMatch(t *testing.T) { 17 | 18 | helmURL := "https://github.com/helm/helm/releases/download/" 19 | installVersion := "helm_" 20 | installPath := "/.helm.versions_test/" 21 | goarch := runtime.GOARCH 22 | goos := runtime.GOOS 23 | 24 | // get current user 25 | usr, errCurr := user.Current() 26 | if errCurr != nil { 27 | log.Fatal(errCurr) 28 | } 29 | 30 | fmt.Printf("Current user: %v \n", usr.HomeDir) 31 | installLocation := usr.HomeDir + installPath 32 | 33 | // create /.helm.versions_test/ directory to store code 34 | if _, err := os.Stat(installLocation); os.IsNotExist(err) { 35 | log.Printf("Creating directory for helm: %v", installLocation) 36 | err = os.MkdirAll(installLocation, 0755) 37 | if err != nil { 38 | fmt.Printf("Unable to create directory for helm: %v", installLocation) 39 | panic(err) 40 | } 41 | } 42 | 43 | /* test download lowest helm version */ 44 | lowestVersion := "0.13.9" 45 | 46 | url := helmURL + "v" + lowestVersion + "/" + installVersion + goos + "_" + goarch 47 | expectedFile := usr.HomeDir + installPath + installVersion + goos + "_" + goarch 48 | installedFile, _ := lib.DownloadFromURL(installLocation, url) 49 | 50 | if installedFile == expectedFile { 51 | t.Logf("Expected file %v", expectedFile) 52 | t.Logf("Downloaded file %v", installedFile) 53 | t.Log("Download file matches expected file") 54 | } else { 55 | t.Logf("Expected file %v", expectedFile) 56 | t.Logf("Downloaded file %v", installedFile) 57 | t.Error("Download file mismatches expected file") 58 | } 59 | 60 | /* test download latest helm version */ 61 | latestVersion := "0.14.11" 62 | 63 | url = helmURL + "v" + latestVersion + "/" + installVersion + goos + "_" + goarch 64 | expectedFile = usr.HomeDir + installPath + installVersion + goos + "_" + goarch 65 | installedFile, _ = lib.DownloadFromURL(installLocation, url) 66 | 67 | if installedFile == expectedFile { 68 | t.Logf("Expected file name %v", expectedFile) 69 | t.Logf("Downloaded file name %v", installedFile) 70 | t.Log("Download file name matches expected file") 71 | } else { 72 | t.Logf("Expected file name %v", expectedFile) 73 | t.Logf("Downloaded file name %v", installedFile) 74 | t.Error("Downoad file name mismatches expected file") 75 | } 76 | 77 | cleanUp(installLocation) 78 | } 79 | 80 | // TestDownloadFromURL_FileExist : Check expected file exist when downloaded 81 | func TestDownloadFromURL_FileExist(t *testing.T) { 82 | 83 | helmURL := "https://github.com/helm/helm/releases/download/" 84 | installVersion := "helm_" 85 | installPath := "/.helm.versions_test/" 86 | goarch := runtime.GOARCH 87 | goos := runtime.GOOS 88 | 89 | // get current user 90 | usr, errCurr := user.Current() 91 | if errCurr != nil { 92 | log.Fatal(errCurr) 93 | } 94 | 95 | fmt.Printf("Current user: %v \n", usr.HomeDir) 96 | installLocation := usr.HomeDir + installPath 97 | 98 | // create /.helm.versions_test/ directory to store code 99 | if _, err := os.Stat(installLocation); os.IsNotExist(err) { 100 | log.Printf("Creating directory for helm: %v", installLocation) 101 | err = os.MkdirAll(installLocation, 0755) 102 | if err != nil { 103 | fmt.Printf("Unable to create directory for helm: %v", installLocation) 104 | panic(err) 105 | } 106 | } 107 | 108 | /* test download lowest helm version */ 109 | lowestVersion := "0.13.9" 110 | 111 | url := helmURL + "v" + lowestVersion + "/" + installVersion + goos + "_" + goarch 112 | expectedFile := usr.HomeDir + installPath + installVersion + goos + "_" + goarch 113 | installedFile, _ := lib.DownloadFromURL(installLocation, url) 114 | 115 | if checkFileExist(expectedFile) { 116 | t.Logf("Expected file %v", expectedFile) 117 | t.Logf("Downloaded file %v", installedFile) 118 | t.Log("Download file matches expected file") 119 | } else { 120 | t.Logf("Expected file %v", expectedFile) 121 | t.Logf("Downloaded file %v", installedFile) 122 | t.Error("Downoad file mismatches expected file") 123 | } 124 | 125 | /* test download latest helm version */ 126 | latestVersion := "0.14.11" 127 | 128 | url = helmURL + "v" + latestVersion + "/" + installVersion + goos + "_" + goarch 129 | expectedFile = usr.HomeDir + installPath + installVersion + goos + "_" + goarch 130 | installedFile, _ = lib.DownloadFromURL(installLocation, url) 131 | 132 | if checkFileExist(expectedFile) { 133 | t.Logf("Expected file %v", expectedFile) 134 | t.Logf("Downloaded file %v", installedFile) 135 | t.Log("Download file matches expected file") 136 | } else { 137 | t.Logf("Expected file %v", expectedFile) 138 | t.Logf("Downloaded file %v", installedFile) 139 | t.Error("Downoad file mismatches expected file") 140 | } 141 | 142 | cleanUp(installLocation) 143 | } 144 | 145 | func TestDownloadFromURL_Valid(t *testing.T) { 146 | 147 | helmURL := "https://github.com/helm/helm/releases/download/" 148 | 149 | url, err := url.ParseRequestURI(helmURL) 150 | if err != nil { 151 | t.Errorf("Valid URL provided: %v", err) 152 | t.Errorf("Invalid URL %v", err) 153 | } else { 154 | t.Logf("Valid URL from %v", url) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /lib/files.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "archive/tar" 5 | "bufio" 6 | "bytes" 7 | "compress/gzip" 8 | "crypto/sha256" 9 | "fmt" 10 | "io" 11 | "io/ioutil" 12 | "log" 13 | "os" 14 | "path/filepath" 15 | "strings" 16 | ) 17 | 18 | // RenameFile : rename file name 19 | func RenameFile(src string, dest string) { 20 | err := os.Rename(src, dest) 21 | if err != nil { 22 | fmt.Println(err) 23 | return 24 | } 25 | } 26 | 27 | // RemoveFiles : remove file 28 | func RemoveFiles(src string) { 29 | files, err := filepath.Glob(src) 30 | if err != nil { 31 | 32 | panic(err) 33 | } 34 | for _, f := range files { 35 | if err := os.Remove(f); err != nil { 36 | panic(err) 37 | } 38 | } 39 | } 40 | 41 | // CheckFileExist : check if file exist in directory 42 | func CheckFileExist(file string) bool { 43 | _, err := os.Stat(file) 44 | if err != nil { 45 | return false 46 | } 47 | return true 48 | } 49 | 50 | //CreateDirIfNotExist : create directory if directory does not exist 51 | func CreateDirIfNotExist(dir string) { 52 | if _, err := os.Stat(dir); os.IsNotExist(err) { 53 | log.Printf("Creating directory for helm: %v", dir) 54 | err = os.MkdirAll(dir, 0755) 55 | if err != nil { 56 | fmt.Printf("Unable to create directory for helm: %v", dir) 57 | panic(err) 58 | } 59 | } 60 | } 61 | 62 | //WriteLines : writes into file 63 | func WriteLines(lines []string, path string) (err error) { 64 | var ( 65 | file *os.File 66 | ) 67 | 68 | if file, err = os.Create(path); err != nil { 69 | return err 70 | } 71 | defer file.Close() 72 | 73 | for _, item := range lines { 74 | _, err := file.WriteString(strings.TrimSpace(item) + "\n") 75 | if err != nil { 76 | fmt.Println(err) 77 | break 78 | } 79 | } 80 | 81 | return nil 82 | } 83 | 84 | // ReadLines : Read a whole file into the memory and store it as array of lines 85 | func ReadLines(path string) (lines []string, err error) { 86 | var ( 87 | file *os.File 88 | part []byte 89 | prefix bool 90 | ) 91 | if file, err = os.Open(path); err != nil { 92 | return 93 | } 94 | defer file.Close() 95 | 96 | reader := bufio.NewReader(file) 97 | buffer := bytes.NewBuffer(make([]byte, 0)) 98 | for { 99 | if part, prefix, err = reader.ReadLine(); err != nil { 100 | break 101 | } 102 | buffer.Write(part) 103 | if !prefix { 104 | lines = append(lines, buffer.String()) 105 | buffer.Reset() 106 | } 107 | } 108 | if err == io.EOF { 109 | err = nil 110 | } 111 | return 112 | } 113 | 114 | //IsDirEmpty : check if directory is empty (TODO UNIT TEST) 115 | func IsDirEmpty(name string) bool { 116 | 117 | exist := false 118 | 119 | f, err := os.Open(name) 120 | if err != nil { 121 | log.Fatal(err) 122 | } 123 | defer f.Close() 124 | 125 | _, err = f.Readdirnames(1) // Or f.Readdir(1) 126 | if err == io.EOF { 127 | exist = true 128 | } 129 | return exist // Either not empty or error, suits both cases 130 | } 131 | 132 | //CheckDirHasHelmBin : // check binary exist (TODO UNIT TEST) 133 | func CheckDirHasHelmBin(dir, prefix string) bool { 134 | 135 | exist := false 136 | 137 | files, err := ioutil.ReadDir(dir) 138 | if err != nil { 139 | log.Fatal(err) 140 | //return exist, err 141 | } 142 | res := []string{} 143 | for _, f := range files { 144 | if !f.IsDir() && strings.HasPrefix(f.Name(), prefix) { 145 | res = append(res, filepath.Join(dir, f.Name())) 146 | exist = true 147 | } 148 | } 149 | return exist 150 | } 151 | 152 | //CheckDirExist : check if directory exist 153 | //dir=path to file 154 | //return path to directory 155 | func CheckDirExist(dir string) bool { 156 | if _, err := os.Stat(dir); os.IsNotExist(err) { 157 | return false 158 | } 159 | return true 160 | } 161 | 162 | // Path : returns path of directory 163 | // value=path to file 164 | func Path(value string) string { 165 | return filepath.Dir(value) 166 | } 167 | 168 | func Untar(dest string, r io.Reader) error { 169 | 170 | gzr, err := gzip.NewReader(r) 171 | if err != nil { 172 | return err 173 | } 174 | defer gzr.Close() 175 | 176 | tr := tar.NewReader(gzr) 177 | 178 | for { 179 | header, err := tr.Next() 180 | 181 | switch { 182 | 183 | // if no more files are found return 184 | case err == io.EOF: 185 | return nil 186 | 187 | // return any other error 188 | case err != nil: 189 | return err 190 | 191 | // if the header is nil, just skip it (not sure how this happens) 192 | case header == nil: 193 | continue 194 | } 195 | 196 | // the target location where the dir/file should be created 197 | target := filepath.Join(dest, header.Name) 198 | 199 | // the following switch could also be done using fi.Mode(), not sure if there 200 | // a benefit of using one vs. the other. 201 | // fi := header.FileInfo() 202 | 203 | // check the file type 204 | switch header.Typeflag { 205 | 206 | // if its a dir and it doesn't exist create it 207 | case tar.TypeDir: 208 | if _, err := os.Stat(target); err != nil { 209 | if err := os.MkdirAll(target, 0755); err != nil { 210 | return err 211 | } 212 | } 213 | 214 | // if it's a file create it 215 | case tar.TypeReg: 216 | f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) 217 | if err != nil { 218 | return err 219 | } 220 | 221 | // copy over contents 222 | if _, err := io.Copy(f, tr); err != nil { 223 | return err 224 | } 225 | 226 | // manually close here after each file operation; defering would cause each file close 227 | // to wait until all operations have completed. 228 | f.Close() 229 | } 230 | } 231 | } 232 | 233 | func VerifyChecksum(fileInstalled string, chkInstalled string) bool { 234 | 235 | fmt.Println("Verifying SHA sum") 236 | 237 | file, err := os.Open(fileInstalled) 238 | if err != nil { 239 | log.Fatal(err) 240 | } 241 | defer file.Close() 242 | 243 | fileHash := sha256.New() 244 | if _, err := io.Copy(fileHash, file); err != nil { 245 | log.Fatal(err) 246 | } 247 | 248 | fileSha := fmt.Sprintf("%x", fileHash.Sum(nil)) 249 | fmt.Println(fileSha) 250 | 251 | chkContent, err := ioutil.ReadFile(chkInstalled) 252 | if err != nil { 253 | log.Fatal(err) 254 | } 255 | 256 | chkOut := string(chkContent) 257 | fmt.Println(chkOut) 258 | 259 | if fileSha != chkOut[0:64] { 260 | log.Fatal("Expecting: " + chkOut + ", Received: " + fileSha + ". Aborting.") 261 | return false 262 | } 263 | os.Remove(chkInstalled) 264 | fmt.Println("SHA sum verified") 265 | return true 266 | 267 | } 268 | -------------------------------------------------------------------------------- /lib/install.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "os/user" 8 | "regexp" 9 | "runtime" 10 | 11 | "github.com/tokiwong/helm-switcher/modal" 12 | ) 13 | 14 | const ( 15 | helmURL = "https://get.helm.sh/" 16 | installFile = "helm" 17 | installVersion = "helm_" 18 | binLocation = "/usr/local/bin/helm" 19 | installPath = "/.helm.versions/" 20 | recentFile = "RECENT" 21 | ) 22 | 23 | var ( 24 | installLocation = "/tmp" 25 | installedBinPath = "/tmp" 26 | ) 27 | 28 | func init() { 29 | /* get current user */ 30 | usr, errCurr := user.Current() 31 | if errCurr != nil { 32 | log.Fatal(errCurr) 33 | } 34 | 35 | /* set installation location */ 36 | installLocation = usr.HomeDir + installPath 37 | 38 | /* set default binary path for helm */ 39 | installedBinPath = binLocation 40 | 41 | /* find helm binary location if helm is already installed*/ 42 | cmd := NewCommand("helm") 43 | next := cmd.Find() 44 | 45 | /* overrride installation default binary path if helm is already installed */ 46 | /* find the last bin path */ 47 | for path := next(); len(path) > 0; path = next() { 48 | installedBinPath = path 49 | } 50 | 51 | /* remove current symlink if exist*/ 52 | symlinkExist := CheckSymlink(installedBinPath) 53 | 54 | if symlinkExist { 55 | RemoveSymlink(installedBinPath) 56 | } 57 | /* Create local installation directory if it does not exist */ 58 | CreateDirIfNotExist(installLocation) 59 | } 60 | 61 | //Install : Install the provided version in the argument 62 | func Install(url string, appversion string, assets []modal.Repo, userBinPath *string) string { 63 | 64 | /* If user provided bin path use user one instead of default */ 65 | if userBinPath != nil { 66 | installedBinPath = *userBinPath 67 | } 68 | 69 | pathDir := Path(installedBinPath) //get path directory from binary path 70 | binDirExist := CheckDirExist(pathDir) //check bin path exist 71 | 72 | if !binDirExist { 73 | fmt.Printf("Binary path does not exist: %s\n", pathDir) 74 | fmt.Printf("Please create binary path: %s for helm installation\n", pathDir) 75 | os.Exit(1) 76 | } 77 | 78 | /* check if selected version already downloaded */ 79 | // fileExist := CheckFileExist(installLocation + installVersion + appversion) 80 | 81 | /* if selected version already exist, */ 82 | // if fileExist { 83 | 84 | // /* remove current symlink if exist*/ 85 | // symlinkExist := CheckSymlink(installedBinPath) 86 | 87 | // if symlinkExist { 88 | // RemoveSymlink(installedBinPath) 89 | // } 90 | // /* set symlink to desired version */ 91 | // CreateSymlink(installLocation+installVersion+appversion, installedBinPath) 92 | // fmt.Printf("Switched helm to version %q \n", appversion) 93 | // return installLocation 94 | // } 95 | 96 | /* remove current symlink if exist*/ 97 | symlinkExist := CheckSymlink(installedBinPath) 98 | 99 | if symlinkExist { 100 | RemoveSymlink(installedBinPath) 101 | } 102 | 103 | /* if selected version already exist, */ 104 | /* proceed to download it from the helm release page */ 105 | //url := helmURL + "v" + helmversion + "/" + "helm" + "_" + goos + "_" + goarch 106 | 107 | goarch := runtime.GOARCH 108 | goos := runtime.GOOS 109 | urlDownload := "" 110 | chkDownload := "" 111 | 112 | for _, v := range assets { 113 | 114 | if v.TagName == "v"+appversion { 115 | if len(v.Assets) > 0 { 116 | for _, b := range v.Assets { 117 | 118 | matchedOS, _ := regexp.MatchString(goos, b.BrowserDownloadURL) 119 | matchedARCH, _ := regexp.MatchString(goarch, b.BrowserDownloadURL) 120 | if matchedOS && matchedARCH { 121 | // urlDownload = b.BrowserDownloadURL 122 | urlDownload = "https://get.helm.sh/helm-" + v.TagName + "-" + goos + "-" + goarch + ".tar.gz" 123 | chkDownload = urlDownload + ".sha256" 124 | break 125 | } 126 | } 127 | } 128 | break 129 | } 130 | } 131 | 132 | fileInstalled, _ := DownloadFromURL(installLocation, urlDownload) 133 | tarRead, readErr := os.Open(fileInstalled) 134 | if readErr != nil { 135 | fmt.Println("Expected a location, found " + fileInstalled) 136 | } 137 | 138 | chkInstalled, _ := DownloadFromURL(installLocation, chkDownload) 139 | verifySha := VerifyChecksum(fileInstalled, chkInstalled) 140 | if verifySha != true { 141 | log.Fatal("didn't pass the verify step") 142 | } 143 | 144 | /* untar the downloaded file*/ 145 | Untar(installLocation, tarRead) 146 | binDir := installLocation + "/" + goos + "-" + goarch + "/helm" 147 | 148 | /* rename file to helm version name - helm_x.x.x */ 149 | RenameFile(binDir, installLocation+installVersion+appversion) 150 | 151 | err := os.Chmod(installLocation+installVersion+appversion, 0755) 152 | if err != nil { 153 | log.Println(err) 154 | } 155 | 156 | /* set symlink to desired version */ 157 | CreateSymlink(installLocation+installVersion+appversion, installedBinPath) 158 | fmt.Printf("Switched helm to version %q \n", appversion) 159 | return installLocation 160 | } 161 | 162 | // AddRecent : add to recent file 163 | func AddRecent(requestedVersion string, installLocation string) { 164 | 165 | semverRegex := regexp.MustCompile(`\d+(\.\d+){2}\z`) 166 | 167 | fileExist := CheckFileExist(installLocation + recentFile) 168 | if fileExist { 169 | lines, errRead := ReadLines(installLocation + recentFile) 170 | 171 | if errRead != nil { 172 | fmt.Printf("Error: %s\n", errRead) 173 | return 174 | } 175 | 176 | for _, line := range lines { 177 | if !semverRegex.MatchString(line) { 178 | RemoveFiles(installLocation + recentFile) 179 | CreateRecentFile(requestedVersion) 180 | return 181 | } 182 | } 183 | 184 | versionExist := VersionExist(requestedVersion, lines) 185 | 186 | if !versionExist { 187 | if len(lines) >= 3 { 188 | _, lines = lines[len(lines)-1], lines[:len(lines)-1] 189 | 190 | lines = append([]string{requestedVersion}, lines...) 191 | WriteLines(lines, installLocation+recentFile) 192 | } else { 193 | lines = append([]string{requestedVersion}, lines...) 194 | WriteLines(lines, installLocation+recentFile) 195 | } 196 | } 197 | 198 | } else { 199 | CreateRecentFile(requestedVersion) 200 | } 201 | } 202 | 203 | // GetRecentVersions : get recent version from file 204 | func GetRecentVersions() ([]string, error) { 205 | 206 | fileExist := CheckFileExist(installLocation + recentFile) 207 | if fileExist { 208 | semverRegex := regexp.MustCompile(`\A\d+(\.\d+){2}\z`) 209 | 210 | lines, errRead := ReadLines(installLocation + recentFile) 211 | 212 | if errRead != nil { 213 | fmt.Printf("Error: %s\n", errRead) 214 | return nil, errRead 215 | } 216 | 217 | for _, line := range lines { 218 | if !semverRegex.MatchString(line) { 219 | RemoveFiles(installLocation + recentFile) 220 | return nil, errRead 221 | } 222 | } 223 | return lines, nil 224 | } 225 | return nil, nil 226 | } 227 | 228 | //CreateRecentFile : create a recent file 229 | func CreateRecentFile(requestedVersion string) { 230 | WriteLines([]string{requestedVersion}, installLocation+recentFile) 231 | } 232 | 233 | // ValidVersionFormat : returns valid version format 234 | /* For example: 0.1.2 = valid 235 | // For example: 0.1.2-beta1 = valid 236 | // For example: 0.1.2-alpha = valid 237 | // For example: a.1.2 = invalid 238 | // For example: 0.1. 2 = invalid 239 | */ 240 | func ValidVersionFormat(version string) bool { 241 | 242 | // Getting versions from body; should return match /X.X.X-@/ where X is a number,@ is a word character between a-z or A-Z 243 | // Follow https://semver.org/spec/v1.0.0-beta.html 244 | // Check regular expression at https://rubular.com/r/ju3PxbaSBALpJB 245 | semverRegex := regexp.MustCompile(`^(\d+\.\d+\.\d+)(-[a-zA-z]+\d*)?$`) 246 | 247 | return semverRegex.MatchString(version) 248 | } 249 | -------------------------------------------------------------------------------- /lib/files_test.go: -------------------------------------------------------------------------------- 1 | package lib_test 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io" 7 | "log" 8 | "os" 9 | "os/user" 10 | "regexp" 11 | "runtime" 12 | "strings" 13 | "testing" 14 | "time" 15 | 16 | "github.com/tokiwong/helm-switcher/lib" 17 | ) 18 | 19 | // TestRenameFile : Create a file, check filename exist, 20 | // rename file, check new filename exit 21 | func TestRenameFile(t *testing.T) { 22 | 23 | installFile := "helm" 24 | installVersion := "helm_" 25 | installPath := "/.helm.versions_test/" 26 | version := "0.0.7" 27 | 28 | usr, errCurr := user.Current() 29 | if errCurr != nil { 30 | log.Fatal(errCurr) 31 | } 32 | installLocation := usr.HomeDir + installPath 33 | 34 | createDirIfNotExist(installLocation) 35 | 36 | createFile(installLocation + installFile) 37 | 38 | if exist := checkFileExist(installLocation + installFile); exist { 39 | t.Logf("File exist %v", installLocation+installFile) 40 | } else { 41 | t.Logf("File does not exist %v", installLocation+installFile) 42 | t.Error("Missing file") 43 | } 44 | 45 | lib.RenameFile(installLocation+installFile, installLocation+installVersion+version) 46 | 47 | if exist := checkFileExist(installLocation + installVersion + version); exist { 48 | t.Logf("New file exist %v", installLocation+installVersion+version) 49 | } else { 50 | t.Logf("New file does not exist %v", installLocation+installVersion+version) 51 | t.Error("Missing new file") 52 | } 53 | 54 | if exist := checkFileExist(installLocation + installFile); exist { 55 | t.Logf("Old file should not exist %v", installLocation+installFile) 56 | t.Error("Did not rename file") 57 | } else { 58 | t.Logf("Old file does not exist %v", installLocation+installFile) 59 | } 60 | 61 | cleanUp(installLocation) 62 | } 63 | 64 | // TestRemoveFiles : Create a file, check file exist, 65 | // remove file, check file does not exist 66 | func TestRemoveFiles(t *testing.T) { 67 | 68 | installFile := "helm" 69 | installPath := "/.helm.versions_test/" 70 | 71 | usr, errCurr := user.Current() 72 | if errCurr != nil { 73 | log.Fatal(errCurr) 74 | } 75 | installLocation := usr.HomeDir + installPath 76 | 77 | createDirIfNotExist(installLocation) 78 | 79 | createFile(installLocation + installFile) 80 | 81 | if exist := checkFileExist(installLocation + installFile); exist { 82 | t.Logf("File exist %v", installLocation+installFile) 83 | } else { 84 | t.Logf("File does not exist %v", installLocation+installFile) 85 | t.Error("Missing file") 86 | } 87 | 88 | lib.RemoveFiles(installLocation + installFile) 89 | 90 | if exist := checkFileExist(installLocation + installFile); exist { 91 | t.Logf("Old file should not exist %v", installLocation+installFile) 92 | t.Error("Did not remove file") 93 | } else { 94 | t.Logf("Old file does not exist %v", installLocation+installFile) 95 | } 96 | 97 | cleanUp(installLocation) 98 | } 99 | 100 | // TestCreateDirIfNotExist : Create a directory, check directory exist 101 | func TestCreateDirIfNotExist(t *testing.T) { 102 | 103 | installPath := "/.helm.versions_test/" 104 | 105 | usr, errCurr := user.Current() 106 | if errCurr != nil { 107 | log.Fatal(errCurr) 108 | } 109 | installLocation := usr.HomeDir + installPath 110 | 111 | cleanUp(installLocation) 112 | 113 | if _, err := os.Stat(installLocation); os.IsNotExist(err) { 114 | t.Logf("Directory should not exist %v (expected)", installLocation) 115 | } else { 116 | t.Logf("Directory already exist %v (unexpected)", installLocation) 117 | t.Error("Directory should not exist") 118 | } 119 | 120 | lib.CreateDirIfNotExist(installLocation) 121 | t.Logf("Creating directory %v", installLocation) 122 | 123 | if _, err := os.Stat(installLocation); err == nil { 124 | t.Logf("Directory exist %v (expected)", installLocation) 125 | } else { 126 | t.Logf("Directory should exist %v (unexpected)", installLocation) 127 | t.Error("Directory should exist") 128 | } 129 | 130 | cleanUp(installLocation) 131 | } 132 | 133 | //TestWriteLines : write to file, check readline to verify 134 | func TestWriteLines(t *testing.T) { 135 | 136 | installPath := "/.helm.versions_test/" 137 | recentFile := "RECENT" 138 | semverRegex := regexp.MustCompile(`\A\d+(\.\d+){2}\z`) 139 | 140 | usr, errCurr := user.Current() 141 | if errCurr != nil { 142 | log.Fatal(errCurr) 143 | } 144 | installLocation := usr.HomeDir + installPath 145 | 146 | createDirIfNotExist(installLocation) 147 | 148 | test_array := []string{"0.0.1", "0.0.2", "0.0.3"} 149 | 150 | errWrite := lib.WriteLines(test_array, installLocation+recentFile) 151 | 152 | if errWrite != nil { 153 | t.Logf("Write should work %v (unexpected)", errWrite) 154 | log.Fatal(errWrite) 155 | } else { 156 | 157 | var ( 158 | file *os.File 159 | part []byte 160 | prefix bool 161 | errOpen, errRead error 162 | lines []string 163 | ) 164 | if file, errOpen = os.Open(installLocation + recentFile); errOpen != nil { 165 | log.Fatal(errOpen) 166 | } 167 | defer file.Close() 168 | 169 | reader := bufio.NewReader(file) 170 | buffer := bytes.NewBuffer(make([]byte, 0)) 171 | for { 172 | if part, prefix, errRead = reader.ReadLine(); errRead != nil { 173 | break 174 | } 175 | buffer.Write(part) 176 | if !prefix { 177 | lines = append(lines, buffer.String()) 178 | buffer.Reset() 179 | } 180 | } 181 | if errRead == io.EOF { 182 | errRead = nil 183 | } 184 | 185 | if errRead != nil { 186 | log.Fatalf("Error: %s\n", errRead) 187 | } 188 | 189 | for _, line := range lines { 190 | if !semverRegex.MatchString(line) { 191 | log.Fatalf("Write to file is not invalid: %s\n", line) 192 | break 193 | } 194 | } 195 | 196 | t.Log("Write versions exist (expected)") 197 | } 198 | 199 | cleanUp(installLocation) 200 | 201 | } 202 | 203 | // TestReadLines : read from file, check write to verify 204 | func TestReadLines(t *testing.T) { 205 | installPath := "/.helm.versions_test/" 206 | recentFile := "RECENT" 207 | semverRegex := regexp.MustCompile(`\A\d+(\.\d+){2}\z`) 208 | 209 | usr, errCurr := user.Current() 210 | if errCurr != nil { 211 | log.Fatal(errCurr) 212 | } 213 | installLocation := usr.HomeDir + installPath 214 | 215 | createDirIfNotExist(installLocation) 216 | 217 | test_array := []string{"0.0.1", "0.0.2", "0.0.3"} 218 | 219 | var ( 220 | file *os.File 221 | errCreate error 222 | ) 223 | 224 | if file, errCreate = os.Create(installLocation + recentFile); errCreate != nil { 225 | log.Fatalf("Error: %s\n", errCreate) 226 | } 227 | defer file.Close() 228 | 229 | for _, item := range test_array { 230 | _, err := file.WriteString(strings.TrimSpace(item) + "\n") 231 | if err != nil { 232 | log.Fatalf("Error: %s\n", err) 233 | break 234 | } 235 | } 236 | 237 | lines, errRead := lib.ReadLines(installLocation + recentFile) 238 | 239 | if errRead != nil { 240 | log.Fatalf("Error: %s\n", errRead) 241 | } 242 | 243 | for _, line := range lines { 244 | if !semverRegex.MatchString(line) { 245 | log.Fatalf("Write to file is not invalid: %s\n", line) 246 | break 247 | } 248 | } 249 | 250 | t.Log("Read versions exist (expected)") 251 | 252 | cleanUp(installLocation) 253 | 254 | } 255 | 256 | // TestIsDirEmpty : create empty directory, check if empty 257 | func TestIsDirEmpty(t *testing.T) { 258 | 259 | current := time.Now() 260 | 261 | installPath := "/.helm.versions_test/" 262 | 263 | usr, errCurr := user.Current() 264 | if errCurr != nil { 265 | log.Fatal(errCurr) 266 | } 267 | installLocation := usr.HomeDir + installPath 268 | 269 | test_dir := current.Format("2006-01-02") 270 | t.Logf("Create test dir: %v \n", test_dir) 271 | 272 | createDirIfNotExist(installLocation) 273 | 274 | createDirIfNotExist(installLocation + "/" + test_dir) 275 | 276 | empty := lib.IsDirEmpty(installLocation + "/" + test_dir) 277 | 278 | t.Logf("Expected directory to be empty %v [expected]", installLocation+"/"+test_dir) 279 | 280 | if empty == true { 281 | t.Logf("Directory empty") 282 | } else { 283 | t.Error("Directory not empty") 284 | } 285 | 286 | cleanUp(installLocation + "/" + test_dir) 287 | 288 | cleanUp(installLocation) 289 | 290 | } 291 | 292 | // TestCheckDirHasHelmBin : create Helm file in directory, check if exist 293 | func TestCheckDirHasHelmBin(t *testing.T) { 294 | 295 | goarch := runtime.GOARCH 296 | goos := runtime.GOOS 297 | installPath := "/.helm.versions_test/" 298 | installFile := "helm" 299 | 300 | usr, errCurr := user.Current() 301 | if errCurr != nil { 302 | log.Fatal(errCurr) 303 | } 304 | installLocation := usr.HomeDir + installPath 305 | 306 | createDirIfNotExist(installLocation) 307 | 308 | createFile(installLocation + installFile + "_" + goos + "_" + goarch) 309 | 310 | empty := lib.CheckDirHasHelmBin(installLocation, installFile) 311 | 312 | t.Logf("Expected directory to have Helm file %v [expected]", installLocation+installFile+"_"+goos+"_"+goarch) 313 | 314 | if empty == true { 315 | t.Logf("Directory empty") 316 | } else { 317 | t.Error("Directory not empty") 318 | } 319 | 320 | cleanUp(installLocation) 321 | } 322 | -------------------------------------------------------------------------------- /lib/list_versions.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "net/http" 11 | "net/url" 12 | "os" 13 | "reflect" 14 | "regexp" 15 | "sort" 16 | "strconv" 17 | "strings" 18 | "sync" 19 | "time" 20 | 21 | "github.com/tokiwong/helm-switcher/modal" 22 | ) 23 | 24 | type AppVersionList struct { 25 | applist []string 26 | appDown *modal.Assets 27 | } 28 | 29 | var wg = sync.WaitGroup{} 30 | 31 | var numPages = 5 32 | 33 | //GetAppList : Get the list of available app versions 34 | func GetAppList(appURL string, client *modal.Client) ([]string, []modal.Repo) { 35 | 36 | v := url.Values{} 37 | v.Set("clientID", client.ClientID) 38 | v.Add("clientSecret", client.ClientSecret) 39 | 40 | gswitch := http.Client{ 41 | Timeout: time.Second * 10, // Maximum of 10 secs [decresing this seem to fail] 42 | } 43 | 44 | apiURL := appURL + v.Encode() 45 | 46 | req, err := http.NewRequest(http.MethodGet, apiURL, nil) 47 | if err != nil { 48 | log.Fatal("Unable to make request. Please try again.") 49 | } 50 | 51 | req.Header.Set("User-Agent", "App Installer") 52 | 53 | resp, _ := gswitch.Do(req) 54 | links := resp.Header.Get("Link") 55 | link := strings.Split(links, ",") 56 | 57 | for _, pagNum := range link { 58 | if strings.Contains(pagNum, "last") { 59 | strPage := inBetween(pagNum, "page=", ">") 60 | page, err := strconv.Atoi(strPage) 61 | if err != nil { 62 | fmt.Println(err) 63 | os.Exit(2) 64 | } 65 | numPages = page 66 | } 67 | } 68 | 69 | applist, assets := getAppVersion(appURL, numPages, client) 70 | 71 | if len(applist) == 40 { 72 | for header, v := range resp.Header { 73 | switch header { 74 | case "X-Ratelimit-Remaining": 75 | fmt.Print("API Requests Remaining") 76 | fmt.Print(" : ") 77 | fmt.Println(v) 78 | case "X-Ratelimit-Reset": 79 | fmt.Print("Your Rate Limit will reset at") 80 | fmt.Print(" : ") 81 | epochTime, err := strconv.Atoi(strings.Join(v, " ")) 82 | if err != nil { 83 | panic(err) 84 | } 85 | resetTime := time.Unix(int64(epochTime), 0) 86 | fmt.Println(resetTime) 87 | log.Fatal() 88 | } 89 | 90 | } 91 | log.Fatal("Unable to get release from repo, please try again later") 92 | os.Exit(1) 93 | 94 | } 95 | 96 | return applist, assets 97 | } 98 | 99 | //VersionExist : check if requested version exist 100 | func VersionExist(val interface{}, array interface{}) (exists bool) { 101 | 102 | exists = false 103 | switch reflect.TypeOf(array).Kind() { 104 | case reflect.Slice: 105 | s := reflect.ValueOf(array) 106 | 107 | for i := 0; i < s.Len(); i++ { 108 | if reflect.DeepEqual(val, s.Index(i).Interface()) == true { 109 | exists = true 110 | return exists 111 | } 112 | } 113 | } 114 | 115 | return exists 116 | } 117 | 118 | //RemoveDuplicateVersions : remove duplicate version 119 | func RemoveDuplicateVersions(elements []string) []string { 120 | // Use map to record duplicates as we find them. 121 | encountered := map[string]bool{} 122 | result := []string{} 123 | 124 | for v := range elements { 125 | if encountered[elements[v]] == true { 126 | // Do not add duplicate. 127 | } else { 128 | // Record this element as an encountered element. 129 | encountered[elements[v]] = true 130 | // Append to result slice. 131 | result = append(result, elements[v]) 132 | } 133 | } 134 | // Return the new slice. 135 | return result 136 | } 137 | 138 | func inBetween(value string, a string, b string) string { 139 | // Get substring between two strings. 140 | posFirst := strings.Index(value, a) 141 | if posFirst == -1 { 142 | return "" 143 | } 144 | posLast := strings.Index(value, b) 145 | if posLast == -1 { 146 | return "" 147 | } 148 | posFirstAdjusted := posFirst + len(a) 149 | if posFirstAdjusted >= posLast { 150 | return "" 151 | } 152 | return value[posFirstAdjusted:posLast] 153 | } 154 | 155 | func getAppVersion(appURL string, numPages int, client *modal.Client) ([]string, []modal.Repo) { 156 | assets := make([]modal.Repo, 0) 157 | ch := make(chan *[]modal.Repo, 10) 158 | 159 | for i := 1; i <= numPages; i++ { 160 | page := strconv.Itoa(i) 161 | v := url.Values{} 162 | v.Set("page", page) 163 | v.Add("clientID", client.ClientID) 164 | v.Add("clientSecret", client.ClientSecret) 165 | 166 | apiURL := appURL + v.Encode() 167 | wg.Add(1) 168 | go getAppBody(apiURL, ch) 169 | } 170 | 171 | go func(ch chan<- *[]modal.Repo) { 172 | defer close(ch) 173 | wg.Wait() 174 | }(ch) 175 | 176 | for i := range ch { 177 | assets = append(assets, *i...) 178 | } 179 | 180 | semvers := []*Version{} 181 | 182 | var sortedVersion []string 183 | 184 | for _, v := range assets { 185 | semverRegex := regexp.MustCompile(`\Av\d+(\.\d+){2}\z`) 186 | if semverRegex.MatchString(v.TagName) { 187 | trimstr := strings.Trim(v.TagName, "v") 188 | sv, err := NewVersion(trimstr) 189 | if err != nil { 190 | fmt.Println(err) 191 | } 192 | semvers = append(semvers, sv) 193 | } 194 | } 195 | 196 | Sort(semvers) 197 | 198 | for _, sv := range semvers { 199 | sortedVersion = append(sortedVersion, sv.String()) 200 | } 201 | 202 | return sortedVersion, assets 203 | } 204 | 205 | func getAppBody(helmURLPage string, ch chan<- *[]modal.Repo) { 206 | defer wg.Done() 207 | 208 | gswitch := http.Client{ 209 | Timeout: time.Second * 10, // Maximum of 10 secs [decresing this seem to fail] 210 | } 211 | 212 | req, err := http.NewRequest(http.MethodGet, helmURLPage, nil) 213 | if err != nil { 214 | log.Fatal("Unable to make request. Please try again.") 215 | } 216 | 217 | req.Header.Set("User-Agent", "github-appinstaller") 218 | 219 | res, getErr := gswitch.Do(req) 220 | if getErr != nil { 221 | log.Fatal("Unable to make request Please try again.") 222 | } 223 | 224 | body, readErr := ioutil.ReadAll(res.Body) 225 | if readErr != nil { 226 | log.Fatal("Unable to get release from repo ") 227 | log.Fatal(readErr) 228 | } 229 | 230 | var repo []modal.Repo 231 | jsonErr := json.Unmarshal(body, &repo) 232 | if jsonErr != nil { 233 | for header, v := range res.Header { 234 | switch header { 235 | case "X-Ratelimit-Remaining": 236 | fmt.Print("API Requests Remaining") 237 | fmt.Print(" : ") 238 | fmt.Println(v) 239 | case "X-Ratelimit-Reset": 240 | fmt.Print("Your Rate Limit will reset at") 241 | fmt.Print(" : ") 242 | epochTime, err := strconv.Atoi(strings.Join(v, " ")) 243 | if err != nil { 244 | panic(err) 245 | } 246 | resetTime := time.Unix(int64(epochTime), 0) 247 | fmt.Println(resetTime) 248 | } 249 | 250 | } 251 | log.Fatal("Unable to get release from repo ") 252 | log.Fatal(jsonErr) 253 | } 254 | 255 | var validRepo []modal.Repo 256 | 257 | for _, num := range repo { 258 | if num.Prerelease == false && num.Draft == false { 259 | semverRegex := regexp.MustCompile(`\Av\d+(\.\d+){2}\z`) 260 | if semverRegex.MatchString(num.TagName) { 261 | validRepo = append(validRepo, num) 262 | } 263 | } 264 | 265 | } 266 | 267 | ch <- &validRepo 268 | 269 | //return &repo 270 | } 271 | 272 | type Version struct { 273 | Major int64 274 | Minor int64 275 | Patch int64 276 | PreRelease PreRelease 277 | Metadata string 278 | } 279 | 280 | type PreRelease string 281 | 282 | func splitOff(input *string, delim string) (val string) { 283 | parts := strings.SplitN(*input, delim, 2) 284 | 285 | if len(parts) == 2 { 286 | *input = parts[0] 287 | val = parts[1] 288 | } 289 | 290 | return val 291 | } 292 | 293 | func New(version string) *Version { 294 | return Must(NewVersion(version)) 295 | } 296 | 297 | func NewVersion(version string) (*Version, error) { 298 | v := Version{} 299 | 300 | if err := v.Set(version); err != nil { 301 | return nil, err 302 | } 303 | 304 | return &v, nil 305 | } 306 | 307 | // Must is a helper for wrapping NewVersion and will panic if err is not nil. 308 | func Must(v *Version, err error) *Version { 309 | if err != nil { 310 | panic(err) 311 | } 312 | return v 313 | } 314 | 315 | // Set parses and updates v from the given version string. Implements flag.Value 316 | func (v *Version) Set(version string) error { 317 | metadata := splitOff(&version, "+") 318 | preRelease := PreRelease(splitOff(&version, "-")) 319 | dotParts := strings.SplitN(version, ".", 3) 320 | 321 | if len(dotParts) != 3 { 322 | return fmt.Errorf("%s is not in dotted-tri format", version) 323 | } 324 | 325 | if err := validateIdentifier(string(preRelease)); err != nil { 326 | return fmt.Errorf("failed to validate pre-release: %v", err) 327 | } 328 | 329 | if err := validateIdentifier(metadata); err != nil { 330 | return fmt.Errorf("failed to validate metadata: %v", err) 331 | } 332 | 333 | parsed := make([]int64, 3, 3) 334 | 335 | for i, v := range dotParts[:3] { 336 | val, err := strconv.ParseInt(v, 10, 64) 337 | parsed[i] = val 338 | if err != nil { 339 | return err 340 | } 341 | } 342 | 343 | v.Metadata = metadata 344 | v.PreRelease = preRelease 345 | v.Major = parsed[0] 346 | v.Minor = parsed[1] 347 | v.Patch = parsed[2] 348 | return nil 349 | } 350 | 351 | func (v Version) String() string { 352 | var buffer bytes.Buffer 353 | 354 | fmt.Fprintf(&buffer, "%d.%d.%d", v.Major, v.Minor, v.Patch) 355 | 356 | if v.PreRelease != "" { 357 | fmt.Fprintf(&buffer, "-%s", v.PreRelease) 358 | } 359 | 360 | if v.Metadata != "" { 361 | fmt.Fprintf(&buffer, "+%s", v.Metadata) 362 | } 363 | 364 | return buffer.String() 365 | } 366 | 367 | func (v *Version) UnmarshalYAML(unmarshal func(interface{}) error) error { 368 | var data string 369 | if err := unmarshal(&data); err != nil { 370 | return err 371 | } 372 | return v.Set(data) 373 | } 374 | 375 | func (v Version) MarshalJSON() ([]byte, error) { 376 | return []byte(`"` + v.String() + `"`), nil 377 | } 378 | 379 | func (v *Version) UnmarshalJSON(data []byte) error { 380 | l := len(data) 381 | if l == 0 || string(data) == `""` { 382 | return nil 383 | } 384 | if l < 2 || data[0] != '"' || data[l-1] != '"' { 385 | return errors.New("invalid semver string") 386 | } 387 | return v.Set(string(data[1 : l-1])) 388 | } 389 | 390 | // Compare tests if v is less than, equal to, or greater than versionB, 391 | // returning -1, 0, or +1 respectively. 392 | func (v Version) Compare(versionB Version) int { 393 | if cmp := recursiveCompare(v.Slice(), versionB.Slice()); cmp != 0 { 394 | return cmp 395 | } 396 | return preReleaseCompare(v, versionB) 397 | } 398 | 399 | // Equal tests if v is equal to versionB. 400 | func (v Version) Equal(versionB Version) bool { 401 | return v.Compare(versionB) == 0 402 | } 403 | 404 | // LessThan tests if v is less than versionB. 405 | func (v Version) LessThan(versionB Version) bool { 406 | return v.Compare(versionB) < 0 407 | } 408 | 409 | // Slice converts the comparable parts of the semver into a slice of integers. 410 | func (v Version) Slice() []int64 { 411 | return []int64{v.Major, v.Minor, v.Patch} 412 | } 413 | 414 | func (p PreRelease) Slice() []string { 415 | preRelease := string(p) 416 | return strings.Split(preRelease, ".") 417 | } 418 | 419 | func preReleaseCompare(versionA Version, versionB Version) int { 420 | a := versionA.PreRelease 421 | b := versionB.PreRelease 422 | 423 | /* Handle the case where if two versions are otherwise equal it is the 424 | * one without a PreRelease that is greater */ 425 | if len(a) == 0 && (len(b) > 0) { 426 | return 1 427 | } else if len(b) == 0 && (len(a) > 0) { 428 | return -1 429 | } 430 | 431 | // If there is a prerelease, check and compare each part. 432 | return recursivePreReleaseCompare(a.Slice(), b.Slice()) 433 | } 434 | 435 | func recursiveCompare(versionA []int64, versionB []int64) int { 436 | if len(versionA) == 0 { 437 | return 0 438 | } 439 | 440 | a := versionA[0] 441 | b := versionB[0] 442 | 443 | if a > b { 444 | return 1 445 | } else if a < b { 446 | return -1 447 | } 448 | 449 | return recursiveCompare(versionA[1:], versionB[1:]) 450 | } 451 | 452 | func recursivePreReleaseCompare(versionA []string, versionB []string) int { 453 | // A larger set of pre-release fields has a higher precedence than a smaller set, 454 | // if all of the preceding identifiers are equal. 455 | if len(versionA) == 0 { 456 | if len(versionB) > 0 { 457 | return -1 458 | } 459 | return 0 460 | } else if len(versionB) == 0 { 461 | // We're longer than versionB so return 1. 462 | return 1 463 | } 464 | 465 | a := versionA[0] 466 | b := versionB[0] 467 | 468 | aInt := false 469 | bInt := false 470 | 471 | aI, err := strconv.Atoi(versionA[0]) 472 | if err == nil { 473 | aInt = true 474 | } 475 | 476 | bI, err := strconv.Atoi(versionB[0]) 477 | if err == nil { 478 | bInt = true 479 | } 480 | 481 | // Numeric identifiers always have lower precedence than non-numeric identifiers. 482 | if aInt && !bInt { 483 | return -1 484 | } else if !aInt && bInt { 485 | return 1 486 | } 487 | 488 | // Handle Integer Comparison 489 | if aInt && bInt { 490 | if aI > bI { 491 | return 1 492 | } else if aI < bI { 493 | return -1 494 | } 495 | } 496 | 497 | // Handle String Comparison 498 | if a > b { 499 | return 1 500 | } else if a < b { 501 | return -1 502 | } 503 | 504 | return recursivePreReleaseCompare(versionA[1:], versionB[1:]) 505 | } 506 | 507 | // BumpMajor increments the Major field by 1 and resets all other fields to their default values 508 | func (v *Version) BumpMajor() { 509 | v.Major += 1 510 | v.Minor = 0 511 | v.Patch = 0 512 | v.PreRelease = PreRelease("") 513 | v.Metadata = "" 514 | } 515 | 516 | // BumpMinor increments the Minor field by 1 and resets all other fields to their default values 517 | func (v *Version) BumpMinor() { 518 | v.Minor += 1 519 | v.Patch = 0 520 | v.PreRelease = PreRelease("") 521 | v.Metadata = "" 522 | } 523 | 524 | // BumpPatch increments the Patch field by 1 and resets all other fields to their default values 525 | func (v *Version) BumpPatch() { 526 | v.Patch += 1 527 | v.PreRelease = PreRelease("") 528 | v.Metadata = "" 529 | } 530 | 531 | // validateIdentifier makes sure the provided identifier satisfies semver spec 532 | func validateIdentifier(id string) error { 533 | if id != "" && !reIdentifier.MatchString(id) { 534 | return fmt.Errorf("%s is not a valid semver identifier", id) 535 | } 536 | return nil 537 | } 538 | 539 | // reIdentifier is a regular expression used to check that pre-release and metadata 540 | // identifiers satisfy the spec requirements 541 | var reIdentifier = regexp.MustCompile(`^[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*$`) 542 | 543 | type Versions []*Version 544 | 545 | func (s Versions) Len() int { 546 | return len(s) 547 | } 548 | 549 | func (s Versions) Swap(i, j int) { 550 | s[i], s[j] = s[j], s[i] 551 | } 552 | 553 | func (s Versions) Less(i, j int) bool { 554 | return s[i].LessThan(*s[j]) 555 | } 556 | 557 | // Sort sorts the given slice of Version 558 | func Sort(versions []*Version) { 559 | sort.Sort(sort.Reverse(Versions(versions))) 560 | } 561 | --------------------------------------------------------------------------------