Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 56 additions & 15 deletions pkg/platform/facts/os/os_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,27 @@ import (
func gather(ctx *common.GatherContext) (api.DeviceFactsRequestOs, error) {
name, version := getLinuxDistribution()

return api.DeviceFactsRequestOs{
Arch: runtime.GOARCH,
Family: api.DEVICEFACTSOSFAMILY_LINUX,
Name: api.PtrString(name),
Version: api.PtrString(version),
}, nil
info := api.DeviceFactsRequestOs{
Arch: runtime.GOARCH,
Family: api.DEVICEFACTSOSFAMILY_LINUX,
Name: api.PtrString(name),
}
if version != "" {
info.Version = api.PtrString(version)
}

return info, nil
}

func getLinuxDistribution() (string, string) {
// Try /etc/os-release first (systemd standard)
if fullVersion := parseOSRelease("/etc/os-release"); fullVersion != "" {
return extractVersion(fullVersion)
if name, version := parseOSRelease("/etc/os-release"); name != "" {
return name, version
}

// Try /usr/lib/os-release as fallback
if fullVersion := parseOSRelease("/usr/lib/os-release"); fullVersion != "" {
return extractVersion(fullVersion)
if name, version := parseOSRelease("/usr/lib/os-release"); name != "" {
return name, version
}

// Try /etc/lsb-release (Ubuntu/Debian)
Expand All @@ -57,25 +61,62 @@ func getLinuxDistribution() (string, string) {
return "Linux", getKernelVersion()
}

func parseOSRelease(filename string) string {
func parseOSRelease(filename string) (string, string) {
file, err := os.Open(filename)
if err != nil {
return ""
return "", ""
}
defer func() {
_ = file.Close()
}()

scanner := bufio.NewScanner(file)
var name, prettyName, versionID, version, buildID string

for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, "PRETTY_NAME=") {
return strings.Trim(strings.TrimPrefix(line, "PRETTY_NAME="), "\"")
key, value, ok := strings.Cut(line, "=")
if !ok {
continue
}
value = strings.Trim(value, "\"'")
switch key {
case "NAME":
name = value
case "PRETTY_NAME":
prettyName = value
case "VERSION_ID":
versionID = value
case "VERSION":
version = value
case "BUILD_ID":
buildID = value
}
}

return ""
fullName := prettyName
if fullName == "" {
fullName = name
}
if fullName == "" {
return "", ""
}

parsedName, parsedVersion := extractVersion(fullName)
if parsedVersion != "" {
return parsedName, parsedVersion
}
if versionID != "" {
return parsedName, versionID
}
if version != "" {
return parsedName, version
}
if buildID != "" {
return parsedName, buildID
}

return parsedName, ""
}

func parseLSBRelease() (string, string) {
Expand Down
55 changes: 54 additions & 1 deletion pkg/platform/facts/os/os_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package os

import (
stdos "os"
"path/filepath"
"runtime"
"slices"
"strings"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -18,7 +21,9 @@ func TestGather(t *testing.T) {
assert.NotEqual(t, info.Family, "")
assert.True(t, slices.Contains(api.AllowedDeviceFactsOSFamilyEnumValues, info.Family))
assert.Equal(t, info.Arch, runtime.GOARCH)
assert.Regexp(t, `(\d+\.(?:\d+\.?)+)`, *info.Version, "Version must only contain numbers: '%s'", *info.Version)
if info.Version != nil {
assert.NotEqual(t, strings.TrimSpace(*info.Version), "")
}
}

func TestExtract(t *testing.T) {
Expand Down Expand Up @@ -46,6 +51,54 @@ func TestExtract(t *testing.T) {
}
}

func TestParseOSRelease(t *testing.T) {
for _, tc := range []struct {
name string
content string
osName string
version string
}{
{
name: "pretty name with embedded version",
content: `NAME="Ubuntu"
PRETTY_NAME="Ubuntu 24.04.3 LTS"
`,
osName: "Ubuntu",
version: "24.04.3 LTS",
},
{
name: "fallback to version id",
content: `NAME="TestOS"
PRETTY_NAME="TestOS"
VERSION_ID="1.2"
VERSION="rolling"
BUILD_ID="2025.03.19"
`,
osName: "TestOS",
version: "1.2",
},
{
name: "fallback to build id",
content: `NAME="EndeavourOS"
PRETTY_NAME="EndeavourOS"
BUILD_ID="2025.03.19"
`,
osName: "EndeavourOS",
version: "2025.03.19",
},
} {
t.Run(tc.name, func(t *testing.T) {
path := filepath.Join(t.TempDir(), "os-release")
err := stdos.WriteFile(path, []byte(tc.content), 0o600)
assert.NoError(t, err)

name, version := parseOSRelease(path)
assert.Equal(t, tc.osName, name)
assert.Equal(t, tc.version, version)
})
}
}

func TestGatherLinux(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("Skipping Linux-specific test")
Expand Down
Loading