From b58c11ef831dba8d9bde7f3d43d57ea85f2fdf57 Mon Sep 17 00:00:00 2001 From: Aditya Singh Sisodiya Date: Wed, 25 Aug 2021 16:55:11 +0530 Subject: [PATCH] feat: support for release version info --- go.mod | 4 +- go.sum | 4 ++ version/release.go | 100 +++++++++++++++++++++++++++++++++++++ version/release_test.go | 108 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 version/release.go create mode 100644 version/release_test.go diff --git a/go.mod b/go.mod index 74a7d30..8fc3fc1 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,13 @@ module github.com/odpf/salt go 1.16 require ( + github.com/gorilla/mux v1.8.0 + github.com/hashicorp/go-version v1.3.0 github.com/jeremywohl/flatten v1.0.1 github.com/mcuadros/go-defaults v1.2.0 github.com/mitchellh/mapstructure v1.4.1 github.com/olekukonko/tablewriter v0.0.5 - github.com/pkg/errors v0.9.1 // indirect + github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.8.1 github.com/spf13/viper v1.8.1 github.com/stretchr/testify v1.7.0 diff --git a/go.sum b/go.sum index abf2f11..18f56fc 100644 --- a/go.sum +++ b/go.sum @@ -139,6 +139,8 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= @@ -152,6 +154,8 @@ github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerX github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw= +github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= diff --git a/version/release.go b/version/release.go new file mode 100644 index 0000000..b251248 --- /dev/null +++ b/version/release.go @@ -0,0 +1,100 @@ +package version + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "time" + + "github.com/hashicorp/go-version" + "github.com/odpf/salt/log" + "github.com/pkg/errors" +) + +var ( + ReleaseInfoTimeout = time.Second * 1 + Release = "https://api.github.com/repos/%s/releases/latest" +) + +type Info struct { + Version string + TarURL string +} + +// GithubInfo fetches details related to provided release URL +// releaseURL should point to a specific version +// for example: https://api.github.com/repos/odpf/optimus/releases/latest +func GithubInfo(releaseURL string) (*Info, error) { + httpClient := http.Client{ + Timeout: ReleaseInfoTimeout, + } + req, err := http.NewRequest(http.MethodGet, releaseURL, nil) + if err != nil { + return nil, errors.Wrapf(err, "failed to create request") + } + req.Header.Set("User-Agent", "odpf/salt") + resp, err := httpClient.Do(req) + if err != nil { + return nil, errors.Wrapf(err, "failed to reach releaseURL: %s", releaseURL) + } + if resp.StatusCode != http.StatusOK { + return nil, errors.Wrapf(err, "failed to reach releaseURL: %s, returned: %d", releaseURL, resp.StatusCode) + } + if resp.Body != nil { + defer resp.Body.Close() + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrapf(err, "failed to read response body") + } + + var releaseBody struct { + TagName string `json:"tag_name"` + Tarball string `json:"tarball_url"` + } + if err = json.Unmarshal(body, &releaseBody); err != nil { + return nil, errors.Wrapf(err, "failed to parse: %s", string(body)) + } + + return &Info{ + Version: releaseBody.TagName, + TarURL: releaseBody.Tarball, + }, nil +} + +// IsCurrentLatest returns true if the current version string is greater than +// or equal to latestVersion as per semantic versioning +func IsCurrentLatest(currVersion, latestVersion string) (bool, error) { + currentV, err := version.NewVersion(currVersion) + if err != nil { + return false, errors.Wrapf(err, "failed to parse current version") + } + latestV, err := version.NewVersion(latestVersion) + if err != nil { + return false, errors.Wrapf(err, "failed to parse latest version") + } + if currentV.GreaterThanOrEqual(latestV) { + return true, nil + } + return false, nil +} + +// NotifyIfNotLatest prints a notification if current application version +// is not latest +func NotifyIfNotLatest(logger log.Logger, currentVersion, githubRepo string) error { + info, err := GithubInfo(fmt.Sprintf(Release, githubRepo)) + if err != nil { + return err + } + + isLatest, err := IsCurrentLatest(currentVersion, info.Version) + if err != nil { + return nil + } + if isLatest { + logger.Info(fmt.Sprintf("a newer version is available: %s, consider updating the client", info.Version)) + } + return nil +} diff --git a/version/release_test.go b/version/release_test.go new file mode 100644 index 0000000..f2c8414 --- /dev/null +++ b/version/release_test.go @@ -0,0 +1,108 @@ +package version_test + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gorilla/mux" + "github.com/odpf/salt/version" + "github.com/stretchr/testify/assert" +) + +func TestGithubInfo(t *testing.T) { + muxRouter := mux.NewRouter() + server := httptest.NewServer(muxRouter) + + t.Run("should check for latest version availability by extracting correct version tag for valid json response on release URL", func(t *testing.T) { + muxRouter.HandleFunc("/latest", func(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("Content-Type", "application/json") + response, _ := json.Marshal(struct { + TagName string `json:"tag_name"` + }{ + TagName: "v0.0.2", + }) + rw.Write(response) + }) + info, err := version.GithubInfo("http://" + server.Listener.Addr().String() + "/latest") + assert.Nil(t, err) + assert.Equal(t, "v0.0.2", info.Version) + info, err = version.GithubInfo("http://" + server.Listener.Addr().String() + "/latest") + assert.Nil(t, err) + assert.NotEqual(t, "v0.0.1", info.Version) + }) +} +func TestIsCurrentLatest(t *testing.T) { + muxRouter := mux.NewRouter() + server := httptest.NewServer(muxRouter) + + t.Run("should return true for current version as the latest version ", func(t *testing.T) { + muxRouter.HandleFunc("/latest", func(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("Content-Type", "application/json") + response, _ := json.Marshal(struct { + TagName string `json:"tag_name"` + }{ + TagName: "v0.0.2", + }) + rw.Write(response) + }) + info, err := version.GithubInfo("http://" + server.Listener.Addr().String() + "/latest") + assert.Nil(t, err) + assert.Equal(t, "v0.0.2", info.Version) + res, err := version.IsCurrentLatest("v0.0.2", info.Version) + assert.Nil(t, err) + assert.True(t, res) + }) + t.Run("should return false for current version not same as the latest version", func(t *testing.T) { + muxRouter.HandleFunc("/latest", func(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("Content-Type", "application/json") + response, _ := json.Marshal(struct { + TagName string `json:"tag_name"` + }{ + TagName: "v0.0.2", + }) + rw.Write(response) + }) + info, err := version.GithubInfo("http://" + server.Listener.Addr().String() + "/latest") + assert.Nil(t, err) + assert.Equal(t, "v0.0.2", info.Version) + res, err := version.IsCurrentLatest("v0.0.1", info.Version) + assert.Nil(t, err) + assert.False(t, res) + res, err = version.IsCurrentLatest("", info.Version) + assert.NotNil(t, err) + assert.False(t, res) + res, err = version.IsCurrentLatest("v0.0.3", "") + assert.NotNil(t, err) + assert.False(t, res) + }) +} + +func TestNotifyIfNotLatest(t *testing.T) { + muxRouter := mux.NewRouter() + server := httptest.NewServer(muxRouter) + t.Run("basic check for notify latest version", func(t *testing.T) { + muxRouter.HandleFunc("/latest", func(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("Content-Type", "application/json") + response, _ := json.Marshal(struct { + TagName string `json:"tag_name"` + }{ + TagName: "v0.0.1", + }) + rw.Write(response) + }) + info, err := version.GithubInfo("http://" + server.Listener.Addr().String() + "/latest") + assert.Nil(t, err) + assert.Equal(t, "v0.0.1", info.Version) + res, err := version.IsCurrentLatest("v0.0.1", info.Version) + assert.Nil(t, err) + assert.True(t, res) + err = version.NotifyIfNotLatest(nil, "v0.0.1", "odpf/optimus") + if err != nil { + t.Errorf("Error in notifyIfNotLatest: %v", err) + } + err = version.NotifyIfNotLatest(nil, "", "odpf/optimus") + assert.Nil(t, err) + }) +}