diff --git a/pkg/cmd/system/info.go b/pkg/cmd/system/info.go index 5e87cf24fc4..6fce7d68334 100644 --- a/pkg/cmd/system/info.go +++ b/pkg/cmd/system/info.go @@ -25,19 +25,19 @@ import ( "text/template" "github.com/containerd/containerd" + "github.com/containerd/containerd/api/services/introspection/v1" "github.com/containerd/log" - "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/docker/go-units" "golang.org/x/text/cases" "golang.org/x/text/language" - "github.com/containerd/containerd/api/services/introspection/v1" + "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/formatter" "github.com/containerd/nerdctl/v2/pkg/infoutil" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/strutil" - "github.com/docker/go-units" ) func Info(ctx context.Context, client *containerd.Client, options types.SystemInfoOptions) error { @@ -153,13 +153,45 @@ func prettyPrintInfoDockerCompat(stdout io.Writer, stderr io.Writer, info *docke // Storage Driver is not really Server concept for nerdctl, but mimics `docker info` output fmt.Fprintf(w, " Storage Driver: %s\n", info.Driver) fmt.Fprintf(w, " Logging Driver: %s\n", info.LoggingDriver) - fmt.Fprintf(w, " Cgroup Driver: %s\n", info.CgroupDriver) - fmt.Fprintf(w, " Cgroup Version: %s\n", info.CgroupVersion) + printF(w, " Cgroup Driver: ", info.CgroupDriver) + printF(w, " Cgroup Version: ", info.CgroupVersion) fmt.Fprintf(w, " Plugins:\n") - fmt.Fprintf(w, " Log: %s\n", strings.Join(info.Plugins.Log, " ")) + fmt.Fprintf(w, " Log: %s\n", strings.Join(info.Plugins.Log, " ")) fmt.Fprintf(w, " Storage: %s\n", strings.Join(info.Plugins.Storage, " ")) + + // print Security options + printSecurityOptions(w, info.SecurityOptions) + + fmt.Fprintf(w, " Kernel Version: %s\n", info.KernelVersion) + fmt.Fprintf(w, " Operating System: %s\n", info.OperatingSystem) + fmt.Fprintf(w, " OSType: %s\n", info.OSType) + fmt.Fprintf(w, " Architecture: %s\n", info.Architecture) + fmt.Fprintf(w, " CPUs: %d\n", info.NCPU) + fmt.Fprintf(w, " Total Memory: %s\n", units.BytesSize(float64(info.MemTotal))) + fmt.Fprintf(w, " Name: %s\n", info.Name) + fmt.Fprintf(w, " ID: %s\n", info.ID) + + fmt.Fprintln(w) + if len(info.Warnings) > 0 { + fmt.Fprintln(stderr, strings.Join(info.Warnings, "\n")) + } + return nil +} + +func printF(w io.Writer, label string, dockerCompatInfo string) { + if dockerCompatInfo == "" { + return + } + fmt.Fprintf(w, " %s: %s\n", label, dockerCompatInfo) +} + +func printSecurityOptions(w io.Writer, securityOptions []string) { + if len(securityOptions) == 0 { + return + } + fmt.Fprintf(w, " Security Options:\n") - for _, s := range info.SecurityOptions { + for _, s := range securityOptions { m, err := strutil.ParseCSVMap(s) if err != nil { log.L.WithError(err).Warnf("unparsable security option %q", s) @@ -175,21 +207,7 @@ func prettyPrintInfoDockerCompat(stdout io.Writer, stderr io.Writer, info *docke if k == "name" { continue } - fmt.Fprintf(w, " %s: %s\n", cases.Title(language.English).String(k), v) + fmt.Fprintf(w, " %s:\t%s\n", cases.Title(language.English).String(k), v) } } - fmt.Fprintf(w, " Kernel Version: %s\n", info.KernelVersion) - fmt.Fprintf(w, " Operating System: %s\n", info.OperatingSystem) - fmt.Fprintf(w, " OSType: %s\n", info.OSType) - fmt.Fprintf(w, " Architecture: %s\n", info.Architecture) - fmt.Fprintf(w, " CPUs: %d\n", info.NCPU) - fmt.Fprintf(w, " Total Memory: %s\n", units.BytesSize(float64(info.MemTotal))) - fmt.Fprintf(w, " Name: %s\n", info.Name) - fmt.Fprintf(w, " ID: %s\n", info.ID) - - fmt.Fprintln(w) - if len(info.Warnings) > 0 { - fmt.Fprintln(stderr, strings.Join(info.Warnings, "\n")) - } - return nil } diff --git a/pkg/infoutil/infoutil_unix.go b/pkg/infoutil/infoutil_unix.go index 782a17537f0..1a4068a067a 100644 --- a/pkg/infoutil/infoutil_unix.go +++ b/pkg/infoutil/infoutil_unix.go @@ -23,7 +23,6 @@ import ( "io" "os" "regexp" - "strings" "golang.org/x/sys/unix" diff --git a/pkg/infoutil/infoutil_windows.go b/pkg/infoutil/infoutil_windows.go index 7612bd2015a..6f01255a7b6 100644 --- a/pkg/infoutil/infoutil_windows.go +++ b/pkg/infoutil/infoutil_windows.go @@ -17,33 +17,210 @@ package infoutil import ( - "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" + "fmt" + "runtime" + "strings" + + "github.com/containerd/log" + "github.com/docker/docker/pkg/meminfo" "github.com/docker/docker/pkg/sysinfo" + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/registry" + + "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" ) -// UnameR returns `uname -r` +const UnameO = "Microsoft Windows" + +// MsiNTProductType is the product type of the operating system. +// https://learn.microsoft.com/en-us/windows/win32/msi/msintproducttype +// Ref: https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-osversioninfoexa +const ( + verNTServer = 0x0000003 +) + +type windowsInfoUtil interface { + RtlGetVersion() *windows.OsVersionInfoEx + GetRegistryStringValue(key registry.Key, path string, name string) (string, error) + GetRegistryIntValue(key registry.Key, path string, name string) (int, error) +} + +type winInfoUtil struct{} + +// RtlGetVersion implements the RtlGetVersion method using the actual windows package +func (sw *winInfoUtil) RtlGetVersion() *windows.OsVersionInfoEx { + return windows.RtlGetVersion() +} + +// UnameR returns the Kernel version func UnameR() string { - return "" + util := &winInfoUtil{} + version, err := getKernelVersion(util) + if err != nil { + log.L.Error(err.Error()) + } + + return version } -// UnameM returns `uname -m` +// UnameM returns the architecture of the system func UnameM() string { - return "" + arch := runtime.GOARCH + + if strings.ToLower(arch) == "amd64" { + return "x86_64" + } + + // "386": 32-bit Intel/AMD processors (x86 architecture) + if strings.ToLower(arch) == "386" { + return "x86" + } + + // arm, s390x, and so on + return arch } +// DistroName returns version information about the currently running operating system func DistroName() string { - return "" + util := &winInfoUtil{} + version, err := distroName(util) + if err != nil { + log.L.Error(err.Error()) + } + + return version } +func distroName(sw windowsInfoUtil) (string, error) { + // Get the OS version information from the Windows registry + regPath := `SOFTWARE\Microsoft\Windows NT\CurrentVersion` + + // Eg. 22631 (REG_SZ) + currBuildNo, err := sw.GetRegistryStringValue(registry.LOCAL_MACHINE, regPath, "CurrentBuildNumber") + if err != nil { + return "", fmt.Errorf("failed to get os version (build number) %v", err) + } + + // Eg. 23H2 (REG_SZ) + displayVersion, err := sw.GetRegistryStringValue(registry.LOCAL_MACHINE, regPath, "DisplayVersion") + if err != nil { + return "", fmt.Errorf("failed to get os version (display version) %v", err) + } + + // UBR: Update Build Revision. Eg. 3737 (REG_DWORD 32-bit Value) + ubr, err := sw.GetRegistryIntValue(registry.LOCAL_MACHINE, regPath, "UBR") + if err != nil { + return "", fmt.Errorf("failed to get os version (ubr) %v", err) + } + + productType := "" + if isWindowsServer(sw) { + productType = "Server" + } + + // Concatenate the reg.key values to get the OS version information + // Example: "Microsoft Windows Version 23H2 (OS Build 22631.3737)" + versionString := fmt.Sprintf("%s %s Version %s (OS Build %s.%d)", + UnameO, + productType, + displayVersion, + currBuildNo, + ubr, + ) + + // Replace double spaces with single spaces + versionString = strings.ReplaceAll(versionString, " ", " ") + + return versionString, nil +} + +func getKernelVersion(sw windowsInfoUtil) (string, error) { + // Get BuildLabEx value from the Windows registry + // [buiild number].[revision number].[architecture].[branch].[date]-[time] + // Eg. "BuildLabEx: 10240.16412.amd64fre.th1.150729-1800" + buildLab, err := sw.GetRegistryStringValue(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, "BuildLabEx") + if err != nil { + return "", err + } + + // Get Version: Contains the major and minor version numbers of the operating system. + // Eg. "10.0" + osvi := sw.RtlGetVersion() + + // Concatenate the OS version and BuildLabEx values to get the Kernel version information + // Example: "10.0 22631 (10240.16412.amd64fre.th1.150729-1800)" + version := fmt.Sprintf("%d.%d %d (%s)", osvi.MajorVersion, osvi.MinorVersion, osvi.BuildNumber, buildLab) + return version, nil +} + +// GetRegistryStringValue retrieves a string value from the Windows registry +func (sw *winInfoUtil) GetRegistryStringValue(key registry.Key, path string, name string) (string, error) { + k, err := registry.OpenKey(key, path, registry.QUERY_VALUE) + if err != nil { + return "", err + } + defer k.Close() + + v, _, err := k.GetStringValue(name) + if err != nil { + return "", err + } + return v, nil +} + +// GetRegistryIntValue retrieves an integer value from the Windows registry +func (sw *winInfoUtil) GetRegistryIntValue(key registry.Key, path string, name string) (int, error) { + k, err := registry.OpenKey(key, path, registry.QUERY_VALUE) + if err != nil { + return 0, err + } + defer k.Close() + + v, _, err := k.GetIntegerValue(name) + if err != nil { + return 0, err + } + return int(v), nil +} + +func isWindowsServer(sw windowsInfoUtil) bool { + osvi := sw.RtlGetVersion() + return osvi.ProductType == verNTServer +} + +// Cgroups not supported on Windows func CgroupsVersion() string { return "" } func fulfillPlatformInfo(info *dockercompat.Info) { - // unimplemented + mobySysInfo := mobySysInfo(info) + + // NOTE: cgroup fields are not available on Windows + // https://techcommunity.microsoft.com/t5/containers/introducing-the-host-compute-service-hcs/ba-p/382332 + + info.IPv4Forwarding = !mobySysInfo.IPv4ForwardingDisabled + if !info.IPv4Forwarding { + info.Warnings = append(info.Warnings, "WARNING: IPv4 forwarding is disabled") + } + info.BridgeNfIptables = !mobySysInfo.BridgeNFCallIPTablesDisabled + if !info.BridgeNfIptables { + info.Warnings = append(info.Warnings, "WARNING: bridge-nf-call-iptables is disabled") + } + info.BridgeNfIP6tables = !mobySysInfo.BridgeNFCallIP6TablesDisabled + if !info.BridgeNfIP6tables { + info.Warnings = append(info.Warnings, "WARNING: bridge-nf-call-ip6tables is disabled") + } + info.NCPU = sysinfo.NumCPU() + memLimit, err := meminfo.Read() + if err != nil { + info.Warnings = append(info.Warnings, fmt.Sprintf("failed to read mem info: %v", err)) + } else { + info.MemTotal = memLimit.MemTotal + } } -func mobySysInfo(info *dockercompat.Info) *sysinfo.SysInfo { +func mobySysInfo(_ *dockercompat.Info) *sysinfo.SysInfo { var sysinfo sysinfo.SysInfo return &sysinfo } diff --git a/pkg/infoutil/infoutil_windows_test.go b/pkg/infoutil/infoutil_windows_test.go new file mode 100644 index 00000000000..d59c157513c --- /dev/null +++ b/pkg/infoutil/infoutil_windows_test.go @@ -0,0 +1,200 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package infoutil + +import ( + "testing" + + mocks "github.com/containerd/nerdctl/v2/pkg/infoutil/infoutilmock" + "go.uber.org/mock/gomock" + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/registry" + "gotest.tools/v3/assert" +) + +func setUpMocks(t *testing.T) *mocks.MockWindowsInfoUtil { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockInfoUtil := mocks.NewMockWindowsInfoUtil(ctrl) + + // Mock registry value: CurrentBuildNumber + mockInfoUtil. + EXPECT(). + GetRegistryStringValue(gomock.Any(), gomock.Any(), "CurrentBuildNumber"). + Return("19041", nil). + AnyTimes() + + // Mock registry value: DisplayVersion + mockInfoUtil. + EXPECT(). + GetRegistryStringValue(gomock.Any(), gomock.Any(), "DisplayVersion"). + Return("22H4", nil). + AnyTimes() + + // Mock registry value: UBR + mockInfoUtil. + EXPECT(). + GetRegistryIntValue(gomock.Any(), gomock.Any(), "UBR"). + Return(558, nil). + AnyTimes() + + return mockInfoUtil +} + +const ( + verNTWorkStation = 0x0000001 + verNTDomainController = 0x0000002 +) + +func TestDistroName(t *testing.T) { + mockInfoUtil := setUpMocks(t) + + baseVersion := windows.OsVersionInfoEx{ + MajorVersion: 10, + MinorVersion: 0, + BuildNumber: 19041, + } + + tests := []struct { + productType byte + expected string + }{ + { + productType: verNTWorkStation, + expected: "Microsoft Windows Version 22H4 (OS Build 19041.558)", + }, + { + productType: verNTServer, + expected: "Microsoft Windows Server Version 22H4 (OS Build 19041.558)", + }, + } + + for _, tt := range tests { + // Mock sys/windows RtlGetVersion + osvi := baseVersion + osvi.ProductType = tt.productType + mockInfoUtil.EXPECT().RtlGetVersion().Return(&osvi).Times(1) + + t.Run(tt.expected, func(t *testing.T) { + actual, err := distroName(mockInfoUtil) + assert.Equal(t, tt.expected, actual, "distroName should return the name of the operating system") + assert.NilError(t, err) + }) + } +} + +func TestDistroNameError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockInfoUtil := mocks.NewMockWindowsInfoUtil(ctrl) + + mockInfoUtil.EXPECT().RtlGetVersion().Return(nil).Times(0) + mockInfoUtil. + EXPECT(). + GetRegistryStringValue(gomock.Any(), gomock.Any(), gomock.Any()). + Return("19041", registry.ErrNotExist).AnyTimes() + + actual, err := distroName(mockInfoUtil) + assert.ErrorContains(t, err, registry.ErrNotExist.Error(), "distroName should return an error on error") + assert.Equal(t, "", actual, "distroname should return an empty string on error") +} + +func TestGetKernelVersion(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockInfoUtil := mocks.NewMockWindowsInfoUtil(ctrl) + + // Mock registry value: BuildLabEx + mockInfoUtil. + EXPECT(). + GetRegistryStringValue(gomock.Any(), gomock.Any(), "BuildLabEx"). + Return("10240.16412.amd64fre.th1.150729-1800", nil). + Times(1) + + baseVersion := windows.OsVersionInfoEx{ + MajorVersion: 10, + MinorVersion: 0, + BuildNumber: 19041, + } + + expected := "10.0 19041 (10240.16412.amd64fre.th1.150729-1800)" + + // Mock sys/windows RtlGetVersion + osvi := baseVersion + mockInfoUtil.EXPECT().RtlGetVersion().Return(&osvi).Times(1) + + actual, err := getKernelVersion(mockInfoUtil) + assert.NilError(t, err) + assert.Equal(t, expected, actual, "getKernelVersion should return the kernel version") +} + +func TestGetKernelVersionError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockInfoUtil := mocks.NewMockWindowsInfoUtil(ctrl) + + mockInfoUtil.EXPECT().RtlGetVersion().Return(nil).Times(0) + mockInfoUtil. + EXPECT(). + GetRegistryStringValue(gomock.Any(), gomock.Any(), gomock.Any()). + Return("", registry.ErrNotExist).Times(1) + + actual, err := getKernelVersion(mockInfoUtil) + assert.ErrorContains(t, err, registry.ErrNotExist.Error(), "getKernelVersion should return an error on error") + assert.Equal(t, "", actual, "getKernelVersion should return an empty string on error") +} + +func TestIsWindowsServer(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + tests := []struct { + productType string + osvi windows.OsVersionInfoEx + expected bool + }{ + { + productType: "VER_NT_WORKSTATION", + osvi: windows.OsVersionInfoEx{ProductType: verNTWorkStation}, + expected: false, + }, + { + productType: "VER_NT_DOMAIN_CONTROLLER", + osvi: windows.OsVersionInfoEx{ProductType: verNTDomainController}, + expected: false, + }, + { + productType: "VER_NT_SERVER", + osvi: windows.OsVersionInfoEx{ProductType: verNTServer}, + expected: true, + }, + } + + mockSysCall := mocks.NewMockWindowsInfoUtil(ctrl) + for _, tt := range tests { + mockSysCall.EXPECT().RtlGetVersion().Return(&tt.osvi) + + t.Run(tt.productType, func(t *testing.T) { + actual := isWindowsServer(mockSysCall) + assert.Equal(t, tt.expected, actual, "isWindowsServer should return true on Windows Server") + }) + } +} diff --git a/pkg/infoutil/infoutilmock/info.util.mock.go b/pkg/infoutil/infoutilmock/info.util.mock.go new file mode 100644 index 00000000000..298597ece67 --- /dev/null +++ b/pkg/infoutil/infoutilmock/info.util.mock.go @@ -0,0 +1,108 @@ +//go:build windows + +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package infoutilmock + +import ( + "reflect" + + "go.uber.org/mock/gomock" + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/registry" +) + +// MockWindowsInfoUtil is a mock of windowsInfoUtil interface +type MockWindowsInfoUtil struct { + ctrl *gomock.Controller + recorder *MockWindowsInfoUtilMockRecorder +} + +// MockWindowsInfoUtilMockRecorder is the mock recorder for MockWindowsInfoUtil +type MockWindowsInfoUtilMockRecorder struct { + mock *MockWindowsInfoUtil +} + +// NewMockWindowsInfoUtil creates a new mock instance +func NewMockWindowsInfoUtil(ctrl *gomock.Controller) *MockWindowsInfoUtil { + mock := &MockWindowsInfoUtil{ctrl: ctrl} + mock.recorder = &MockWindowsInfoUtilMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockWindowsInfoUtil) EXPECT() *MockWindowsInfoUtilMockRecorder { + return m.recorder +} + +// Create mocks the RtlGetVersion method of windowsInfoUtil +func (m *MockWindowsInfoUtil) RtlGetVersion() *windows.OsVersionInfoEx { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RtlGetVersion") + ret0, _ := ret[0].(*windows.OsVersionInfoEx) + return ret0 +} + +// Expected call of RtlGetVersion +func (m *MockWindowsInfoUtilMockRecorder) RtlGetVersion() *gomock.Call { + m.mock.ctrl.T.Helper() + return m.mock.ctrl.RecordCallWithMethodType( + m.mock, + "RtlGetVersion", + reflect.TypeOf((*MockWindowsInfoUtil)(nil).RtlGetVersion), + ) +} + +// Create mocks the GetRegistryStringValue method of windowsInfoUtil +func (m *MockWindowsInfoUtil) GetRegistryStringValue(key registry.Key, path string, name string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRegistryStringValue", key, path, name) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Expected call of GetRegistryStringValue +func (m *MockWindowsInfoUtilMockRecorder) GetRegistryStringValue(key any, path any, name any) *gomock.Call { + m.mock.ctrl.T.Helper() + return m.mock.ctrl.RecordCallWithMethodType( + m.mock, + "GetRegistryStringValue", + reflect.TypeOf((*MockWindowsInfoUtil)(nil).GetRegistryStringValue), + key, path, name, + ) +} + +// Create mocks the GetRegistryIntValue method of windowsInfoUtil +func (m *MockWindowsInfoUtil) GetRegistryIntValue(key registry.Key, path string, name string) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRegistryIntValue", key, path, name) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Expected call of GetRegistryIntValue +func (m *MockWindowsInfoUtilMockRecorder) GetRegistryIntValue(key any, path any, name any) *gomock.Call { + m.mock.ctrl.T.Helper() + return m.mock.ctrl.RecordCallWithMethodType( + m.mock, + "GetRegistryIntValue", + reflect.TypeOf((*MockWindowsInfoUtil)(nil).GetRegistryIntValue), + key, path, name, + ) +}