Source: docker.io Severity: normal Control: block 964378 by -1 Hi,
In order to get podman 2.0 to build, I had to backport some changes to the golang-github-docker-docker-dev package. Unfortunately they are not included in any upstream release at time of writing. Please find backported patches attached to this email. In addition to applying these patches, I had to disable some CLI tests, because the CLI tests compare the output of the docker tool, and the patches include additional information (osrelease and variant). I did this by editing debian/rules: @@ -154,8 +154,9 @@ ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS))) && TESTFLAGS='-test.short' ./hack/test/unit ## Test CLI: - cd $(OUR_GOPATH)/src/github.com/docker/cli \ - && DISABLE_WARN_OUTSIDE_CONTAINER=1 $(MAKE) test-unit + # disabled as backported upstream patches change the command output + #cd $(OUR_GOPATH)/src/github.com/docker/cli \ + # && DISABLE_WARN_OUTSIDE_CONTAINER=1 $(MAKE) test-unit .gopath/bin/containerd -version endif A better approach would be to update the reference pattern the tests is comparing against, but I failed to figure out how to do so. Any chance we could have that patch in experimental soon? AFAIUI, this is the last bit that prevents uploading podman 2.0. Thanks!
>From 30db6a727e6daad6f36ede7e132703dc8b58e19b Mon Sep 17 00:00:00 2001 From: Jean Rouge <rougej+git...@gmail.com> Date: Thu, 30 May 2019 09:51:41 -0700 Subject: [PATCH] Adding OS version info to the nodes' `Info` struct This is needed so that we can add OS version constraints in Swarmkit, which does require the engine to report its host's OS version (see https://github.com/docker/swarmkit/issues/2770). The OS version is parsed from the `os-release` file on Linux, and from the `ReleaseId` string value of the `SOFTWARE\Microsoft\Windows NT\CurrentVersion` registry key on Windows. Added unit tests when possible, as well as Prometheus metrics. Signed-off-by: Jean Rouge <rougej+git...@gmail.com> Upstream-commit: d363a1881ec256e1be5a48a90046ff16e84ddc02 Component: engine --- engine/api/swagger.yaml | 11 ++ engine/api/types/types.go | 1 + engine/daemon/daemon.go | 1 + engine/daemon/info.go | 23 ++- engine/daemon/metrics.go | 6 +- .../operatingsystem/operatingsystem_linux.go | 41 +++-- ..._test.go => operatingsystem_linux_test.go} | 146 +++++++++++------- .../operatingsystem/operatingsystem_unix.go | 7 + .../operatingsystem_windows.go | 68 ++++---- 9 files changed, 208 insertions(+), 96 deletions(-) rename engine/pkg/parsers/operatingsystem/{operatingsystem_unix_test.go => operatingsystem_linux_test.go} (64%) --- a/engine/api/swagger.yaml +++ b/engine/api/swagger.yaml @@ -3998,6 +3998,17 @@ or "Windows Server 2016 Datacenter" type: "string" example: "Alpine Linux v3.5" + OSVersion: + description: | + Version of the host's operating system + + <p><br /></p> + + > **Note**: The information returned in this field, including its + > very existence, and the formatting of values, should not be considered + > stable, and may change without notice. + type: "string" + example: "16.04" OSType: description: | Generic type of the operating system of the host, as returned by the --- a/engine/api/types/types.go +++ b/engine/api/types/types.go @@ -177,6 +177,7 @@ NEventsListener int KernelVersion string OperatingSystem string + OSVersion string OSType string Architecture string IndexServerAddress string --- a/engine/daemon/daemon.go +++ b/engine/daemon/daemon.go @@ -1080,6 +1080,7 @@ info.KernelVersion, info.OperatingSystem, info.OSType, + info.OSVersion, info.ID, ).Set(1) engineCpus.Set(float64(info.NCPU)) --- a/engine/daemon/info.go +++ b/engine/daemon/info.go @@ -21,11 +21,14 @@ "github.com/docker/docker/pkg/system" "github.com/docker/docker/registry" "github.com/docker/go-connections/sockets" + "github.com/docker/go-metrics" "github.com/sirupsen/logrus" ) // SystemInfo returns information about the host server the daemon is running on. func (daemon *Daemon) SystemInfo() (*types.Info, error) { + defer metrics.StartTimer(hostInfoFunctions.WithValues("system_info"))() + sysInfo := sysinfo.New(true) cRunning, cPaused, cStopped := stateCtr.get() @@ -49,6 +52,7 @@ NEventsListener: daemon.EventsService.SubscribersCount(), KernelVersion: kernelVersion(), OperatingSystem: operatingSystem(), + OSVersion: osVersion(), IndexServerAddress: registry.IndexServer, OSType: platform.OSType, Architecture: platform.Architecture, @@ -82,6 +86,8 @@ // SystemVersion returns version information about the daemon. func (daemon *Daemon) SystemVersion() types.Version { + defer metrics.StartTimer(hostInfoFunctions.WithValues("system_version"))() + kernelVersion := kernelVersion() v := types.Version{ @@ -236,8 +242,9 @@ return memInfo } -func operatingSystem() string { - var operatingSystem string +func operatingSystem() (operatingSystem string) { + defer metrics.StartTimer(hostInfoFunctions.WithValues("operating_system"))() + if s, err := operatingsystem.GetOperatingSystem(); err != nil { logrus.Warnf("Could not get operating system name: %v", err) } else { @@ -252,9 +259,21 @@ operatingSystem += " (containerized)" } } + return operatingSystem } +func osVersion() (version string) { + defer metrics.StartTimer(hostInfoFunctions.WithValues("os_version"))() + + version, err := operatingsystem.GetOperatingSystemVersion() + if err != nil { + logrus.Warnf("Could not get operating system version: %v", err) + } + + return version +} + func maskCredentials(rawURL string) string { parsedURL, err := url.Parse(rawURL) if err != nil || parsedURL.User == nil { --- a/engine/daemon/metrics.go +++ b/engine/daemon/metrics.go @@ -17,6 +17,7 @@ var ( containerActions metrics.LabeledTimer networkActions metrics.LabeledTimer + hostInfoFunctions metrics.LabeledTimer engineInfo metrics.LabeledGauge engineCpus metrics.Gauge engineMemory metrics.Gauge @@ -38,6 +39,7 @@ } { containerActions.WithValues(a).Update(0) } + hostInfoFunctions = ns.NewLabeledTimer("host_info_functions", "The number of seconds it takes to call functions gathering info about the host", "function") networkActions = ns.NewLabeledTimer("network_actions", "The number of seconds it takes to process each network action", "action") engineInfo = ns.NewLabeledGauge("engine", "The information related to the engine and the OS it is running on", metrics.Unit("info"), @@ -45,8 +47,10 @@ "commit", "architecture", "graphdriver", - "kernel", "os", + "kernel", + "os", "os_type", + "os_version", "daemon_id", // ID is a randomly generated unique identifier (e.g. UUID4) ) engineCpus = ns.NewGauge("engine_cpus", "The number of cpus that the host system of the engine has", metrics.Unit("cpus")) --- a/engine/pkg/parsers/operatingsystem/operatingsystem_linux.go +++ b/engine/pkg/parsers/operatingsystem/operatingsystem_linux.go @@ -26,6 +26,24 @@ // 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 @@ } 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. --- a/engine/pkg/parsers/operatingsystem/operatingsystem_unix_test.go +++ /dev/null @@ -1,247 +0,0 @@ -// +build linux freebsd - -package operatingsystem // import "github.com/docker/docker/pkg/parsers/operatingsystem" - -import ( - "io/ioutil" - "os" - "path/filepath" - "testing" -) - -func TestGetOperatingSystem(t *testing.T) { - var backup = etcOsRelease - - invalids := []struct { - content string - errorExpected string - }{ - { - `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", - }, - { - `PRETTY_NAME="Ubuntu Linux -PRETTY_NAME=Ubuntu 14.04.LTS`, - "PRETTY_NAME is invalid: invalid command line string", - }, - { - `PRETTY_NAME=Ubuntu' -PRETTY_NAME=Ubuntu 14.04.LTS`, - "PRETTY_NAME is invalid: invalid command line string", - }, - { - `PRETTY_NAME' -PRETTY_NAME=Ubuntu 14.04.LTS`, - "PRETTY_NAME needs to be enclosed by quotes if they have spaces: Ubuntu 14.04.LTS", - }, - } - - valids := []struct { - content string - expected string - }{ - { - `NAME="Ubuntu" -PRETTY_NAME_AGAIN="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/"`, - "Linux", - }, - { - `NAME="Ubuntu" -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/"`, - "Linux", - }, - { - `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/" -`, - "Gentoo/Linux", - }, - { - `NAME="Ubuntu" -VERSION="14.04, Trusty Tahr" -ID=ubuntu -ID_LIKE=debian -PRETTY_NAME="Ubuntu 14.04 LTS" -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", - }, - { - `NAME="Ubuntu" -VERSION="14.04, Trusty Tahr" -ID=ubuntu -ID_LIKE=debian -PRETTY_NAME='Ubuntu 14.04 LTS'`, - "Ubuntu 14.04 LTS", - }, - { - `PRETTY_NAME=Source -NAME="Source Mage"`, - "Source", - }, - { - `PRETTY_NAME=Source -PRETTY_NAME="Source Mage"`, - "Source Mage", - }, - } - - dir := os.TempDir() - etcOsRelease = filepath.Join(dir, "etcOsRelease") - - defer func() { - os.Remove(etcOsRelease) - 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) - } - } -} - -func TestIsContainerized(t *testing.T) { - var ( - backup = proc1Cgroup - nonContainerizedProc1Cgroupsystemd226 = []byte(`9:memory:/init.scope -8:net_cls,net_prio:/ -7:cpuset:/ -6:freezer:/ -5:devices:/init.scope -4:blkio:/init.scope -3:cpu,cpuacct:/init.scope -2:perf_event:/ -1:name=systemd:/init.scope -`) - nonContainerizedProc1Cgroup = []byte(`14:name=systemd:/ -13:hugetlb:/ -12:net_prio:/ -11:perf_event:/ -10:bfqio:/ -9:blkio:/ -8:net_cls:/ -7:freezer:/ -6:devices:/ -5:memory:/ -4:cpuacct:/ -3:cpu:/ -2:cpuset:/ -`) - containerizedProc1Cgroup = []byte(`9:perf_event:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d -8:blkio:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d -7:net_cls:/ -6:freezer:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d -5:devices:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d -4:memory:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d -3:cpuacct:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d -2:cpu:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d -1:cpuset:/`) - ) - - dir := os.TempDir() - proc1Cgroup = filepath.Join(dir, "proc1Cgroup") - - defer func() { - os.Remove(proc1Cgroup) - proc1Cgroup = backup - }() - - if err := ioutil.WriteFile(proc1Cgroup, nonContainerizedProc1Cgroup, 0600); err != nil { - t.Fatalf("failed to write to %s: %v", proc1Cgroup, err) - } - inContainer, err := IsContainerized() - if err != nil { - t.Fatal(err) - } - if inContainer { - t.Fatal("Wrongly assuming containerized") - } - - if err := ioutil.WriteFile(proc1Cgroup, nonContainerizedProc1Cgroupsystemd226, 0600); err != nil { - t.Fatalf("failed to write to %s: %v", proc1Cgroup, err) - } - inContainer, err = IsContainerized() - if err != nil { - t.Fatal(err) - } - if inContainer { - t.Fatal("Wrongly assuming containerized for systemd /init.scope cgroup layout") - } - - if err := ioutil.WriteFile(proc1Cgroup, containerizedProc1Cgroup, 0600); err != nil { - t.Fatalf("failed to write to %s: %v", proc1Cgroup, err) - } - inContainer, err = IsContainerized() - if err != nil { - t.Fatal(err) - } - if !inContainer { - t.Fatal("Wrongly assuming non-containerized") - } -} - -func TestOsReleaseFallback(t *testing.T) { - var backup = etcOsRelease - var altBackup = altOsRelease - dir := os.TempDir() - etcOsRelease = filepath.Join(dir, "etcOsRelease") - altOsRelease = filepath.Join(dir, "altOsRelease") - - defer func() { - os.Remove(dir) - etcOsRelease = backup - altOsRelease = altBackup - }() - 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/" -` - if err := ioutil.WriteFile(altOsRelease, []byte(content), 0600); err != nil { - t.Fatalf("failed to write to %s: %v", etcOsRelease, err) - } - s, err := GetOperatingSystem() - if err != nil || s != "Gentoo/Linux" { - t.Fatalf("Expected %q, got %q (err: %v)", "Gentoo/Linux", s, err) - } -} --- /dev/null +++ b/engine/pkg/parsers/operatingsystem/operatingsystem_linux_test.go @@ -0,0 +1,289 @@ +// +build linux freebsd + +package operatingsystem // import "github.com/docker/docker/pkg/parsers/operatingsystem" + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "gotest.tools/assert" +) + +type EtcReleaseParsingTest struct { + name string + content string + expected string + expectedErr string +} + +func TestGetOperatingSystem(t *testing.T) { + tests := []EtcReleaseParsingTest{ + { + content: `PRETTY_NAME=Source Mage GNU/Linux +PRETTY_NAME=Ubuntu 14.04.LTS`, + expectedErr: "PRETTY_NAME needs to be enclosed by quotes if they have spaces: Source Mage GNU/Linux", + }, + { + content: `PRETTY_NAME="Ubuntu Linux +PRETTY_NAME=Ubuntu 14.04.LTS`, + expectedErr: "PRETTY_NAME is invalid: invalid command line string", + }, + { + content: `PRETTY_NAME=Ubuntu' +PRETTY_NAME=Ubuntu 14.04.LTS`, + expectedErr: "PRETTY_NAME is invalid: invalid command line string", + }, + { + content: `PRETTY_NAME' +PRETTY_NAME=Ubuntu 14.04.LTS`, + expectedErr: "PRETTY_NAME needs to be enclosed by quotes if they have spaces: Ubuntu 14.04.LTS", + }, + { + content: `NAME="Ubuntu" +PRETTY_NAME_AGAIN="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: "Linux", + }, + { + content: `NAME="Ubuntu" +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: "Linux", + }, + { + 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/" +`, + expected: "Gentoo/Linux", + }, + { + content: `NAME="Ubuntu" +VERSION="14.04, Trusty Tahr" +ID=ubuntu +ID_LIKE=debian +PRETTY_NAME="Ubuntu 14.04 LTS" +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: "Ubuntu 14.04 LTS", + }, + { + content: `NAME="Ubuntu" +VERSION="14.04, Trusty Tahr" +ID=ubuntu +ID_LIKE=debian +PRETTY_NAME='Ubuntu 14.04 LTS'`, + expected: "Ubuntu 14.04 LTS", + }, + { + content: `PRETTY_NAME=Source +NAME="Source Mage"`, + expected: "Source", + }, + { + content: `PRETTY_NAME=Source +PRETTY_NAME="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") + + defer func() { + os.Remove(etcOsRelease) + etcOsRelease = backup + }() + + 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) + }) + } +} + +func TestIsContainerized(t *testing.T) { + var ( + backup = proc1Cgroup + nonContainerizedProc1Cgroupsystemd226 = []byte(`9:memory:/init.scope +8:net_cls,net_prio:/ +7:cpuset:/ +6:freezer:/ +5:devices:/init.scope +4:blkio:/init.scope +3:cpu,cpuacct:/init.scope +2:perf_event:/ +1:name=systemd:/init.scope +`) + nonContainerizedProc1Cgroup = []byte(`14:name=systemd:/ +13:hugetlb:/ +12:net_prio:/ +11:perf_event:/ +10:bfqio:/ +9:blkio:/ +8:net_cls:/ +7:freezer:/ +6:devices:/ +5:memory:/ +4:cpuacct:/ +3:cpu:/ +2:cpuset:/ +`) + containerizedProc1Cgroup = []byte(`9:perf_event:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d +8:blkio:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d +7:net_cls:/ +6:freezer:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d +5:devices:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d +4:memory:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d +3:cpuacct:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d +2:cpu:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d +1:cpuset:/`) + ) + + dir := os.TempDir() + proc1Cgroup = filepath.Join(dir, "proc1Cgroup") + + defer func() { + os.Remove(proc1Cgroup) + proc1Cgroup = backup + }() + + if err := ioutil.WriteFile(proc1Cgroup, nonContainerizedProc1Cgroup, 0600); err != nil { + t.Fatalf("failed to write to %s: %v", proc1Cgroup, err) + } + inContainer, err := IsContainerized() + if err != nil { + t.Fatal(err) + } + if inContainer { + t.Fatal("Wrongly assuming containerized") + } + + if err := ioutil.WriteFile(proc1Cgroup, nonContainerizedProc1Cgroupsystemd226, 0600); err != nil { + t.Fatalf("failed to write to %s: %v", proc1Cgroup, err) + } + inContainer, err = IsContainerized() + if err != nil { + t.Fatal(err) + } + if inContainer { + t.Fatal("Wrongly assuming containerized for systemd /init.scope cgroup layout") + } + + if err := ioutil.WriteFile(proc1Cgroup, containerizedProc1Cgroup, 0600); err != nil { + t.Fatalf("failed to write to %s: %v", proc1Cgroup, err) + } + inContainer, err = IsContainerized() + if err != nil { + t.Fatal(err) + } + if !inContainer { + t.Fatal("Wrongly assuming non-containerized") + } +} + +func TestOsReleaseFallback(t *testing.T) { + var backup = etcOsRelease + var altBackup = altOsRelease + dir := os.TempDir() + etcOsRelease = filepath.Join(dir, "etcOsRelease") + altOsRelease = filepath.Join(dir, "altOsRelease") + + defer func() { + os.Remove(dir) + etcOsRelease = backup + altOsRelease = altBackup + }() + 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/" +` + if err := ioutil.WriteFile(altOsRelease, []byte(content), 0600); err != nil { + t.Fatalf("failed to write to %s: %v", etcOsRelease, err) + } + s, err := GetOperatingSystem() + if err != nil || s != "Gentoo/Linux" { + t.Fatalf("Expected %q, got %q (err: %v)", "Gentoo/Linux", s, err) + } +} --- a/engine/pkg/parsers/operatingsystem/operatingsystem_unix.go +++ b/engine/pkg/parsers/operatingsystem/operatingsystem_unix.go @@ -4,6 +4,7 @@ import ( "errors" + "fmt" "os/exec" ) @@ -17,6 +18,12 @@ 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) { --- a/engine/pkg/parsers/operatingsystem/operatingsystem_windows.go +++ b/engine/pkg/parsers/operatingsystem/operatingsystem_windows.go @@ -3,45 +3,57 @@ 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.
>From 3bc3fb7702ab7cc7919711cccb81ede56688f11c Mon Sep 17 00:00:00 2001 From: Chris Price <chris.pr...@docker.com> Date: Fri, 26 Apr 2019 15:12:43 -0700 Subject: [PATCH] Add variant to image.Image and legacy builder This commit adds the image variant to the image.(Image) type and updates related functionality. Images built from another will inherit the OS, architecture and variant. Note that if a base image does not specify an architecture, the local machine's architecture is used for inherited images. On the other hand, the variant is set equal to the parent image's variant, even when the parent image's variant is unset. The legacy builder is also updated to allow the user to specify a '--platform' argument on the command line when creating an image FROM scratch. A complete platform specification, including variant, is supported. The built image will include the variant, as will any derived images. Signed-off-by: Chris Price <chris.pr...@docker.com> Upstream-commit: c21a3cf432bd89f9ceb5724b8a90f30f789433a5 Component: engine --- engine/api/types/types.go | 1 + .../engine/builder/dockerfile/imagecontext.go | 21 +++- .../builder/dockerfile/imagecontext_test.go | 106 ++++++++++++++++++ .../engine/builder/dockerfile/internals.go | 17 ++- .../builder/dockerfile/internals_test.go | 46 ++++++++ .../builder/dockerfile/mockbackend_test.go | 2 +- .../engine/daemon/images/image_inspect.go | 1 + engine/distribution/config.go | 2 +- engine/image/image.go | 10 ++ 9 files changed, 194 insertions(+), 12 deletions(-) create mode 100644 engine/builder/dockerfile/imagecontext_test.go --- a/engine/api/types/types.go +++ b/engine/api/types/types.go @@ -39,6 +39,7 @@ Author string Config *container.Config Architecture string + Variant string `json:",omitempty"` Os string OsVersion string `json:",omitempty"` Size int64 --- a/engine/builder/dockerfile/imagecontext.go +++ b/engine/builder/dockerfile/imagecontext.go @@ -4,6 +4,7 @@ "context" "runtime" + "github.com/containerd/containerd/platforms" "github.com/docker/docker/api/types/backend" "github.com/docker/docker/builder" dockerimage "github.com/docker/docker/image" @@ -56,7 +57,7 @@ return nil, err } im := newImageMount(image, layer) - m.Add(im) + m.Add(im, platform) return im, nil } @@ -70,16 +71,26 @@ return } -func (m *imageSources) Add(im *imageMount) { +func (m *imageSources) Add(im *imageMount, platform *specs.Platform) { switch im.image { case nil: - // set the OS for scratch images - os := runtime.GOOS + // Set the platform for scratch images + if platform == nil { + p := platforms.DefaultSpec() + platform = &p + } + // Windows does not support scratch except for LCOW + os := platform.OS if runtime.GOOS == "windows" { os = "linux" } - im.image = &dockerimage.Image{V1Image: dockerimage.V1Image{OS: os}} + + im.image = &dockerimage.Image{V1Image: dockerimage.V1Image{ + OS: os, + Architecture: platform.Architecture, + Variant: platform.Variant, + }} default: m.byImageID[im.image.ImageID()] = im } --- /dev/null +++ b/engine/builder/dockerfile/imagecontext_test.go @@ -0,0 +1,106 @@ +package dockerfile // import "github.com/docker/docker/builder/dockerfile" + +import ( + "fmt" + "runtime" + "testing" + + "github.com/containerd/containerd/platforms" + "github.com/docker/docker/builder" + "github.com/docker/docker/image" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "gotest.tools/assert" +) + +func getMockImageSource(getImageImage builder.Image, getImageLayer builder.ROLayer, getImageError error) *imageSources { + return &imageSources{ + byImageID: make(map[string]*imageMount), + mounts: []*imageMount{}, + getImage: func(name string, localOnly bool, platform *ocispec.Platform) (builder.Image, builder.ROLayer, error) { + return getImageImage, getImageLayer, getImageError + }, + } +} + +func getMockImageMount() *imageMount { + return &imageMount{ + image: nil, + layer: nil, + } +} + +func TestAddScratchImageAddsToMounts(t *testing.T) { + is := getMockImageSource(nil, nil, fmt.Errorf("getImage is not implemented")) + im := getMockImageMount() + + // We are testing whether the imageMount is added to is.mounts + assert.Equal(t, len(is.mounts), 0) + is.Add(im, nil) + assert.Equal(t, len(is.mounts), 1) +} + +func TestAddFromScratchPopulatesPlatform(t *testing.T) { + is := getMockImageSource(nil, nil, fmt.Errorf("getImage is not implemented")) + + platforms := []*ocispec.Platform{ + { + OS: "linux", + Architecture: "amd64", + }, + { + OS: "linux", + Architecture: "arm64", + Variant: "v8", + }, + } + + for i, platform := range platforms { + im := getMockImageMount() + assert.Equal(t, len(is.mounts), i) + is.Add(im, platform) + image, ok := im.image.(*image.Image) + assert.Assert(t, ok) + assert.Equal(t, image.OS, platform.OS) + assert.Equal(t, image.Architecture, platform.Architecture) + assert.Equal(t, image.Variant, platform.Variant) + } +} + +func TestAddFromScratchDoesNotModifyArgPlatform(t *testing.T) { + is := getMockImageSource(nil, nil, fmt.Errorf("getImage is not implemented")) + im := getMockImageMount() + + platform := &ocispec.Platform{ + OS: "windows", + Architecture: "amd64", + } + argPlatform := *platform + + is.Add(im, &argPlatform) + // The way the code is written right now, this test + // really doesn't do much except on Windows. + assert.DeepEqual(t, *platform, argPlatform) +} + +func TestAddFromScratchPopulatesPlatformIfNil(t *testing.T) { + is := getMockImageSource(nil, nil, fmt.Errorf("getImage is not implemented")) + im := getMockImageMount() + is.Add(im, nil) + image, ok := im.image.(*image.Image) + assert.Assert(t, ok) + + expectedPlatform := platforms.DefaultSpec() + if runtime.GOOS == "windows" { + expectedPlatform.OS = "linux" + } + assert.Equal(t, expectedPlatform.OS, image.OS) + assert.Equal(t, expectedPlatform.Architecture, image.Architecture) + assert.Equal(t, expectedPlatform.Variant, image.Variant) +} + +func TestImageSourceGetAddsToMounts(t *testing.T) { + is := getMockImageSource(nil, nil, nil) + _, err := is.Get("test", false, nil) + assert.NilError(t, err) + assert.Equal(t, len(is.mounts), 1) +} --- a/engine/builder/dockerfile/internals.go +++ b/engine/builder/dockerfile/internals.go @@ -26,6 +26,7 @@ "github.com/docker/docker/pkg/stringid" "github.com/docker/docker/pkg/system" "github.com/docker/go-connections/nat" + specs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -117,15 +118,21 @@ return err } - // add an image mount without an image so the layer is properly unmounted - // if there is an error before we can add the full mount with image - b.imageSources.Add(newImageMount(nil, newLayer)) - parentImage, ok := parent.(*image.Image) if !ok { return errors.Errorf("unexpected image type") } + platform := &specs.Platform{ + OS: parentImage.OS, + Architecture: parentImage.Architecture, + Variant: parentImage.Variant, + } + + // add an image mount without an image so the layer is properly unmounted + // if there is an error before we can add the full mount with image + b.imageSources.Add(newImageMount(nil, newLayer), platform) + newImage := image.NewChildImage(parentImage, image.ChildConfig{ Author: state.maintainer, ContainerConfig: runConfig, @@ -146,7 +153,7 @@ } state.imageID = exportedImage.ImageID() - b.imageSources.Add(newImageMount(exportedImage, newLayer)) + b.imageSources.Add(newImageMount(exportedImage, newLayer), platform) return nil } --- a/engine/builder/dockerfile/internals_test.go +++ b/engine/builder/dockerfile/internals_test.go @@ -11,8 +11,12 @@ "github.com/docker/docker/api/types/container" "github.com/docker/docker/builder" "github.com/docker/docker/builder/remotecontext" + "github.com/docker/docker/image" + "github.com/docker/docker/layer" "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/containerfs" "github.com/docker/go-connections/nat" + "github.com/opencontainers/go-digest" "gotest.tools/assert" is "gotest.tools/assert/cmp" "gotest.tools/skip" @@ -180,3 +184,45 @@ copy.Shell[0] = "sh" assert.Check(t, is.DeepEqual(fullMutableRunConfig(), runConfig)) } + +type MockRWLayer struct{} + +func (l *MockRWLayer) Release() error { return nil } +func (l *MockRWLayer) Root() containerfs.ContainerFS { return nil } +func (l *MockRWLayer) Commit() (builder.ROLayer, error) { + return &MockROLayer{ + diffID: layer.DiffID(digest.Digest("sha256:1234")), + }, nil +} + +type MockROLayer struct { + diffID layer.DiffID +} + +func (l *MockROLayer) Release() error { return nil } +func (l *MockROLayer) NewRWLayer() (builder.RWLayer, error) { return nil, nil } +func (l *MockROLayer) DiffID() layer.DiffID { return l.diffID } + +func getMockBuildBackend() builder.Backend { + return &MockBackend{} +} + +func TestExportImage(t *testing.T) { + ds := newDispatchState(NewBuildArgs(map[string]*string{})) + layer := &MockRWLayer{} + parentImage := &image.Image{ + V1Image: image.V1Image{ + OS: "linux", + Architecture: "arm64", + Variant: "v8", + }, + } + runConfig := &container.Config{} + + b := &Builder{ + imageSources: getMockImageSource(nil, nil, nil), + docker: getMockBuildBackend(), + } + err := b.exportImage(ds, layer, parentImage, runConfig) + assert.NilError(t, err) +} --- a/engine/builder/dockerfile/mockbackend_test.go +++ b/engine/builder/dockerfile/mockbackend_test.go @@ -82,7 +82,7 @@ } func (m *MockBackend) CreateImage(config []byte, parent string) (builder.Image, error) { - return nil, nil + return &mockImage{id: "test"}, nil } type mockImage struct { --- a/engine/daemon/images/image_inspect.go +++ b/engine/daemon/images/image_inspect.go @@ -76,6 +76,7 @@ Author: img.Author, Config: img.Config, Architecture: img.Architecture, + Variant: img.Variant, Os: img.OperatingSystem(), OsVersion: img.OSVersion, Size: size, --- a/engine/distribution/config.go +++ b/engine/distribution/config.go @@ -170,7 +170,7 @@ if !system.IsOSSupported(os) { return nil, system.ErrNotSupportedOperatingSystem } - return &specs.Platform{OS: os, Architecture: unmarshalledConfig.Architecture, OSVersion: unmarshalledConfig.OSVersion}, nil + return &specs.Platform{OS: os, Architecture: unmarshalledConfig.Architecture, Variant: unmarshalledConfig.Variant, OSVersion: unmarshalledConfig.OSVersion}, nil } type storeLayerProvider struct { --- a/engine/image/image.go +++ b/engine/image/image.go @@ -53,6 +53,8 @@ Config *container.Config `json:"config,omitempty"` // Architecture is the hardware that the image is built and runs on Architecture string `json:"architecture,omitempty"` + // Variant is the CPU architecture variant (presently ARM-only) + Variant string `json:"variant,omitempty"` // OS is the operating system used to build and run the image OS string `json:"os,omitempty"` // Size is the total size of the image including all layers it is composed of @@ -105,6 +107,13 @@ return arch } +// BaseImgVariant returns the image's variant, whether populated or not. +// This avoids creating an inconsistency where the stored image variant +// is "greater than" (i.e. v8 vs v6) the actual image variant. +func (img *Image) BaseImgVariant() string { + return img.Variant +} + // OperatingSystem returns the image's operating system. If not populated, defaults to the host runtime OS. func (img *Image) OperatingSystem() string { os := img.OS @@ -167,6 +176,7 @@ DockerVersion: dockerversion.Version, Config: child.Config, Architecture: img.BaseImgArch(), + Variant: img.BaseImgVariant(), OS: os, Container: child.ContainerID, ContainerConfig: *child.ContainerConfig,
-- System Information: Debian Release: bullseye/sid APT prefers testing APT policy: (500, 'testing'), (50, 'unstable'), (1, 'experimental') Architecture: amd64 (x86_64) Kernel: Linux 5.6.0-1-amd64 (SMP w/4 CPU cores) Kernel taint flags: TAINT_WARN, TAINT_OOT_MODULE, TAINT_UNSIGNED_MODULE Locale: LANG=en_US.UTF-8, LC_CTYPE=en_US.UTF-8 (charmap=UTF-8), LANGUAGE=en_US.UTF-8 (charmap=UTF-8) Shell: /bin/sh linked to /bin/dash Init: systemd (via /run/systemd/system) LSM: AppArmor: enabled