Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitaly.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSami Hiltunen <shiltunen@gitlab.com>2023-10-13 12:03:02 +0300
committerSami Hiltunen <shiltunen@gitlab.com>2023-10-23 19:47:49 +0300
commit42c47b90ad97c64dd1ae55760d0d6171833c770f (patch)
treeb37ad598298597b38307b20e5fda7dd4905e9276
parentf518ad6e5799727f90d554b9a7b2e87751933e2e (diff)
Add tools for detecting kernel version
This commit adds a utility function for detecting whether the running kernel is at least at given version. This can be used to determine whether functionality in some later kernels can be used safely by Gitaly. Version detection is for now only implemented for Linux as we don't have use cases elsewhere yet.
-rw-r--r--internal/kernel/testhelper_test.go11
-rw-r--r--internal/kernel/version.go82
-rw-r--r--internal/kernel/version_linux.go16
-rw-r--r--internal/kernel/version_linux_test.go14
-rw-r--r--internal/kernel/version_test.go99
-rw-r--r--internal/kernel/version_unsupported.go9
6 files changed, 231 insertions, 0 deletions
diff --git a/internal/kernel/testhelper_test.go b/internal/kernel/testhelper_test.go
new file mode 100644
index 000000000..25357f06f
--- /dev/null
+++ b/internal/kernel/testhelper_test.go
@@ -0,0 +1,11 @@
+package kernel
+
+import (
+ "testing"
+
+ "gitlab.com/gitlab-org/gitaly/v16/internal/testhelper"
+)
+
+func TestMain(m *testing.M) {
+ testhelper.Run(m)
+}
diff --git a/internal/kernel/version.go b/internal/kernel/version.go
new file mode 100644
index 000000000..a74fd5826
--- /dev/null
+++ b/internal/kernel/version.go
@@ -0,0 +1,82 @@
+package kernel
+
+import (
+ "fmt"
+ "regexp"
+ "strconv"
+ "sync"
+
+ "gitlab.com/gitlab-org/gitaly/v16/internal/structerr"
+)
+
+// Version models a kernel version.
+type Version struct {
+ // Major is the major version of the kernel.
+ Major int
+ // Minor is the minor version of the kernel.
+ Minor int
+}
+
+// IsAtLeast checks whether the running kernel is at the given version or later.
+func IsAtLeast(expected Version) (bool, error) {
+ actual, err := getVersion()
+ if err != nil {
+ return false, fmt.Errorf("get version: %w", err)
+ }
+
+ return isAtLeast(expected, actual), nil
+}
+
+func isAtLeast(expected, actual Version) bool {
+ if actual.Major > expected.Major {
+ return true
+ }
+
+ return actual.Major == expected.Major && actual.Minor >= expected.Minor
+}
+
+// As the kernel version doesn't change across invocations, we're only retrieving it during
+// the first call to getVersion, caching it, and using the cached version for all other calls.
+var (
+ cachedVersion Version
+ cachedError error
+ cacheOnce sync.Once
+)
+
+func getVersion() (Version, error) {
+ cacheOnce.Do(func() {
+ release, err := getRelease()
+ if err != nil {
+ cachedError = err
+ return
+ }
+
+ cachedVersion, cachedError = parseRelease(release)
+ })
+
+ return cachedVersion, cachedError
+}
+
+var releaseRegexp = regexp.MustCompile(`^([0-9]+)\.([0-9]+)\.([0-9]+)`)
+
+func parseRelease(release string) (Version, error) {
+ matches := releaseRegexp.FindStringSubmatch(release)
+ if len(matches) == 0 {
+ return Version{}, structerr.New("unexpected release format").WithMetadata("release", release)
+ }
+
+ major, err := strconv.Atoi(matches[1])
+ if err != nil {
+ return Version{}, structerr.New("parse major: %w", err)
+ }
+
+ minor, err := strconv.Atoi(matches[2])
+ if err != nil {
+ return Version{}, structerr.New("parse minor: %w", err)
+ }
+
+ return Version{
+ Major: major,
+ Minor: minor,
+ }, nil
+}
diff --git a/internal/kernel/version_linux.go b/internal/kernel/version_linux.go
new file mode 100644
index 000000000..4981ab733
--- /dev/null
+++ b/internal/kernel/version_linux.go
@@ -0,0 +1,16 @@
+package kernel
+
+import (
+ "fmt"
+
+ "golang.org/x/sys/unix"
+)
+
+func getRelease() (string, error) {
+ uname := &unix.Utsname{}
+ if err := unix.Uname(uname); err != nil {
+ return "", fmt.Errorf("uname: %w", err)
+ }
+
+ return unix.ByteSliceToString(uname.Release[:]), nil
+}
diff --git a/internal/kernel/version_linux_test.go b/internal/kernel/version_linux_test.go
new file mode 100644
index 000000000..f5e0abbb8
--- /dev/null
+++ b/internal/kernel/version_linux_test.go
@@ -0,0 +1,14 @@
+package kernel
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestIsAtleast(t *testing.T) {
+ // Go 1.17 requires kernel version of at least 2.6.32 so we use that as the assertion here.
+ isAtLeast, err := IsAtLeast(Version{Major: 2, Minor: 6})
+ require.NoError(t, err)
+ require.True(t, isAtLeast)
+}
diff --git a/internal/kernel/version_test.go b/internal/kernel/version_test.go
new file mode 100644
index 000000000..3eb3342a7
--- /dev/null
+++ b/internal/kernel/version_test.go
@@ -0,0 +1,99 @@
+package kernel
+
+import (
+ "errors"
+ "strconv"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "gitlab.com/gitlab-org/gitaly/v16/internal/structerr"
+)
+
+func TestIsAtLeast(t *testing.T) {
+ for _, tc := range []struct {
+ desc string
+ expected Version
+ actual Version
+ isAtLeast bool
+ }{
+ {
+ desc: "exact major and minor expected",
+ expected: Version{Major: 1, Minor: 1},
+ actual: Version{Major: 1, Minor: 1},
+ isAtLeast: true,
+ },
+ {
+ desc: "older major than expected",
+ expected: Version{Major: 2, Minor: 1},
+ actual: Version{Major: 1, Minor: 2},
+ isAtLeast: false,
+ },
+ {
+ desc: "newer major than expected",
+ expected: Version{Major: 1, Minor: 2},
+ actual: Version{Major: 2, Minor: 1},
+ isAtLeast: true,
+ },
+ {
+ desc: "newer minor than expected",
+ expected: Version{Minor: 1},
+ actual: Version{Minor: 2},
+ isAtLeast: true,
+ },
+ {
+ desc: "older minor than expected",
+ expected: Version{Minor: 2},
+ actual: Version{Minor: 1},
+ isAtLeast: false,
+ },
+ } {
+ t.Run(tc.desc, func(t *testing.T) {
+ require.Equal(t, tc.isAtLeast, isAtLeast(tc.expected, tc.actual))
+ })
+ }
+}
+
+func TestParseRelease(t *testing.T) {
+ for _, tc := range []struct {
+ desc string
+ release string
+ expectedVersion Version
+ expectedError error
+ }{
+ {
+ desc: "valid release",
+ release: "6.5.5-200.fc38.aarch64",
+ expectedVersion: Version{Major: 6, Minor: 5},
+ },
+ {
+ desc: "invalid major",
+ release: "nan.5.200.fc38.aarch64",
+ expectedError: structerr.New("unexpected release format").WithMetadata("release", "nan.5.200.fc38.aarch64"),
+ },
+ {
+ desc: "invalid minor release",
+ release: "6.nan.5-200.fc38.aarch64",
+ expectedError: structerr.New("unexpected release format").WithMetadata("release", "6.nan.5-200.fc38.aarch64"),
+ },
+ {
+ desc: "major overflows int",
+ release: "9223372036854775808.1.5-200.fc38.aarch64",
+ expectedError: structerr.New("parse major: %w", &strconv.NumError{
+ Func: "Atoi",
+ Num: "9223372036854775808",
+ Err: errors.New("value out of range"),
+ }),
+ },
+ {
+ desc: "everything after patch ignored",
+ release: "17.10.50this-is-ignored",
+ expectedVersion: Version{Major: 17, Minor: 10},
+ },
+ } {
+ t.Run(tc.desc, func(t *testing.T) {
+ version, err := parseRelease(tc.release)
+ require.Equal(t, tc.expectedError, err)
+ require.Equal(t, tc.expectedVersion, version)
+ })
+ }
+}
diff --git a/internal/kernel/version_unsupported.go b/internal/kernel/version_unsupported.go
new file mode 100644
index 000000000..ff2074978
--- /dev/null
+++ b/internal/kernel/version_unsupported.go
@@ -0,0 +1,9 @@
+//go:build !linux
+
+package kernel
+
+import "errors"
+
+func getRelease() (string, error) {
+ return "", errors.New("kernel version detection not supported")
+}