Skip to content

Commit b48a6f9

Browse files
committed
Add extended version info
This change introduces a pair of CLI options - `-version` and `-version-json` - to print the verions of scylla-bench and gocql driver, in human-readable or JSON formats respectively. Closes: #156
1 parent c56dbe4 commit b48a6f9

File tree

4 files changed

+579
-10
lines changed

4 files changed

+579
-10
lines changed

Makefile

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
GOCQL_REPO?=github.com/scylladb/gocql
22
DOCKER_IMAGE_TAG?=scylla-bench
33

4+
VERSION ?= $(shell git describe --tags 2>/dev/null || echo "dev")
5+
COMMIT ?= $(shell git rev-parse HEAD 2>/dev/null || echo "unknown")
6+
BUILD_DATE ?= $(shell git log -1 --format=%cd --date=format:%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u +"%Y-%m-%dT%H:%M:%SZ")
7+
LDFLAGS_VERSION := -X github.com/scylladb/scylla-bench/internal/version.version=$(VERSION) -X github.com/scylladb/scylla-bench/internal/version.commit=$(COMMIT) -X github.com/scylladb/scylla-bench/internal/version.date=$(BUILD_DATE)
8+
49
_prepare_build_dir:
510
@mkdir build >/dev/null 2>&1 || true
611

@@ -12,16 +17,16 @@ _use-custom-gocql-version:
1217
fi;\
1318
echo "Using custom gocql commit \"${GOCQL_VERSION}\"";\
1419
go mod edit -replace "github.com/gocql/gocql=${GOCQL_REPO}@${GOCQL_VERSION}";\
15-
go mod tidy;\
20+
go mod tidy -compat=1.17;\
1621
}
1722

1823
build: _prepare_build_dir
1924
@echo "Building static scylla-bench"
20-
@CGO_ENABLED=0 go build -ldflags="-s -w" -o ./build/scylla-bench .
25+
@CGO_ENABLED=0 go build -ldflags="-s -w $(LDFLAGS_VERSION)" -o ./build/scylla-bench .
2126

2227
build-debug: _prepare_build_dir
2328
@echo "Building debug version of static scylla-bench"
24-
@CGO_ENABLED=0 go build -gcflags "all=-N -l" -o ./build/scylla-bench .
29+
@CGO_ENABLED=0 go build -gcflags "all=-N -l" -ldflags="$(LDFLAGS_VERSION)" -o ./build/scylla-bench .
2530

2631
.PHONY: build-with-custom-gocql-version
2732
build-with-custom-gocql-version: _use-custom-gocql-version build

internal/version/version.go

