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

Reply via email to