Skip to content

Commit a575053

Browse files
authored
Merge pull request #1093 from rocket-pool/migrate-version-check
Migrate version check
2 parents 205f4ee + b263300 commit a575053

13 files changed

Lines changed: 5713 additions & 39 deletions

File tree

rocketpool-cli/service/service.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,12 +126,12 @@ func printPatchNotes() {
126126
fmt.Println()
127127
}
128128

129-
// Install the Rocket Pool update tracker for the metrics dashboard
129+
// Install the OS update tracker for the metrics dashboard
130130
func installUpdateTracker(yes, verbose bool) error {
131131

132132
// Prompt for confirmation
133133
if prompt.Declined(yes,
134-
"This will add the ability to display any available Operating System updates or new Rocket Pool versions on the metrics dashboard. "+
134+
"This will add the ability to display any available Operating System updates on the metrics dashboard. "+
135135
"Are you sure you want to install the update tracker?") {
136136
fmt.Println("Cancelled.")
137137
return nil
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package collectors
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"net/http"
8+
"strings"
9+
"sync"
10+
"time"
11+
12+
semver "github.com/blang/semver/v4"
13+
"github.com/prometheus/client_golang/prometheus"
14+
15+
"github.com/rocket-pool/smartnode/shared"
16+
)
17+
18+
const (
19+
githubLatestReleaseURL = "https://api.github.com/repos/rocket-pool/smartnode/releases/latest"
20+
versionCheckTimeout = 15 * time.Second
21+
)
22+
23+
// VersionUpdateCollector exposes whether a newer Smart Node release is available.
24+
type VersionUpdateCollector struct {
25+
versionUpdate *prometheus.Desc
26+
current string
27+
latestURL string
28+
client *http.Client
29+
logf func(string, ...interface{})
30+
31+
mu sync.Mutex
32+
updateAvailable float64
33+
latestVersion string
34+
}
35+
36+
type githubReleaseResponse struct {
37+
TagName string `json:"tag_name"`
38+
}
39+
40+
// NewVersionUpdateCollector creates a collector backed by an hourly GitHub release check.
41+
func NewVersionUpdateCollector(logf func(string, ...interface{})) *VersionUpdateCollector {
42+
return &VersionUpdateCollector{
43+
versionUpdate: prometheus.NewDesc(prometheus.BuildFQName(namespace, "", "version_update"),
44+
"The latest available Rocket Pool version",
45+
[]string{"version"}, nil,
46+
),
47+
current: shared.RocketPoolVersion(),
48+
latestURL: githubLatestReleaseURL,
49+
client: &http.Client{
50+
Timeout: versionCheckTimeout,
51+
},
52+
logf: logf,
53+
}
54+
}
55+
56+
// Describe writes metric descriptions to the Prometheus channel.
57+
func (collector *VersionUpdateCollector) Describe(channel chan<- *prometheus.Desc) {
58+
channel <- collector.versionUpdate
59+
}
60+
61+
// Collect emits the latest cached version update status.
62+
func (collector *VersionUpdateCollector) Collect(channel chan<- prometheus.Metric) {
63+
collector.checkIfDue(context.Background())
64+
65+
collector.mu.Lock()
66+
latestVersion := collector.latestVersion
67+
collector.mu.Unlock()
68+
69+
if latestVersion != "" {
70+
channel <- prometheus.MustNewConstMetric(
71+
collector.versionUpdate, prometheus.GaugeValue, collector.updateAvailable, latestVersion)
72+
}
73+
}
74+
75+
func (collector *VersionUpdateCollector) checkIfDue(ctx context.Context) {
76+
collector.mu.Lock()
77+
defer collector.mu.Unlock()
78+
79+
updateAvailable, latestVersion, err := collector.checkForUpdate(ctx)
80+
if err != nil {
81+
if collector.logf != nil {
82+
collector.logf("Error checking latest Rocket Pool release: %v", err)
83+
}
84+
return
85+
}
86+
87+
if updateAvailable {
88+
collector.updateAvailable = 1
89+
} else {
90+
collector.updateAvailable = 0
91+
}
92+
collector.latestVersion = latestVersion
93+
}
94+
95+
func (collector *VersionUpdateCollector) checkForUpdate(ctx context.Context) (bool, string, error) {
96+
latest, err := collector.getLatestVersion(ctx)
97+
if err != nil {
98+
return false, "", err
99+
}
100+
101+
updateAvailable, err := isNewerVersion(collector.current, latest)
102+
if err != nil {
103+
return false, "", err
104+
}
105+
106+
return updateAvailable, latest, nil
107+
}
108+
109+
func (collector *VersionUpdateCollector) getLatestVersion(ctx context.Context) (string, error) {
110+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, collector.latestURL, nil)
111+
if err != nil {
112+
return "", fmt.Errorf("error creating GitHub release request: %w", err)
113+
}
114+
req.Header.Set("Accept", "application/vnd.github+json")
115+
req.Header.Set("User-Agent", "rocketpool-smartnode")
116+
117+
resp, err := collector.client.Do(req)
118+
if err != nil {
119+
return "", fmt.Errorf("error fetching latest GitHub release: %w", err)
120+
}
121+
defer func() {
122+
if err := resp.Body.Close(); err != nil {
123+
collector.logf("Error closing GitHub release response body: %v", err)
124+
}
125+
}()
126+
127+
if resp.StatusCode != http.StatusOK {
128+
return "", fmt.Errorf("GitHub release request returned status %s", resp.Status)
129+
}
130+
131+
var release githubReleaseResponse
132+
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
133+
return "", fmt.Errorf("error decoding latest GitHub release: %w", err)
134+
}
135+
if strings.TrimSpace(release.TagName) == "" {
136+
return "", fmt.Errorf("latest GitHub release did not include a tag_name")
137+
}
138+
139+
return release.TagName, nil
140+
}
141+
142+
func isNewerVersion(currentVersion string, latestVersion string) (bool, error) {
143+
current, err := semver.ParseTolerant(strings.TrimSpace(currentVersion))
144+
if err != nil {
145+
return false, fmt.Errorf("error parsing current version %q: %w", currentVersion, err)
146+
}
147+
148+
latest, err := semver.ParseTolerant(strings.TrimSpace(latestVersion))
149+
if err != nil {
150+
return false, fmt.Errorf("error parsing latest version %q: %w", latestVersion, err)
151+
}
152+
153+
return latest.Compare(current) > 0, nil
154+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package collectors
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
)
9+
10+
func TestIsNewerVersion(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
current string
14+
latest string
15+
want bool
16+
}{
17+
{
18+
name: "latest version is newer",
19+
current: "1.20.2",
20+
latest: "v1.20.3",
21+
want: true,
22+
},
23+
{
24+
name: "same version is not newer",
25+
current: "1.20.2",
26+
latest: "v1.20.2",
27+
want: false,
28+
},
29+
{
30+
name: "older latest version is not newer",
31+
current: "1.20.2",
32+
latest: "v1.20.1",
33+
want: false,
34+
},
35+
{
36+
name: "dev version is newer than latest",
37+
current: "v1.20.3-dev",
38+
latest: "v1.20.2",
39+
want: false,
40+
},
41+
}
42+
43+
for _, tt := range tests {
44+
t.Run(tt.name, func(t *testing.T) {
45+
got, err := isNewerVersion(tt.current, tt.latest)
46+
if err != nil {
47+
t.Fatalf("isNewerVersion returned error: %v", err)
48+
}
49+
if got != tt.want {
50+
t.Fatalf("isNewerVersion(%q, %q) = %t, want %t", tt.current, tt.latest, got, tt.want)
51+
}
52+
})
53+
}
54+
}
55+
56+
func TestCheckIfDueCachesLatestVersion(t *testing.T) {
57+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
58+
_, err := w.Write([]byte(`{"tag_name":"v1.20.3"}`))
59+
if err != nil {
60+
t.Fatalf("error writing response: %v", err)
61+
}
62+
}))
63+
defer server.Close()
64+
65+
collector := NewVersionUpdateCollector(nil)
66+
collector.current = "1.20.2"
67+
collector.latestURL = server.URL
68+
collector.client = server.Client()
69+
70+
collector.checkIfDue(context.Background())
71+
72+
if collector.updateAvailable != 1 {
73+
t.Fatalf("updateAvailable = %f, want 1", collector.updateAvailable)
74+
}
75+
if collector.latestVersion != "v1.20.3" {
76+
t.Fatalf("latestVersion = %q, want %q", collector.latestVersion, "v1.20.3")
77+
}
78+
}