+357
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,357 @@
1+
package version
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
"os"
8+
"os/exec"
9+
"regexp"
10+
"runtime/debug"
11+
"strings"
12+
"time"
13+
)
14+
15+
const (
16+
gocqlPackage = "github.com/gocql/gocql"
17+
githubTimeout = 5 * time.Second
18+
userAgent = "scylla-bench (github.com/scylladb/scylla-bench)"
19+
githubAPIBaseURL = "https://api.github.com"
20+
)
21+
22+
// Default version values; can be overridden via ldflags
23+
var (
24+
version = "dev"
25+
commit = "unknown"
26+
date = "unknown"
27+
)
28+
29+
type ComponentInfo struct {
30+
Version string `json:"version"`
31+
CommitDate string `json:"commit_date"`
32+
CommitSHA string `json:"commit_sha"`
33+
}
34+
35+
type VersionInfo struct {
36+
ScyllaBench ComponentInfo `json:"scylla-bench"`
37+
Driver ComponentInfo `json:"scylla-driver"`
38+
}
39+
40+
type githubRelease struct {
41+
TagName string `json:"tag_name"`
42+
CreatedAt time.Time `json:"created_at"`
43+
}
44+
45+
type githubTag struct {
46+
Object struct {
47+
SHA string `json:"sha"`
48+
} `json:"object"`
49+
}
50+
51+
type githubClient struct {
52+
client *http.Client
53+
baseURL string
54+
userAgent string
55+
}
56+
57+
func newGithubClient() *githubClient {
58+
return &githubClient{
59+
client: &http.Client{Timeout: githubTimeout},
60+
baseURL: githubAPIBaseURL,
61+
userAgent: userAgent,
62+
}
63+
}
64+
65+
// Performs an HTTP GET request and decodes the JSON response to the target
66+
func (g *githubClient) getJSON(path string, target interface{}) error {
67+
url := g.baseURL + path
68+
req, err := http.NewRequest("GET", url, nil)
69+
if err != nil {
70+
return fmt.Errorf("failed to create request: %w", err)
71+
}
72+
req.Header.Set("User-Agent", g.userAgent)
73+
74+
resp, err := g.client.Do(req)
75+
if err != nil {
76+
return fmt.Errorf("request failed: %w", err)
77+
}
78+
defer resp.Body.Close()
79+
80+
if resp.StatusCode != http.StatusOK {
81+
return fmt.Errorf("unexpected status %d", resp.StatusCode)
82+
}
83+
84+
if err := json.NewDecoder(resp.Body).Decode(target); err != nil {
85+
return fmt.Errorf("failed to decode JSON: %w", err)
86+
}
87+
return nil
88+
}
89+
90+
// Fetches release date and commit SHA for the given version
91+
func (g *githubClient) getReleaseInfo(owner, repo, version string) (date, sha string, err error) {
92+
// Fetch releases
93+
path := fmt.Sprintf("/repos/%s/%s/releases", owner, repo)
94+
var releases []githubRelease
95+
if err := g.getJSON(path, &releases); err != nil {
96+
return "", "", fmt.Errorf("failed to fetch releases: %w", err)
97+
}
98+
99+
// Find matching release
100+
cleanVersion := strings.TrimPrefix(version, "v")
101+
var releaseDate, tagName string
102+
for _, release := range releases {
103+
if strings.TrimPrefix(release.TagName, "v") == cleanVersion {
104+
releaseDate, tagName = release.CreatedAt.Format(time.RFC3339), release.TagName
105+
break
106+
}
107+
}
108+
if tagName == "" {
109+
return "", "", fmt.Errorf("release %s not found", version)
110+
}
111+
112+
// Fetch tag info to get the commit SHA
113+
tagPath := fmt.Sprintf("/repos/%s/%s/git/refs/tags/%s", owner, repo, tagName)
114+
var tag githubTag
115+
if err := g.getJSON(tagPath, &tag); err != nil {
116+
return releaseDate, "", fmt.Errorf("failed to fetch tag info: %w", err)
117+
}
118+
119+
return releaseDate, tag.Object.SHA, nil
120+
}
121+
122+
// Fetches information about the given commit
123+
func (g *githubClient) getCommitInfo(owner, repo, sha string) (date string, err error) {
124+
path := fmt.Sprintf("/repos/%s/%s/commits/%s", owner, repo, sha)
125+
var commit struct {
126+
Commit struct {
127+
Committer struct {
128+
Date string `json:"date"`
129+
} `json:"committer"`
130+
} `json:"commit"`
131+
}
132+
133+
if err := g.getJSON(path, &commit); err != nil {
134+
return "", fmt.Errorf("failed to fetch commit info: %w", err)
135+
}
136+
137+
return commit.Commit.Committer.Date, nil
138+
}
139+
140+
func tryGitCommand(args ...string) (string, bool) {
141+
output, err := exec.Command("git", args...).Output()
142+
if err != nil {
143+
return "", false
144+
}
145+
return strings.TrimSpace(string(output)), true
146+
}
147+
148+
// Reads version info from local Git repository
149+
func getGitVersionInfo() (ver, sha, buildDate string, ok bool) {
150+
// Check if git is available and we're in a git repo
151+
if _, ok := tryGitCommand("rev-parse", "--is-inside-work-tree"); !ok {
152+
return "", "", "", false
153+
}
154+
155+
// Get released version details
156+
sha, ok = tryGitCommand("rev-parse", "HEAD")
157+
if !ok {
158+
return "", "", "", false
159+
}
160+
buildDate, ok = tryGitCommand("log", "-1", "--format=%cd", "--date=format:%Y-%m-%dT%H:%M:%SZ")
161+
if !ok {
162+
return "", "", "", false
163+
}
164+
if tags, ok := tryGitCommand("tag", "--points-at", "HEAD"); ok && tags != "" {
165+
for _, tag := range strings.Split(tags, "\n") {
166+
if strings.HasPrefix(tag, "v") {
167+
return tag, sha, buildDate, true
168+
}
169+
}
170+
}
171+
172+
// If not a released version, use the most recent tag to build a dev version string
173+
if closestTag, ok := tryGitCommand("describe", "--tags", "--abbrev=0"); ok && strings.HasPrefix(closestTag, "v") {
174+
return fmt.Sprintf("%s-dev-%s", closestTag, sha[:8]), sha, buildDate, true
175+
}
176+
177+
return fmt.Sprintf("dev-%s", sha[:8]), sha, buildDate, true
178+
}
179+
180+
func isCommitSHA(s string) bool {
181+
shaPattern := regexp.MustCompile(`^[0-9a-fA-F]{7,40}$`)
182+
return shaPattern.MatchString(s)
183+
}
184+
185+
func extractCommitSHA(version string) string {
186+
if isCommitSHA(version) {
187+
return version
188+
}
189+
190+
// If version is in pseudo-version format (e.g v0.0.0-20230101120000-abcdef123456)
191+
if strings.Count(version, "-") >= 2 {
192+
parts := strings.Split(version, "-")
193+
candidate := parts[len(parts)-1]
194+
if isCommitSHA(candidate) {
195+
return candidate
196+
}
197+
}
198+
199+
return ""
200+
}
201+
202+
func extractRepoOwner(repoPath string, defaultOwner string) string {
203+
parts := strings.Split(repoPath, "/")
204+
if len(parts) >= 2 {
205+
return parts[1]
206+
}
207+
return defaultOwner
208+
}
209+
210+
// Extracts driver version info
211+
func getDriverVersionInfo() ComponentInfo {
212+
info := ComponentInfo{
213+
Version: "unknown",
214+
CommitSHA: "unknown",
215+
CommitDate: "unknown",
216+
}
217+
218+
// Get driver module from build info
219+
buildInfo, ok := debug.ReadBuildInfo()
220+
if !ok {
221+
return info
222+
}
223+
var driverModule *debug.Module
224+
for _, dep := range buildInfo.Deps {
225+
if dep.Path == gocqlPackage {
226+
driverModule = dep
227+
break
228+
}
229+
}
230+
if driverModule == nil {
231+
return info
232+
}
233+
234+
github := newGithubClient()
235+
if replacement := driverModule.Replace; replacement != nil {
236+
// If custom version is set via GOCQL_VERSION env var
237+
if envSHA := os.Getenv("GOCQL_VERSION"); envSHA != "" && isCommitSHA(envSHA) {
238+
info.Version = envSHA
239+
info.CommitSHA = envSHA
240+
241+
repoOwner := extractRepoOwner(os.Getenv("GOCQL_REPO"), "scylladb")
242+
if date, err := github.getCommitInfo(repoOwner, "gocql", envSHA); err == nil {
243+
info.CommitDate = date
244+
}
245+
return info
246+
}
247+
248+
// Otherwise try to extract released version (e.g. v1.2.3)
249+
if strings.HasPrefix(replacement.Version, "v") && !strings.Contains(replacement.Version, "-") {
250+
info.Version = replacement.Version
251+
if date, sha, err := github.getReleaseInfo("scylladb", "gocql", replacement.Version); err == nil {
252+
info.CommitDate = date
253+
info.CommitSHA = sha
254+
}
255+
return info
256+
}
257+
258+
// Otherwise handle pseudo-versions or direct SHA
259+
version := replacement.Version
260+
if sha := extractCommitSHA(version); sha != "" {
261+
info.Version = sha
262+
info.CommitSHA = sha
263+
264+
repoOwner := extractRepoOwner(replacement.Path, "scylladb")
265+
if date, err := github.getCommitInfo(repoOwner, "gocql", sha); err == nil {
266+
info.CommitDate = date
267+
}
268+
return info
269+
}
270+
271+
// As a fallback, just use what we have as a version
272+
info.Version = version
273+
return info
274+
}
275+
276+
// If no driver module replacement, this is the upstream gocql driver
277+
info.Version = driverModule.Version
278+
info.CommitSHA = "upstream release"
279+
return info
280+
}
281+
282+
// Extracts scylla-bench version info
283+
func getMainBuildInfo() (ver, sha, buildDate string) {
284+
ver, sha, buildDate = version, commit, date
285+
286+
// Use git info if no version was provided via ldflags
287+
if ver == "dev" {
288+
if gitVer, gitSha, gitDate, ok := getGitVersionInfo(); ok {
289+
return gitVer, gitSha, gitDate
290+
}
291+
}
292+
293+
// Otherwise fall back to go build info
294+
if info, ok := debug.ReadBuildInfo(); ok {
295+
if ver == "dev" {
296+
if info.Main.Version != "" {
297+
ver = info.Main.Version
298+
} else {
299+
ver = "(devel)"
300+
}
301+
}
302+
303+
for _, setting := range info.Settings {
304+
switch setting.Key {
305+
case "vcs.revision":
306+
if sha == "unknown" {
307+
sha = setting.Value
308+
}
309+
case "vcs.time":
310+
if buildDate == "unknown" {
311+
buildDate = setting.Value
312+
}
313+
}
314+
}
315+
}
316+
return
317+
}
318+
319+
// GetVersionInfo returns the version info for scylla-bench and scylla-gocql-driver
320+
func GetVersionInfo() VersionInfo {
321+
ver, sha, buildDate := getMainBuildInfo()
322+
return VersionInfo{
323+
ScyllaBench: ComponentInfo{
324+
Version: ver,
325+
CommitDate: buildDate,
326+
CommitSHA: sha,
327+
},
328+
Driver: getDriverVersionInfo(),
329+
}
330+
}
331+
332+
// FormatHuman returns a human-readable string with version info
333+
func (v VersionInfo) FormatHuman() string {
334+
return fmt.Sprintf(`scylla-bench:
335+
version: %s
336+
commit sha: %s
337+
commit date: %s
338+
scylla-gocql-driver:
339+
version: %s
340+
commit sha: %s
341+
commit date: %s`,
342+
v.ScyllaBench.Version,
343+
v.ScyllaBench.CommitSHA,
344+
v.ScyllaBench.CommitDate,
345+
v.Driver.Version,
346+
v.Driver.CommitSHA,
347+
v.Driver.CommitDate)
348+
}
349+
350+
// FormatJSON returns a JSON-formatted string with version info
351+
func (v VersionInfo) FormatJSON() (string, error) {
352+
data, err := json.MarshalIndent(v, "", " ")
353+
if err != nil {
354+
return "", fmt.Errorf("failed to marshal version info to JSON: %w", err)
355+
}
356+
return string(data), nil
357+
}

0 commit comments

Comments
 (0)