From e740848eb987fde1ac4aa12c191cc8b9f2b391af Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Tue, 12 May 2026 14:20:19 +0200 Subject: [PATCH 1/3] info: report configured and discovered CDI devices Add CDI information to podman info and podman system info. The host info now includes the configured CDI spec directories and the currently discovered CDI devices. The devices are resolved when the info endpoint is called and there is no need to refresh these in the background. Also map the same data into the Docker-compatible /info response as CDISpecDirs and DiscoveredDevices. Signed-off-by: Evan Lezar --- docs/source/markdown/podman-info.1.md | 1 + libpod/define/info.go | 8 +++++++ libpod/info.go | 29 +++++++++++++++++++++++ pkg/api/handlers/compat/info.go | 13 ++++++++++ test/e2e/info_test.go | 34 +++++++++++++++++++++++++++ test/system/005-info.bats | 31 ++++++++++++++++++++++++ 6 files changed, 116 insertions(+) diff --git a/docs/source/markdown/podman-info.1.md b/docs/source/markdown/podman-info.1.md index cb7151d9403..d68d188194e 100644 --- a/docs/source/markdown/podman-info.1.md +++ b/docs/source/markdown/podman-info.1.md @@ -11,6 +11,7 @@ podman\-info - Display Podman related system information ## DESCRIPTION Displays information pertinent to the host, current storage stats, configured container registries, and build of podman. +Host information includes configured CDI spec directories and resolved CDI devices when present. ## OPTIONS diff --git a/libpod/define/info.go b/libpod/define/info.go index 2dacf09c928..84995629d4d 100644 --- a/libpod/define/info.go +++ b/libpod/define/info.go @@ -33,6 +33,8 @@ type HostInfo struct { CgroupManager string `json:"cgroupManager"` CgroupsVersion string `json:"cgroupVersion"` CgroupControllers []string `json:"cgroupControllers"` + CDISpecDirs []string `json:"cdiSpecDirs"` + DiscoveredDevices []DeviceInfo `json:"discoveredDevices,omitempty"` Conmon *ConmonInfo `json:"conmon"` CPUs int `json:"cpus"` CPUUtilization *CPUUsage `json:"cpuUtilization"` @@ -72,6 +74,12 @@ type HostInfo struct { EmulatedArchitectures []string `json:"emulatedArchitectures,omitempty"` } +// DeviceInfo describes a device discovered by a device source. +type DeviceInfo struct { + Source string `json:"source"` + ID string `json:"id"` +} + // RemoteSocket describes information about the API socket type RemoteSocket struct { Path string `json:"path,omitempty"` diff --git a/libpod/info.go b/libpod/info.go index 5aab80902d6..9c163064788 100644 --- a/libpod/info.go +++ b/libpod/info.go @@ -23,6 +23,7 @@ import ( "go.podman.io/podman/v6/libpod/define" "go.podman.io/podman/v6/libpod/linkmode" "go.podman.io/storage/pkg/system" + "tags.cncf.io/container-device-interface/pkg/cdi" ) // Info returns the store and host information @@ -130,6 +131,7 @@ func (r *Runtime) hostInfo() (*define.HostInfo, error) { SwapFree: mi.SwapFree, SwapTotal: mi.SwapTotal, } + info.CDISpecDirs, info.DiscoveredDevices = r.cdiInfo() platform := parse.DefaultPlatform() pArr := strings.Split(platform, "/") if len(pArr) == 3 { @@ -177,6 +179,33 @@ func (r *Runtime) hostInfo() (*define.HostInfo, error) { return &info, nil } +func (r *Runtime) cdiInfo() ([]string, []define.DeviceInfo) { + registry, err := cdi.NewCache( + cdi.WithSpecDirs(r.config.Engine.CdiSpecDirs.Get()...), + cdi.WithAutoRefresh(false), + ) + if err != nil { + logrus.Debugf("Creating CDI registry for info: %v", err) + return r.config.Engine.CdiSpecDirs.Get(), nil + } + if err := registry.Refresh(); err != nil { + logrus.Debugf("The following error was triggered when refreshing the CDI registry for info: %v", err) + } + + return registry.GetSpecDirectories(), cdiDeviceInfo(registry.ListDevices()) +} + +func cdiDeviceInfo(deviceNames []string) []define.DeviceInfo { + devices := make([]define.DeviceInfo, 0, len(deviceNames)) + for _, device := range deviceNames { + devices = append(devices, define.DeviceInfo{ + Source: "cdi", + ID: device, + }) + } + return devices +} + func (r *Runtime) getContainerStoreInfo() (define.ContainerStore, error) { var paused, running, stopped int cs := define.ContainerStore{} diff --git a/pkg/api/handlers/compat/info.go b/pkg/api/handlers/compat/info.go index 5ef43badd5e..44bf6a1341b 100644 --- a/pkg/api/handlers/compat/info.go +++ b/pkg/api/handlers/compat/info.go @@ -62,6 +62,7 @@ func GetInfo(w http.ResponseWriter, r *http.Request) { CPUSet: sysInfo.Cpuset, CPUShares: sysInfo.CPUShares, CgroupDriver: configInfo.Engine.CgroupManager, + CDISpecDirs: infoData.Host.CDISpecDirs, ContainerdCommit: dockerSystem.Commit{}, Containers: infoData.Store.ContainerStore.Number, ContainersPaused: stateInfo[define.ContainerStatePaused], @@ -70,6 +71,7 @@ func GetInfo(w http.ResponseWriter, r *http.Request) { Debug: log.IsLevelEnabled(log.DebugLevel), DefaultAddressPools: getDefaultAddressPools(configInfo), DefaultRuntime: configInfo.Engine.OCIRuntime, + DiscoveredDevices: getDiscoveredDevices(infoData.Host.DiscoveredDevices), DockerRootDir: infoData.Store.GraphRoot, Driver: infoData.Store.GraphDriverName, DriverStatus: getGraphStatus(infoData.Store.GraphStatus), @@ -132,6 +134,17 @@ func GetInfo(w http.ResponseWriter, r *http.Request) { utils.WriteResponse(w, http.StatusOK, info) } +func getDiscoveredDevices(discoveredDevices []define.DeviceInfo) []dockerSystem.DeviceInfo { + devices := make([]dockerSystem.DeviceInfo, 0, len(discoveredDevices)) + for _, device := range discoveredDevices { + devices = append(devices, dockerSystem.DeviceInfo{ + Source: device.Source, + ID: device.ID, + }) + } + return devices +} + func getServiceConfig(runtime *libpod.Runtime) *registry.ServiceConfig { var indexConfs map[string]*registry.IndexInfo diff --git a/test/e2e/info_test.go b/test/e2e/info_test.go index f0f42555089..f61d9a00d60 100644 --- a/test/e2e/info_test.go +++ b/test/e2e/info_test.go @@ -185,6 +185,40 @@ var _ = Describe("Podman Info", func() { Expect(session.OutputToString()).To(Equal(customNetName)) }) + It("Podman info: check CDI spec dirs and devices from configuration", func() { + cdiDir := filepath.Join(podmanTest.TempDir, "cdi") + err := os.MkdirAll(cdiDir, os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + + cdiSpec := []byte(`{ + "cdiVersion": "0.3.0", + "kind": "vendor.com/device", + "devices": [ + { + "name": "myKmsg", + "containerEdits": { + "env": ["PODMAN_CDI_INFO_TEST=1"] + } + } + ] +}`) + err = os.WriteFile(filepath.Join(cdiDir, "device.json"), cdiSpec, os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + + configPath := filepath.Join(podmanTest.TempDir, "containers.conf") + configContent := fmt.Sprintf("[engine]\ncdi_spec_dirs = [%q]\n", cdiDir) + err = os.WriteFile(configPath, []byte(configContent), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + + GinkgoT().Setenv("CONTAINERS_CONF_OVERRIDE", configPath) + podmanTest.RestartRemoteService() + + session := podmanTest.PodmanExitCleanly("info", "--format", "{{.Host.CDISpecDirs}} {{.Host.DiscoveredDevices}}") + + Expect(session.OutputToString()).To(ContainSubstring(cdiDir)) + Expect(session.OutputToString()).To(ContainSubstring("vendor.com/device=myKmsg")) + }) + It("Podman info: check desired storage driver", func() { // defined in .cirrus.yml want := os.Getenv("CI_DESIRED_STORAGE") diff --git a/test/system/005-info.bats b/test/system/005-info.bats index e56dcfce2c3..7a8a37e3148 100644 --- a/test/system/005-info.bats +++ b/test/system/005-info.bats @@ -62,6 +62,37 @@ store.imageStore.number | 1 done < <(parse_table "$tests") } +@test "podman info - CDI spec dirs and devices" { + skip_if_remote "--cdi-spec-dir flag is not supported for remote" + + cdi_dir=$PODMAN_TMPDIR/cdi + mkdir -p "$cdi_dir" + cat >"$cdi_dir/device.json" < Date: Mon, 1 Jun 2026 21:52:29 +0200 Subject: [PATCH 2/3] test/e2e: use PodmanExitCleanly in info tests Signed-off-by: Evan Lezar --- test/e2e/info_test.go | 68 +++++++++++-------------------------------- 1 file changed, 17 insertions(+), 51 deletions(-) diff --git a/test/e2e/info_test.go b/test/e2e/info_test.go index f61d9a00d60..4e1b7f2530b 100644 --- a/test/e2e/info_test.go +++ b/test/e2e/info_test.go @@ -47,22 +47,16 @@ var _ = Describe("Podman Info", func() { }) It("podman info --format GO template", func() { - session := podmanTest.Podman([]string{"info", "--format", "{{.Store.GraphRoot}}"}) - session.WaitWithDefaultTimeout() - Expect(session).Should(ExitCleanly()) + podmanTest.PodmanExitCleanly("info", "--format", "{{.Store.GraphRoot}}") }) It("podman info --format GO template", func() { - session := podmanTest.Podman([]string{"info", "--format", "{{.Registries}}"}) - session.WaitWithDefaultTimeout() - Expect(session).Should(ExitCleanly()) + session := podmanTest.PodmanExitCleanly("info", "--format", "{{.Registries}}") Expect(session.OutputToString()).To(ContainSubstring("registry")) }) It("podman info --format GO template plugins", func() { - session := podmanTest.Podman([]string{"info", "--format", "{{.Plugins}}"}) - session.WaitWithDefaultTimeout() - Expect(session).Should(ExitCleanly()) + session := podmanTest.PodmanExitCleanly("info", "--format", "{{.Plugins}}") Expect(session.OutputToString()).To(ContainSubstring("local")) Expect(session.OutputToString()).To(ContainSubstring("journald")) Expect(session.OutputToString()).To(ContainSubstring("bridge")) @@ -104,9 +98,7 @@ var _ = Describe("Podman Info", func() { }) It("check RemoteSocket ", func() { - session := podmanTest.Podman([]string{"info", "--format", "{{.Host.RemoteSocket.Path}}"}) - session.WaitWithDefaultTimeout() - Expect(session).Should(ExitCleanly()) + session := podmanTest.PodmanExitCleanly("info", "--format", "{{.Host.RemoteSocket.Path}}") switch podmanTest.RemoteSocketScheme { case "unix": Expect(session.OutputToString()).To(MatchRegexp("/run/.*podman.*sock")) @@ -114,9 +106,7 @@ var _ = Describe("Podman Info", func() { Expect(session.OutputToString()).To(MatchRegexp("tcp://127.0.0.1:.*")) } - session = podmanTest.Podman([]string{"info", "--format", "{{.Host.ServiceIsRemote}}"}) - session.WaitWithDefaultTimeout() - Expect(session).Should(ExitCleanly()) + session = podmanTest.PodmanExitCleanly("info", "--format", "{{.Host.ServiceIsRemote}}") if podmanTest.RemoteTest { Expect(session.OutputToString()).To(Equal("true")) } else { @@ -124,18 +114,14 @@ var _ = Describe("Podman Info", func() { } if IsRemote() { - session = podmanTest.Podman([]string{"info", "--format", "{{.Host.RemoteSocket.Exists}}"}) - session.WaitWithDefaultTimeout() - Expect(session).Should(ExitCleanly()) + session = podmanTest.PodmanExitCleanly("info", "--format", "{{.Host.RemoteSocket.Exists}}") Expect(session.OutputToString()).To(Equal("true")) } }) It("Podman info must contain cgroupControllers with RelevantControllers", func() { SkipIfRootless("Hard to tell which controllers are going to be enabled for rootless") - session := podmanTest.Podman([]string{"info", "--format", "{{.Host.CgroupControllers}}"}) - session.WaitWithDefaultTimeout() - Expect(session).To(ExitCleanly()) + session := podmanTest.PodmanExitCleanly("info", "--format", "{{.Host.CgroupControllers}}") Expect(session.OutputToString()).To(ContainSubstring("memory")) Expect(session.OutputToString()).To(ContainSubstring("pids")) }) @@ -149,21 +135,15 @@ var _ = Describe("Podman Info", func() { } Fail("CIRRUS_CI is set, but CI_DESIRED_RUNTIME is not! See #14912") } - session := podmanTest.Podman([]string{"info", "--format", "{{.Host.OCIRuntime.Name}}"}) - session.WaitWithDefaultTimeout() - Expect(session).To(ExitCleanly()) + session := podmanTest.PodmanExitCleanly("info", "--format", "{{.Host.OCIRuntime.Name}}") Expect(session.OutputToString()).To(Equal(want)) }) It("Podman info: check desired network backend", func() { - session := podmanTest.Podman([]string{"info", "--format", "{{.Host.NetworkBackend}}"}) - session.WaitWithDefaultTimeout() - Expect(session).To(ExitCleanly()) + session := podmanTest.PodmanExitCleanly("info", "--format", "{{.Host.NetworkBackend}}") Expect(session.OutputToString()).To(Equal("netavark")) - session = podmanTest.Podman([]string{"info", "--format", "{{.Host.NetworkBackendInfo.Backend}}"}) - session.WaitWithDefaultTimeout() - Expect(session).To(ExitCleanly()) + session = podmanTest.PodmanExitCleanly("info", "--format", "{{.Host.NetworkBackendInfo.Backend}}") Expect(session.OutputToString()).To(Equal("netavark")) }) @@ -179,9 +159,7 @@ var _ = Describe("Podman Info", func() { Expect(err).ToNot(HaveOccurred()) podmanTest.RestartRemoteService() - session := podmanTest.Podman([]string{"info", "--format", "{{.Host.NetworkBackendInfo.DefaultNetwork}}"}) - session.WaitWithDefaultTimeout() - Expect(session).To(ExitCleanly()) + session := podmanTest.PodmanExitCleanly("info", "--format", "{{.Host.NetworkBackendInfo.DefaultNetwork}}") Expect(session.OutputToString()).To(Equal(customNetName)) }) @@ -228,9 +206,7 @@ var _ = Describe("Podman Info", func() { } Fail("CIRRUS_CI is set, but CI_DESIRED_STORAGE is not! See #20161") } - session := podmanTest.Podman([]string{"info", "--format", "{{.Store.GraphDriverName}}"}) - session.WaitWithDefaultTimeout() - Expect(session).To(ExitCleanly()) + session := podmanTest.PodmanExitCleanly("info", "--format", "{{.Store.GraphDriverName}}") Expect(session.OutputToString()).To(Equal(want), ".Store.GraphDriverName from podman info") // Confirm desired setting of composefs @@ -239,9 +215,7 @@ var _ = Describe("Podman Info", func() { if os.Getenv("CI_DESIRED_COMPOSEFS") != "" { expect = "true" } - session = podmanTest.Podman([]string{"info", "--format", `{{index .Store.GraphOptions "overlay.use_composefs"}}`}) - session.WaitWithDefaultTimeout() - Expect(session).To(ExitCleanly()) + session = podmanTest.PodmanExitCleanly("info", "--format", `{{index .Store.GraphOptions "overlay.use_composefs"}}`) Expect(session.OutputToString()).To(Equal(expect), ".Store.GraphOptions -> overlay.use_composefs") } }) @@ -250,19 +224,13 @@ var _ = Describe("Podman Info", func() { // This should not run on architectures and OSes that use the file locks backend. // Which, for now, is Linux + RISCV and FreeBSD, neither of which are in CI - so // no skips. - info1 := podmanTest.Podman([]string{"info", "--format", "{{ .Host.FreeLocks }}"}) - info1.WaitWithDefaultTimeout() - Expect(info1).To(ExitCleanly()) + info1 := podmanTest.PodmanExitCleanly("info", "--format", "{{ .Host.FreeLocks }}") free1, err := strconv.Atoi(info1.OutputToString()) Expect(err).To(Not(HaveOccurred())) - ctr := podmanTest.Podman([]string{"create", ALPINE, "top"}) - ctr.WaitWithDefaultTimeout() - Expect(ctr).To(ExitCleanly()) + podmanTest.PodmanExitCleanly("create", ALPINE, "top") - info2 := podmanTest.Podman([]string{"info", "--format", "{{ .Host.FreeLocks }}"}) - info2.WaitWithDefaultTimeout() - Expect(info2).To(ExitCleanly()) + info2 := podmanTest.PodmanExitCleanly("info", "--format", "{{ .Host.FreeLocks }}") free2, err := strconv.Atoi(info2.OutputToString()) Expect(err).To(Not(HaveOccurred())) @@ -285,9 +253,7 @@ var _ = Describe("Podman Info", func() { }) It("Podman info: check client information", func() { - info := podmanTest.Podman([]string{"info", "--format", "{{ .Client }}"}) - info.WaitWithDefaultTimeout() - Expect(info).To(ExitCleanly()) + info := podmanTest.PodmanExitCleanly("info", "--format", "{{ .Client }}") // client info should only appear when using the remote client if IsRemote() { Expect(info.OutputToString()).ToNot(Equal("")) From e1bb6870943fcf772d46ca320df759d384b3ea58 Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Mon, 1 Jun 2026 21:55:36 +0200 Subject: [PATCH 3/3] test/e2e: use Ginkgo Setenv in info tests Signed-off-by: Evan Lezar --- test/e2e/info_test.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/e2e/info_test.go b/test/e2e/info_test.go index 4e1b7f2530b..46358ba5f06 100644 --- a/test/e2e/info_test.go +++ b/test/e2e/info_test.go @@ -66,10 +66,7 @@ var _ = Describe("Podman Info", func() { SkipIfNotRootless("test of rootless_storage_path is only meaningful as rootless") SkipIfRemote("Only tests storage on local client") configPath := filepath.Join(podmanTest.TempDir, ".config", "containers", "storage.conf") - os.Setenv("CONTAINERS_STORAGE_CONF", configPath) - defer func() { - os.Unsetenv("CONTAINERS_STORAGE_CONF") - }() + GinkgoT().Setenv("CONTAINERS_STORAGE_CONF", configPath) err := os.RemoveAll(filepath.Dir(configPath)) Expect(err).ToNot(HaveOccurred()) @@ -150,7 +147,7 @@ var _ = Describe("Podman Info", func() { It("Podman info: check default network from configuration", func() { configPath := filepath.Join(podmanTest.TempDir, "containers.conf") - os.Setenv("CONTAINERS_CONF_OVERRIDE", configPath) + GinkgoT().Setenv("CONTAINERS_CONF_OVERRIDE", configPath) customNetName := "my-custom-test-network" configContent := fmt.Sprintf("[network]\ndefault_network=%q\n", customNetName)