Skip to content

Commit 08a0bf7

Browse files
committed
feat: enchance version api
1 parent 1e73c35 commit 08a0bf7

File tree

2 files changed

+196
-0
lines changed

2 files changed

+196
-0
lines changed

version/latest.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package version
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
"net/http"
8+
"net/url"
9+
"time"
10+
11+
goversion "github.com/hashicorp/go-version"
12+
)
13+
14+
const (
15+
latestVersionTimeout = 10 * time.Second
16+
17+
DefaultReleasesURL = "https://github.com/treeverse/lakeFS/releases"
18+
githubBaseURL = "https://api.github.com/"
19+
20+
GithubRepoOwner = "treeverse"
21+
GithubRepoName = "lakeFS"
22+
)
23+
24+
var ErrHTTPStatus = errors.New("unexpected HTTP status code")
25+
26+
type RepositoryRelease struct {
27+
TagName string `json:"tag_name,omitempty"`
28+
Name string `json:"name,omitempty"`
29+
Draft bool `json:"draft,omitempty"`
30+
Prerelease bool `json:"prerelease,omitempty"`
31+
ID int64 `json:"id,omitempty"`
32+
URL string `json:"url,omitempty"`
33+
}
34+
35+
type LatestVersionResponse struct {
36+
CheckTime time.Time `json:"check_time"`
37+
Outdated bool `json:"outdated"`
38+
LatestVersion string `json:"latest_version"`
39+
CurrentVersion string `json:"current_version"`
40+
}
41+
42+
type Source interface {
43+
FetchLatestVersion() (string, error)
44+
}
45+
46+
type CachedVersionSource struct {
47+
Source Source
48+
lastCheck time.Time
49+
cachePeriod time.Duration
50+
fetchErr error
51+
fetchResponse string
52+
}
53+
54+
func NewDefaultVersionSource(cachePeriod time.Duration) Source {
55+
gh := NewGithubReleases(GithubRepoOwner, GithubRepoName)
56+
return NewCachedSource(gh, cachePeriod)
57+
}
58+
59+
func NewCachedSource(src Source, cachePeriod time.Duration) *CachedVersionSource {
60+
return &CachedVersionSource{
61+
Source: src,
62+
cachePeriod: cachePeriod,
63+
}
64+
}
65+
66+
func (cs *CachedVersionSource) FetchLatestVersion() (string, error) {
67+
if time.Since(cs.lastCheck) > cs.cachePeriod {
68+
cs.fetchResponse, cs.fetchErr = cs.Source.FetchLatestVersion()
69+
cs.lastCheck = time.Now()
70+
}
71+
return cs.fetchResponse, cs.fetchErr
72+
}
73+
74+
type GithubReleases struct {
75+
owner string
76+
repository string
77+
}
78+
79+
func NewGithubReleases(owner, repository string) *GithubReleases {
80+
return &GithubReleases{
81+
owner: owner,
82+
repository: repository,
83+
}
84+
}
85+
86+
func (gh *GithubReleases) FetchLatestVersion() (string, error) {
87+
u, err := url.JoinPath(githubBaseURL, "repos", gh.owner, gh.repository, "releases", "latest")
88+
if err != nil {
89+
return "", err
90+
}
91+
92+
req, err := http.NewRequest(http.MethodGet, u, nil)
93+
if err != nil {
94+
return "", err
95+
}
96+
97+
client := &http.Client{
98+
Timeout: latestVersionTimeout,
99+
}
100+
101+
resp, err := client.Do(req)
102+
if err != nil {
103+
return "", err
104+
}
105+
defer func() { _ = resp.Body.Close() }()
106+
107+
if resp.StatusCode != http.StatusOK {
108+
return "", fmt.Errorf("unexpected HTTP response %d: %w", resp.StatusCode, ErrHTTPStatus)
109+
}
110+
111+
var release RepositoryRelease
112+
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
113+
return "", err
114+
}
115+
116+
return release.TagName, nil
117+
}
118+
119+
func CheckLatestVersion(targetVersion string) (*LatestVersionResponse, error) {
120+
targetV, err := goversion.NewVersion(targetVersion)
121+
if err != nil {
122+
return nil, fmt.Errorf("tag parse %s: %w", targetVersion, err)
123+
}
124+
125+
currentV, err := goversion.NewVersion(UserVersion())
126+
if err != nil {
127+
return nil, fmt.Errorf("version parse %s: %w", UserVersion(), err)
128+
}
129+
130+
return &LatestVersionResponse{
131+
Outdated: currentV.LessThan(targetV),
132+
LatestVersion: targetV.String(),
133+
CurrentVersion: currentV.String(),
134+
}, nil
135+
}

version/latest_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package version_test
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
"github.com/treeverse/lakefs/pkg/version"
9+
)
10+
11+
type checkLatestVersionTestCase struct {
12+
CurrentVersion string
13+
LatestVersion string
14+
ShouldError bool
15+
ExpectedOutdated bool
16+
}
17+
18+
func TestCheckLatestVersion(t *testing.T) {
19+
cases := []checkLatestVersionTestCase{
20+
{
21+
CurrentVersion: version.Version,
22+
LatestVersion: "1.0.0",
23+
ExpectedOutdated: false,
24+
},
25+
{
26+
CurrentVersion: "0.0.1",
27+
LatestVersion: "1.2.3",
28+
ExpectedOutdated: true,
29+
},
30+
{
31+
CurrentVersion: "1.2.3",
32+
LatestVersion: "1.2.3",
33+
},
34+
{
35+
CurrentVersion: "1.2.3",
36+
LatestVersion: "1.0.0",
37+
},
38+
{
39+
LatestVersion: "abc",
40+
ShouldError: true,
41+
},
42+
}
43+
for idx, tc := range cases {
44+
t.Run(fmt.Sprintf("check_latest_version_%d", idx), func(t *testing.T) {
45+
version.Version = tc.CurrentVersion
46+
t.Logf("check_latest_version test case input %+v", tc)
47+
latest, err := version.CheckLatestVersion(tc.LatestVersion)
48+
49+
// assert if should error and quit
50+
if tc.ShouldError {
51+
require.Error(t, err, "expected error when comparing latest versions")
52+
return
53+
}
54+
// success path
55+
require.NoError(t, err, "unexpected error when comparing latest versions")
56+
require.Equal(t, tc.ExpectedOutdated, latest.Outdated, "outdated value not as expected")
57+
require.Equal(t, tc.LatestVersion, latest.LatestVersion, "latest version not as expected")
58+
require.Equal(t, tc.CurrentVersion, latest.CurrentVersion, "current version not as expected")
59+
})
60+
}
61+
}

0 commit comments

Comments
 (0)