rocketpool/node/metrics-exporter.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ func runMetricsServer(ctx context.Context, c *cli.Command, logger log.ColorLogge
7171
beaconCollector := collectors.NewBeaconCollector(rp, bc, ec, nodeAccount.Address, stateLocker)
7272
smoothingPoolCollector := collectors.NewSmoothingPoolCollector(rp, ec, stateLocker)
7373
governanceCollector := collectors.NewGovernanceCollector(rp)
74+
versionUpdateCollector := collectors.NewVersionUpdateCollector(logger.Printlnf)
7475

7576
// Set up Prometheus
7677
registry := prometheus.NewRegistry()
@@ -84,6 +85,7 @@ func runMetricsServer(ctx context.Context, c *cli.Command, logger log.ColorLogge
8485
registry.MustRegister(beaconCollector)
8586
registry.MustRegister(smoothingPoolCollector)
8687
registry.MustRegister(governanceCollector)
88+
registry.MustRegister(versionUpdateCollector)
8789

8890
// Set up snapshot checking if enabled
8991
if cfg.Smartnode.GetRocketSignerRegistryAddress() != "" {

shared/services/rocketpool/assets/install/alerting/rules/default.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ groups:
119119

120120
{{{- if .AlertEnabled_RPUpdatesAvailable.Value }}}
121121
- alert: RPUpdatesAvailable
122-
expr: max(rocketpool_version_update{job="node"}) > 0
122+
expr: max(rocketpool_version_update{job="rocketpool"}) > 0
123123
labels:
124124
severity: warning
125125
job: node

0 commit comments

Comments
 (0)