diff --git a/go.mod b/go.mod index efc68cbd..b2e34b24 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/aws/aws-sdk-go v1.55.5 github.com/dustin/go-humanize v1.0.1 github.com/golang-jwt/jwt v3.2.2+incompatible + github.com/hashicorp/go-version v1.7.0 github.com/inhies/go-bytesize v0.0.0-20210819104631-275770b98743 github.com/johannesboyne/gofakes3 v0.0.0-20220627085814-c3ac35da23b2 github.com/manifoldco/promptui v0.9.0 diff --git a/go.sum b/go.sum index 4f72716a..cebd51e2 100644 --- a/go.sum +++ b/go.sum @@ -22,6 +22,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/inhies/go-bytesize v0.0.0-20210819104631-275770b98743 h1:X3Xxno5Ji8idrNiUoFc7QyXpqhSYlDRYQmc7mlpMBzU= github.com/inhies/go-bytesize v0.0.0-20210819104631-275770b98743/go.mod h1:KrtyD5PFj++GKkFS/7/RRrfnRhAMGQwy75GLCHWrCNs= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= diff --git a/main.go b/main.go index dff4c4ae..9d3ed242 100644 --- a/main.go +++ b/main.go @@ -18,7 +18,7 @@ import ( log "github.com/sirupsen/logrus" ) -var Version = "development" +var Version = "0-development" var Usage = `USAGE: %s [command-args] diff --git a/version/version.go b/version/version.go index 1ecb81d3..a8fae698 100644 --- a/version/version.go +++ b/version/version.go @@ -1,9 +1,16 @@ package version import ( + "encoding/json" "errors" "flag" "fmt" + "io" + "net/http" + "os" + "time" + + "github.com/hashicorp/go-version" ) // Help text and command line flags. @@ -26,11 +33,88 @@ var ArgHelp = ` // main program help var Args = flag.NewFlagSet("version", flag.ExitOnError) +type ghResponse struct { + Name string `json:"name"` + Published string `json:"published_at"` + URL string `json:"html_url"` +} + +// this is just so we can mock bad internet connection +var url = "https://api.github.com/repos/NBISweden/sda-cli/releases/latest" +var timeout = 30 * time.Second + // Returns the version of the sda-cli tool. func Version(ver string) error { if len(Args.Args()) > 0 { return errors.New("version does not take any arguments") } + + req, err := http.NewRequest(http.MethodGet, url, http.NoBody) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to initiate request") + fmt.Println("sda-cli version: ", ver) + + return nil + } + req.Header.Add("X-GitHub-Api-Version", "2022-11-28") + req.Header.Add("Accept", "application/vnd.github+json") + + client := &http.Client{Timeout: timeout} + resp, err := client.Do(req) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to fetch releases, reson: %s\n", err.Error()) + fmt.Println("sda-cli version: ", ver) + + return nil + } + if resp.StatusCode >= 400 { + fmt.Fprintf(os.Stderr, "failed to fetch releases, reson: %s\n", resp.Status) + fmt.Println("sda-cli version: ", ver) + + return nil + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + fmt.Fprintln(os.Stderr, "failed to read response") + fmt.Println("sda-cli version: ", ver) + + return nil + } + + ghVersion := ghResponse{} + if err := json.Unmarshal(body, &ghVersion); err != nil { + fmt.Fprintln(os.Stderr, "failed to unmarshal response") + fmt.Println("sda-cli version: ", ver) + + return nil + } + + appVer, err := version.NewVersion(ver) + if err != nil { + fmt.Fprintln(os.Stderr, "faile to parse app version") + fmt.Println("sda-cli version: ", ver) + + return nil + } + ghVer, err := version.NewVersion(ghVersion.Name) + if err != nil { + fmt.Fprintln(os.Stderr, "faile to parse release version") + fmt.Println("sda-cli version: ", ver) + + return nil + } + + if appVer.LessThan(ghVer) { + pt, _ := time.Parse(time.RFC3339, ghVersion.Published) + fmt.Printf("Newer version if sda-cli is avaiable, %s\n", ghVersion.Name) + fmt.Printf("Published: %s\n", pt.Format(time.DateTime)) + fmt.Printf("Download it from here: %s\n", ghVersion.URL) + + return nil + } + fmt.Println("sda-cli version: ", ver) return nil diff --git a/version/version_test.go b/version/version_test.go index b50a062d..0b486117 100644 --- a/version/version_test.go +++ b/version/version_test.go @@ -1,7 +1,12 @@ package version import ( + "io" + "net/http" + "net/http/httptest" + "os" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" @@ -16,9 +21,79 @@ func TestVersionTestSuite(t *testing.T) { } func (suite *VersionTests) TestGetVersion() { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"html_url": "https://github.com/NBISweden/sda-cli/releases/tag/v0.1.3","name": "v0.1.3","published_at": "2024-09-19T09:23:33Z"}`)) + })) + defer mockServer.Close() + url = mockServer.URL - // get version - err := Version("development") + err := Version("1.0.0") assert.NoError(suite.T(), err) +} + +func (suite *VersionTests) TestGetVersion_newerAvailable() { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"html_url": "https://github.com/NBISweden/sda-cli/releases/tag/v0.1.3","name": "v0.1.3","published_at": "2024-09-19T09:23:33Z"}`)) + })) + defer mockServer.Close() + url = mockServer.URL + + storeStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + err := Version("0.0.1") + assert.NoError(suite.T(), err) + + w.Close() + out, _ := io.ReadAll(r) + os.Stdout = storeStdout + + assert.Contains(suite.T(), string(out), "Newer version if sda-cli is avaiable") +} + +func (suite *VersionTests) TestGetVersion_badGateway() { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusBadGateway) + })) + defer mockServer.Close() + url = mockServer.URL + + storeStderr := os.Stderr + r, w, _ := os.Pipe() + os.Stderr = w + + err := Version("0.0.3") + assert.NoError(suite.T(), err) + + w.Close() + out, _ := io.ReadAll(r) + os.Stderr = storeStderr + + assert.Equal(suite.T(), string(out), "failed to fetch releases, reson: 502 Bad Gateway\n") +} + +func (suite *VersionTests) TestGetVersion_networkTimeout() { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + time.Sleep(20 * time.Millisecond) + w.WriteHeader(http.StatusOK) + })) + defer mockServer.Close() + url = mockServer.URL + timeout = 10 * time.Millisecond + + storeStderr := os.Stderr + r, w, _ := os.Pipe() + os.Stderr = w + + err := Version("0.0.3") + assert.NoError(suite.T(), err) + + w.Close() + out, _ := io.ReadAll(r) + os.Stderr = storeStderr + assert.Contains(suite.T(), string(out), "context deadline exceeded") }