Skip to content

Commit

Permalink
Introducing semver + "check for updates" (#543)
Browse files Browse the repository at this point in the history
This PR adds simple "check for updates" functionality. 

We take the latest release from GitHub release API and compare it to
currently running version. If new version is available, we log a warning
and display beautiful yellow banner in the UI:

<img width="700" alt="image"
src="https://github.com/user-attachments/assets/3b637cef-1590-491f-8a48-5a2e72fb8895">

(keeping my FE skills sharp 💪)
  • Loading branch information
mieciu authored Aug 6, 2024
1 parent 2418845 commit d995b98
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 4 deletions.
67 changes: 67 additions & 0 deletions quesma/buildinfo/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,73 @@
// SPDX-License-Identifier: Elastic-2.0
package buildinfo

import (
"encoding/json"
"fmt"
"github.com/coreos/go-semver/semver"
"net/http"
"time"
)

var Version = "development"
var BuildHash = ""
var BuildDate = ""

const GitHubLatestReleaseURL = "https://api.github.com/repos/quesma/quesma/releases/latest"

type ReleaseInfo struct {
Name string `json:"name"`
TagName string `json:"tag_name"`
// CreatedAt is the date of the commit used for the release, ref: https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#get-the-latest-release
CreatedAt time.Time `json:"created_at"`
}

func (r *ReleaseInfo) IsNewerThanCurrentlyRunning() (bool, error) {
// This is injected at build time, ignore IDE warning below
if Version == "development" {
return false, nil
}
return isNewer(r.Name, Version)
}

func isNewer(latest, current string) (bool, error) {
vCurrent, vLatest := new(semver.Version), new(semver.Version)
if err := vCurrent.Set(current); err != nil {
return false, fmt.Errorf("error parsing current version: %v", err)
}
if err := vLatest.Set(latest); err != nil {
return false, fmt.Errorf("error parsing latest version: %v", err)
}
return vCurrent.LessThan(*vLatest), nil
}

// CheckForTheLatestVersion obtains the latest release information from GitHub and compares it to the currently running version.
// It returns a user-facing message indicating whether the latest version is newer, the same, or if there was an error.
func CheckForTheLatestVersion() (updateAvailable bool, messageBanner string) {
latestRelease, err := getLatestRelease()
if err != nil {
return false, fmt.Sprintf("Failed obtaining latest Quesma version from GitHub: %v", err)
}
shouldUpgrade, err := latestRelease.IsNewerThanCurrentlyRunning()
if err != nil {
return false, fmt.Sprintf("Failed comparing Quesma versions: %v", err)
}
if shouldUpgrade {
return true, fmt.Sprintf("A new version of Quesma is available: %s", latestRelease.Name)
} else {
return false, fmt.Sprintf("You are running the latest version of Quesma: %s", Version)
}
}

func getLatestRelease() (*ReleaseInfo, error) {
resp, err := http.Get(GitHubLatestReleaseURL)
if err != nil {
return nil, fmt.Errorf("error fetching latest release from GitHub: %v", err)
}
var releaseInfo ReleaseInfo
if err = json.NewDecoder(resp.Body).Decode(&releaseInfo); err != nil {
return nil, fmt.Errorf("error decoding JSON: %v", err)
}
defer resp.Body.Close()
return &releaseInfo, nil
}
1 change: 1 addition & 0 deletions quesma/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ require (
)

require (
github.com/coreos/go-semver v0.3.1 // indirect
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect
Expand Down
2 changes: 2 additions & 0 deletions quesma/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYW
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df h1:GSoSVRLoBaFpOOds6QyY1L8AX7uoY+Ln3BHc22W40X0=
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df/go.mod h1:hiVxq5OP2bUGBRNS3Z/bt/reCLFNbdcST6gISi1fiOM=
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down
5 changes: 5 additions & 0 deletions quesma/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ func main() {
}, sig, doneCh, asyncQueryTraceLogger)
defer logger.StdLogFile.Close()
defer logger.ErrLogFile.Close()
go func() {
if upgradeAvailable, message := buildinfo.CheckForTheLatestVersion(); upgradeAvailable {
logger.Warn().Msg(message)
}
}()

if asyncQueryTraceLogger != nil {
asyncQueryTraceEvictor := quesma.AsyncQueryTraceLoggerEvictor{AsyncQueryTrace: asyncQueryTraceLogger.AsyncQueryTrace}
Expand Down
38 changes: 34 additions & 4 deletions quesma/quesma/ui/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"net/url"
"quesma/buildinfo"
"quesma/health"
"quesma/logger"
"quesma/quesma/ui/internal/builder"
"quesma/stats/errorstats"
"runtime"
Expand Down Expand Up @@ -182,6 +183,12 @@ func (qmc *QuesmaManagementConsole) generateDashboardPanel() []byte {
buffer.Html(`<div id="dashboard-quesma" class="component">`)
buffer.Html(`<h3>Quesma</h3>`)

buffer = maybePrintUpgradeAvailableBanner(buffer)

buffer.Html(`<div class="status">Version: `)
buffer.Text(buildinfo.Version)
buffer.Html("</div>")

cpuStr := ""
c0, err0 := cpu.Percent(0, false)

Expand Down Expand Up @@ -211,10 +218,6 @@ func (qmc *QuesmaManagementConsole) generateDashboardPanel() []byte {
buffer.Html(fmt.Sprintf(`<div class="status">Host uptime: %s</div>`, secondsToTerseString(h.Uptime)))
}

buffer.Html(`<div class="status">Version: `)
buffer.Text(buildinfo.Version)
buffer.Html("</div>")

buffer.Html(`</div>`)

buffer.Html(`<div id="dashboard-errors" class="component">`)
Expand All @@ -233,3 +236,30 @@ func (qmc *QuesmaManagementConsole) generateDashboardPanel() []byte {

return buffer.Bytes()
}

type latestVersionCheckResult struct {
upgradeAvailable bool
message string
}

// maybePrintUpgradeAvailableBanner has time cap of 500ms to check for the latest version, if it takes longer than that,
// it will log an error message and don't render anything
func maybePrintUpgradeAvailableBanner(buffer builder.HtmlBuffer) builder.HtmlBuffer {

resultChan := make(chan latestVersionCheckResult, 1)
go func() {
upgradeAvailable, message := buildinfo.CheckForTheLatestVersion()
resultChan <- latestVersionCheckResult{upgradeAvailable, message}
}()
select {
case result := <-resultChan:
if result.upgradeAvailable {
buffer.Html(`<div class="status" style="background-color: yellow; padding: 5px;">`)
buffer.Text(result.message)
buffer.Html("</div>")
}
case <-time.After(500 * time.Millisecond):
logger.Error().Msg("Timeout while checking for the latest version.")
}
return buffer
}

0 comments on commit d995b98

Please sign in to comment.