diff --git a/pkg/parsers/operatingsystem/operatingsystem_linux.go b/pkg/parsers/operatingsystem/operatingsystem_linux.go index b251d6a..2fefcab 100644 --- a/pkg/parsers/operatingsystem/operatingsystem_linux.go +++ b/pkg/parsers/operatingsystem/operatingsystem_linux.go @@ -26,6 +26,24 @@ var ( // GetOperatingSystem gets the name of the current operating system. func GetOperatingSystem() (string, error) { + if prettyName, err := getValueFromOsRelease("PRETTY_NAME"); err != nil { + return "", err + } else if prettyName != "" { + return prettyName, nil + } + + // If not set, defaults to PRETTY_NAME="Linux" + // c.f. http://www.freedesktop.org/software/systemd/man/os-release.html + return "Linux", nil +} + +// GetOperatingSystemVersion gets the version of the current operating system, as a string. +func GetOperatingSystemVersion() (string, error) { + return getValueFromOsRelease("VERSION_ID") +} + +// parses the os-release file and returns the value associated with `key` +func getValueFromOsRelease(key string) (string, error) { osReleaseFile, err := os.Open(etcOsRelease) if err != nil { if !os.IsNotExist(err) { @@ -38,28 +56,25 @@ func GetOperatingSystem() (string, error) { } defer osReleaseFile.Close() - var prettyName string + var value string + keyWithTrailingEqual := key + "=" scanner := bufio.NewScanner(osReleaseFile) for scanner.Scan() { line := scanner.Text() - if strings.HasPrefix(line, "PRETTY_NAME=") { + if strings.HasPrefix(line, keyWithTrailingEqual) { data := strings.SplitN(line, "=", 2) - prettyNames, err := shellwords.Parse(data[1]) + values, err := shellwords.Parse(data[1]) if err != nil { - return "", fmt.Errorf("PRETTY_NAME is invalid: %s", err.Error()) + return "", fmt.Errorf("%s is invalid: %s", key, err.Error()) } - if len(prettyNames) != 1 { - return "", fmt.Errorf("PRETTY_NAME needs to be enclosed by quotes if they have spaces: %s", data[1]) + if len(values) != 1 { + return "", fmt.Errorf("%s needs to be enclosed by quotes if they have spaces: %s", key, data[1]) } - prettyName = prettyNames[0] + value = values[0] } } - if prettyName != "" { - return prettyName, nil - } - // If not set, defaults to PRETTY_NAME="Linux" - // c.f. http://www.freedesktop.org/software/systemd/man/os-release.html - return "Linux", nil + + return value, nil } // IsContainerized returns true if we are running inside a container. diff --git a/pkg/parsers/operatingsystem/operatingsystem_unix_test.go b/pkg/parsers/operatingsystem/operatingsystem_linux_test.go similarity index 64% rename from pkg/parsers/operatingsystem/operatingsystem_unix_test.go rename to pkg/parsers/operatingsystem/operatingsystem_linux_test.go index d10ed4c..ec9b05c 100644 --- a/pkg/parsers/operatingsystem/operatingsystem_unix_test.go +++ b/pkg/parsers/operatingsystem/operatingsystem_linux_test.go @@ -7,43 +7,41 @@ import ( "os" "path/filepath" "testing" + + "gotest.tools/assert" ) -func TestGetOperatingSystem(t *testing.T) { - var backup = etcOsRelease +type EtcReleaseParsingTest struct { + name string + content string + expected string + expectedErr string +} - invalids := []struct { - content string - errorExpected string - }{ +func TestGetOperatingSystem(t *testing.T) { + tests := []EtcReleaseParsingTest{ { - `PRETTY_NAME=Source Mage GNU/Linux + content: `PRETTY_NAME=Source Mage GNU/Linux PRETTY_NAME=Ubuntu 14.04.LTS`, - "PRETTY_NAME needs to be enclosed by quotes if they have spaces: Source Mage GNU/Linux", + expectedErr: "PRETTY_NAME needs to be enclosed by quotes if they have spaces: Source Mage GNU/Linux", }, { - `PRETTY_NAME="Ubuntu Linux + content: `PRETTY_NAME="Ubuntu Linux PRETTY_NAME=Ubuntu 14.04.LTS`, - "PRETTY_NAME is invalid: invalid command line string", + expectedErr: "PRETTY_NAME is invalid: invalid command line string", }, { - `PRETTY_NAME=Ubuntu' + content: `PRETTY_NAME=Ubuntu' PRETTY_NAME=Ubuntu 14.04.LTS`, - "PRETTY_NAME is invalid: invalid command line string", + expectedErr: "PRETTY_NAME is invalid: invalid command line string", }, { - `PRETTY_NAME' + content: `PRETTY_NAME' PRETTY_NAME=Ubuntu 14.04.LTS`, - "PRETTY_NAME needs to be enclosed by quotes if they have spaces: Ubuntu 14.04.LTS", + expectedErr: "PRETTY_NAME needs to be enclosed by quotes if they have spaces: Ubuntu 14.04.LTS", }, - } - - valids := []struct { - content string - expected string - }{ { - `NAME="Ubuntu" + content: `NAME="Ubuntu" PRETTY_NAME_AGAIN="Ubuntu 14.04.LTS" VERSION="14.04, Trusty Tahr" ID=ubuntu @@ -52,10 +50,10 @@ VERSION_ID="14.04" HOME_URL="http://www.ubuntu.com/" SUPPORT_URL="http://help.ubuntu.com/" BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"`, - "Linux", + expected: "Linux", }, { - `NAME="Ubuntu" + content: `NAME="Ubuntu" VERSION="14.04, Trusty Tahr" ID=ubuntu ID_LIKE=debian @@ -63,10 +61,10 @@ VERSION_ID="14.04" HOME_URL="http://www.ubuntu.com/" SUPPORT_URL="http://help.ubuntu.com/" BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"`, - "Linux", + expected: "Linux", }, { - `NAME=Gentoo + content: `NAME=Gentoo ID=gentoo PRETTY_NAME="Gentoo/Linux" ANSI_COLOR="1;32" @@ -74,10 +72,10 @@ HOME_URL="http://www.gentoo.org/" SUPPORT_URL="http://www.gentoo.org/main/en/support.xml" BUG_REPORT_URL="https://bugs.gentoo.org/" `, - "Gentoo/Linux", + expected: "Gentoo/Linux", }, { - `NAME="Ubuntu" + content: `NAME="Ubuntu" VERSION="14.04, Trusty Tahr" ID=ubuntu ID_LIKE=debian @@ -86,28 +84,77 @@ VERSION_ID="14.04" HOME_URL="http://www.ubuntu.com/" SUPPORT_URL="http://help.ubuntu.com/" BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"`, - "Ubuntu 14.04 LTS", + expected: "Ubuntu 14.04 LTS", }, { - `NAME="Ubuntu" + content: `NAME="Ubuntu" VERSION="14.04, Trusty Tahr" ID=ubuntu ID_LIKE=debian PRETTY_NAME='Ubuntu 14.04 LTS'`, - "Ubuntu 14.04 LTS", + expected: "Ubuntu 14.04 LTS", }, { - `PRETTY_NAME=Source + content: `PRETTY_NAME=Source NAME="Source Mage"`, - "Source", + expected: "Source", }, { - `PRETTY_NAME=Source + content: `PRETTY_NAME=Source PRETTY_NAME="Source Mage"`, - "Source Mage", + expected: "Source Mage", }, } + runEtcReleaseParsingTests(t, tests, GetOperatingSystem) +} + +func TestGetOperatingSystemVersion(t *testing.T) { + tests := []EtcReleaseParsingTest{ + { + name: "invalid version id", + content: `VERSION_ID="18.04 +VERSION_ID=18.04`, + expectedErr: "VERSION_ID is invalid: invalid command line string", + }, + { + name: "ubuntu 14.04", + content: `NAME="Ubuntu" +PRETTY_NAME="Ubuntu 14.04.LTS" +VERSION="14.04, Trusty Tahr" +ID=ubuntu +ID_LIKE=debian +VERSION_ID="14.04" +HOME_URL="http://www.ubuntu.com/" +SUPPORT_URL="http://help.ubuntu.com/" +BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"`, + expected: "14.04", + }, + { + name: "gentoo", + content: `NAME=Gentoo +ID=gentoo +PRETTY_NAME="Gentoo/Linux" +ANSI_COLOR="1;32" +HOME_URL="http://www.gentoo.org/" +SUPPORT_URL="http://www.gentoo.org/main/en/support.xml" +BUG_REPORT_URL="https://bugs.gentoo.org/" +`, + }, + { + name: "dual version id", + content: `VERSION_ID="14.04" +VERSION_ID=18.04`, + expected: "18.04", + }, + } + + runEtcReleaseParsingTests(t, tests, GetOperatingSystemVersion) +} + +func runEtcReleaseParsingTests(t *testing.T, tests []EtcReleaseParsingTest, parsingFunc func() (string, error)) { + var backup = etcOsRelease + dir := os.TempDir() etcOsRelease = filepath.Join(dir, "etcOsRelease") @@ -116,24 +163,19 @@ PRETTY_NAME="Source Mage"`, etcOsRelease = backup }() - for _, elt := range invalids { - if err := ioutil.WriteFile(etcOsRelease, []byte(elt.content), 0600); err != nil { - t.Fatalf("failed to write to %s: %v", etcOsRelease, err) - } - s, err := GetOperatingSystem() - if err == nil || err.Error() != elt.errorExpected { - t.Fatalf("Expected an error %q, got %q (err: %v)", elt.errorExpected, s, err) - } - } - - for _, elt := range valids { - if err := ioutil.WriteFile(etcOsRelease, []byte(elt.content), 0600); err != nil { - t.Fatalf("failed to write to %s: %v", etcOsRelease, err) - } - s, err := GetOperatingSystem() - if err != nil || s != elt.expected { - t.Fatalf("Expected %q, got %q (err: %v)", elt.expected, s, err) - } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if err := ioutil.WriteFile(etcOsRelease, []byte(test.content), 0600); err != nil { + t.Fatalf("failed to write to %s: %v", etcOsRelease, err) + } + s, err := parsingFunc() + if test.expectedErr == "" { + assert.NilError(t, err) + } else { + assert.Error(t, err, test.expectedErr) + } + assert.Equal(t, s, test.expected) + }) } } diff --git a/pkg/parsers/operatingsystem/operatingsystem_unix.go b/pkg/parsers/operatingsystem/operatingsystem_unix.go index f4792d3..fcc693d 100644 --- a/pkg/parsers/operatingsystem/operatingsystem_unix.go +++ b/pkg/parsers/operatingsystem/operatingsystem_unix.go @@ -4,6 +4,7 @@ package operatingsystem // import "github.com/docker/docker/pkg/parsers/operatin import ( "errors" + "fmt" "os/exec" ) @@ -17,6 +18,12 @@ func GetOperatingSystem() (string, error) { return string(osName), nil } +// GetOperatingSystemVersion gets the version of the current operating system, as a string. +func GetOperatingSystemVersion() (string, error) { + // there's no standard unix way of getting this, sadly... + return "", fmt.Error("Unsupported on generic unix") +} + // IsContainerized returns true if we are running inside a container. // No-op on FreeBSD and Darwin, always returns false. func IsContainerized() (bool, error) { diff --git a/pkg/parsers/operatingsystem/operatingsystem_windows.go b/pkg/parsers/operatingsystem/operatingsystem_windows.go index 372de51..a05bc76 100644 --- a/pkg/parsers/operatingsystem/operatingsystem_windows.go +++ b/pkg/parsers/operatingsystem/operatingsystem_windows.go @@ -3,45 +3,57 @@ package operatingsystem // import "github.com/docker/docker/pkg/parsers/operatin import ( "fmt" + "github.com/docker/docker/pkg/system" "golang.org/x/sys/windows/registry" ) // GetOperatingSystem gets the name of the current operating system. func GetOperatingSystem() (string, error) { - - // Default return value - ret := "Unknown Operating System" - - k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE) - if err != nil { - return ret, err + os, err := withCurrentVersionRegistryKey(func(key registry.Key) (os string, err error) { + if os, _, err = key.GetStringValue("ProductName"); err != nil { + return "", err + } + + releaseId, _, err := key.GetStringValue("ReleaseId") + if err != nil { + return + } + os = fmt.Sprintf("%s Version %s", os, releaseId) + + buildNumber, _, err := key.GetStringValue("CurrentBuildNumber") + if err != nil { + return + } + ubr, _, err := key.GetIntegerValue("UBR") + if err != nil { + return + } + os = fmt.Sprintf("%s (OS Build %s.%d)", os, buildNumber, ubr) + + return + }) + + if os == "" { + // Default return value + os = "Unknown Operating System" } - defer k.Close() - pn, _, err := k.GetStringValue("ProductName") - if err != nil { - return ret, err - } - ret = pn - - ri, _, err := k.GetStringValue("ReleaseId") - if err != nil { - return ret, err - } - ret = fmt.Sprintf("%s Version %s", ret, ri) - - cbn, _, err := k.GetStringValue("CurrentBuildNumber") - if err != nil { - return ret, err - } + return os, err +} - ubr, _, err := k.GetIntegerValue("UBR") +func withCurrentVersionRegistryKey(f func(registry.Key) (string, error)) (string, error) { + key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE) if err != nil { - return ret, err + return "", err } - ret = fmt.Sprintf("%s (OS Build %s.%d)", ret, cbn, ubr) + defer key.Close() + return f(key) +} - return ret, nil +// GetOperatingSystemVersion gets the version of the current operating system, as a string. +func GetOperatingSystemVersion() (string, error) { + version := system.GetOSVersion() + return fmt.Sprintf("%d.%d.%d", version.MajorVersion, version.MinorVersion, version.Build), nil } // IsContainerized returns true if we are running inside a container.