diff --git a/cmd/internal/container/install/install.go b/cmd/internal/container/install/install.go index ec0dd0bc5b..ef6cb7506b 100644 --- a/cmd/internal/container/install/install.go +++ b/cmd/internal/container/install/install.go @@ -20,5 +20,6 @@ import ( _ "github.com/google/cadvisor/container/containerd/install" _ "github.com/google/cadvisor/container/crio/install" _ "github.com/google/cadvisor/container/docker/install" + _ "github.com/google/cadvisor/container/podman/install" _ "github.com/google/cadvisor/container/systemd/install" ) diff --git a/container/container.go b/container/container.go index 8414efc6a0..4c435a0e81 100644 --- a/container/container.go +++ b/container/container.go @@ -35,6 +35,7 @@ const ( ContainerTypeCrio ContainerTypeContainerd ContainerTypeMesos + ContainerTypePodman ) // Interface for container operation handlers. diff --git a/container/podman/client.go b/container/podman/client.go new file mode 100644 index 0000000000..06b132c5f3 --- /dev/null +++ b/container/podman/client.go @@ -0,0 +1,61 @@ +// Copyright 2021 Google Inc. All Rights Reserved. +// +// 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 podman + +import ( + "net/http" + "sync" + + dclient "github.com/docker/docker/client" + "github.com/docker/go-connections/tlsconfig" +) + +var ( + podmanClient *dclient.Client + podmanClientErr error + podmanClientOnce sync.Once +) + +// NewClient creates a Docker API client based on the given Podman flags +// +// At this time, we're using the podmans docker compatibility API layer +// for podman containers. +func NewClient() (*dclient.Client, error) { + podmanClientOnce.Do(func() { + var client *http.Client + if *ArgPodmanTLS { + client = &http.Client{} + options := tlsconfig.Options{ + CAFile: *ArgPodmanCA, + CertFile: *ArgPodmanCert, + KeyFile: *ArgPodmanKey, + InsecureSkipVerify: false, + } + tlsc, err := tlsconfig.Client(options) + if err != nil { + podmanClientErr = err + return + } + client.Transport = &http.Transport{ + TLSClientConfig: tlsc, + } + } + podmanClient, podmanClientErr = dclient.NewClientWithOpts( + dclient.WithHost(*ArgPodmanEndpoint), + dclient.WithHTTPClient(client), + dclient.WithAPIVersionNegotiation()) + }) + return podmanClient, podmanClientErr +} diff --git a/container/podman/factory.go b/container/podman/factory.go new file mode 100644 index 0000000000..5276f7da22 --- /dev/null +++ b/container/podman/factory.go @@ -0,0 +1,212 @@ +// Copyright 2021 Google Inc. All Rights Reserved. +// +// 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 podman + +import ( + "flag" + "fmt" + "path" + "regexp" + "strings" + "sync" + "time" + + "github.com/google/cadvisor/container" + "github.com/google/cadvisor/container/libcontainer" + "github.com/google/cadvisor/fs" + info "github.com/google/cadvisor/info/v1" + "github.com/google/cadvisor/watcher" + + docker "github.com/docker/docker/client" + "golang.org/x/net/context" + "k8s.io/klog/v2" +) + +const ( + // The namespace under which podman aliases are unique. + PodmanNamespace = "podman" + + // The retry times for getting podman root dir + rootDirRetries = 5 + + // The retry period for getting podman root dir, Millisecond + rootDirRetryPeriod time.Duration = 1000 * time.Millisecond +) + +var ( + ArgPodmanEndpoint = flag.String("podman", "unix:///run/podman/podman.sock", "podman endpoint") + ArgPodmanTLS = flag.Bool("podman-tls", false, "use TLS to connect to podman") + ArgPodmanCert = flag.String("podman-tls-cert", "cert.pem", "path to client certificate") + ArgPodmanKey = flag.String("podman-tls-key", "key.pem", "path to private key") + ArgPodmanCA = flag.String("podman-tls-ca", "ca.pem", "path to trusted CA") + + // Regexp that identifies podman cgroups + // --cgroup-parent have another prefix than 'libpod' + podmanCgroupRegexp = regexp.MustCompile(`([a-z0-9]{64})`) + + podmanEnvWhitelist = flag.String("podman_env_metadata_whitelist", "", "a comma-separated list of environment variable keys matched with specified prefix that needs to be collected for podman containers") + // Basepath to all container specific information that libcontainer stores. + podmanRootDir string + + podmanRootDirOnce sync.Once +) + +func RootDir() string { + podmanRootDirOnce.Do(func() { + for i := 0; i < rootDirRetries; i++ { + status, err := Status() + if err == nil && status.RootDir != "" { + podmanRootDir = status.RootDir + break + } else { + time.Sleep(rootDirRetryPeriod) + } + } + }) + return podmanRootDir +} + +type podmanFactory struct { + machineInfoFactory info.MachineInfoFactory + + client *docker.Client + + // Information about the mounted cgroup subsystems. + cgroupSubsystems libcontainer.CgroupSubsystems + + // Information about mounted filesystems. + fsInfo fs.FsInfo + + podmanVersion []int + + podmanAPIVersion []int + + includedMetrics container.MetricSet +} + +func (f *podmanFactory) String() string { + return PodmanNamespace +} + +func (f *podmanFactory) NewContainerHandler(name string, inHostNamespace bool) (handler container.ContainerHandler, err error) { + client, err := NewClient() + if err != nil { + return + } + + metadataEnvs := strings.Split(*podmanEnvWhitelist, ",") + + handler, err = newPodmanContainerHandler( + client, + name, + f.machineInfoFactory, + f.fsInfo, + &f.cgroupSubsystems, + inHostNamespace, + metadataEnvs, + f.podmanVersion, + f.includedMetrics, + ) + return +} + +// Returns the Podman ID from the full container name. +func CgroupNameToPodmanId(name string) string { + id := path.Base(name) + + if matches := podmanCgroupRegexp.FindStringSubmatch(id); matches != nil { + return matches[1] + } + + return id +} + +// isContainerName returns true if the cgroup with associated name +// could be a podman container. +// the actual decision is made by running a ContainerInspect API call +func isContainerName(name string) bool { + // always ignore .mount cgroup even if associated with podman and delegate to systemd + if strings.HasSuffix(name, ".mount") { + return false + } + return podmanCgroupRegexp.MatchString(path.Base(name)) +} + +// Podman handles all containers prefixed with libpod- +func (f *podmanFactory) CanHandleAndAccept(name string) (bool, bool, error) { + // if the container is not associated with podman, we can't handle it or accept it. + if !isContainerName(name) { + return false, false, nil + } + + // Check if the container is known to podman and it is active. + id := CgroupNameToPodmanId(name) + + // We assume that if Inspect fails then the container is not known to podman. + ctnr, err := f.client.ContainerInspect(context.Background(), id) + if err != nil || !ctnr.State.Running { + return false, true, fmt.Errorf("error inspecting container: %v", err) + } + + return true, true, nil +} + +func (f *podmanFactory) DebugInfo() map[string][]string { + return map[string][]string{} +} + +var ( + versionRegexpString = `(\d+)\.(\d+)\.(\d+)` + versionRe = regexp.MustCompile(versionRegexpString) + apiVersionRegexpString = `(\d+)\.(\d+)` + apiVersionRe = regexp.MustCompile(apiVersionRegexpString) +) + +// Register root container before running this function! +func Register(factory info.MachineInfoFactory, fsInfo fs.FsInfo, includedMetrics container.MetricSet) error { + client, err := NewClient() + if err != nil { + return fmt.Errorf("unable to communicate with podman: %v", err) + } + + podmanInfo, err := ValidateInfo() + if err != nil { + return fmt.Errorf("failed to validate Podman info: %v", err) + } + + // Version already validated above, assume no error here. + podmanVersion, _ := parseVersion(podmanInfo.ServerVersion, versionRe, 3) + + podmanAPIVersion, _ := APIVersion() + + cgroupSubsystems, err := libcontainer.GetCgroupSubsystems(includedMetrics) + if err != nil { + return fmt.Errorf("failed to get cgroup subsystems: %v", err) + } + + klog.V(1).Infof("Registering Podman factory") + f := &podmanFactory{ + cgroupSubsystems: cgroupSubsystems, + client: client, + podmanVersion: podmanVersion, + podmanAPIVersion: podmanAPIVersion, + fsInfo: fsInfo, + machineInfoFactory: factory, + includedMetrics: includedMetrics, + } + + container.RegisterContainerHandlerFactory(f, []watcher.ContainerWatchSource{watcher.Raw}) + return nil +} diff --git a/container/podman/handler.go b/container/podman/handler.go new file mode 100644 index 0000000000..4a154e2d2e --- /dev/null +++ b/container/podman/handler.go @@ -0,0 +1,280 @@ +// Copyright 2021 Google Inc. All Rights Reserved. +// +// 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. + +// Handler for Podman containers. +package podman + +import ( + "fmt" + "strconv" + "strings" + "time" + + "github.com/google/cadvisor/container" + "github.com/google/cadvisor/container/common" + containerlibcontainer "github.com/google/cadvisor/container/libcontainer" + "github.com/google/cadvisor/fs" + info "github.com/google/cadvisor/info/v1" + + dockercontainer "github.com/docker/docker/api/types/container" + docker "github.com/docker/docker/client" + "golang.org/x/net/context" +) + +type podmanContainerHandler struct { + // machineInfoFactory provides info.MachineInfo + machineInfoFactory info.MachineInfoFactory + + // Absolute path to the cgroup hierarchies of this container. + // (e.g.: "cpu" -> "/sys/fs/cgroup/cpu/test") + cgroupPaths map[string]string + + fsInfo fs.FsInfo + + // Time at which this container was created. + creationTime time.Time + + // Metadata associated with the container. + envs map[string]string + labels map[string]string + + // Image name used for this container. + image string + + // The network mode of the container + networkMode dockercontainer.NetworkMode + + // Filesystem handler. + fsHandler common.FsHandler + + // The IP address of the container + ipAddress string + + includedMetrics container.MetricSet + + // Reference to the container + reference info.ContainerReference + + libcontainerHandler *containerlibcontainer.Handler +} + +var _ container.ContainerHandler = &podmanContainerHandler{} + +// newPodmanContainerHandler returns a new container.ContainerHandler +func newPodmanContainerHandler( + client *docker.Client, + name string, + machineInfoFactory info.MachineInfoFactory, + fsInfo fs.FsInfo, + cgroupSubsystems *containerlibcontainer.CgroupSubsystems, + inHostNamespace bool, + metadataEnvs []string, + dockerVersion []int, + includedMetrics container.MetricSet, +) (container.ContainerHandler, error) { + // Create the cgroup paths. + cgroupPaths := common.MakeCgroupPaths(cgroupSubsystems.MountPoints, name) + + // Generate the equivalent cgroup manager for this container. + cgroupManager, err := containerlibcontainer.NewCgroupManager(name, cgroupPaths) + if err != nil { + return nil, err + } + + id := CgroupNameToPodmanId(name) + + // We assume that if Inspect fails then the container is not known to podman. + ctnr, err := client.ContainerInspect(context.Background(), id) + if err != nil { + return nil, fmt.Errorf("failed to inspect container %q: %v", id, err) + } + + rootFs := "/" + if !inHostNamespace { + rootFs = "/rootfs" + } + + handler := &podmanContainerHandler{ + machineInfoFactory: machineInfoFactory, + cgroupPaths: cgroupPaths, + fsInfo: fsInfo, + envs: make(map[string]string), + labels: ctnr.Config.Labels, + includedMetrics: includedMetrics, + } + // Timestamp returned by Podman is in time.RFC3339Nano format. + handler.creationTime, err = time.Parse(time.RFC3339Nano, ctnr.Created) + if err != nil { + // This should not happen, report the error just in case + return nil, fmt.Errorf("failed to parse the create timestamp %q for container %q: %v", ctnr.Created, id, err) + } + handler.libcontainerHandler = containerlibcontainer.NewHandler(cgroupManager, rootFs, ctnr.State.Pid, includedMetrics) + + // Add the name and bare ID as aliases of the container. + handler.reference = info.ContainerReference{ + Id: id, + Name: name, + Aliases: []string{strings.TrimPrefix(ctnr.Name, "/"), id}, + Namespace: PodmanNamespace, + } + handler.image = ctnr.Config.Image + handler.networkMode = ctnr.HostConfig.NetworkMode + // Only adds restartcount label if it's greater than 0 + if ctnr.RestartCount > 0 { + handler.labels["restartcount"] = strconv.Itoa(ctnr.RestartCount) + } + + // Obtain the IP address for the container. + // If the NetworkMode starts with 'container:' then we need to use the IP address of the container specified. + // This happens in cases such as kubernetes where the containers doesn't have an IP address itself and we need to use the pod's address + ipAddress := ctnr.NetworkSettings.IPAddress + networkMode := string(ctnr.HostConfig.NetworkMode) + if ipAddress == "" && strings.HasPrefix(networkMode, "container:") { + containerID := strings.TrimPrefix(networkMode, "container:") + c, err := client.ContainerInspect(context.Background(), containerID) + if err != nil { + return nil, fmt.Errorf("failed to inspect container %q: %v", id, err) + } + ipAddress = c.NetworkSettings.IPAddress + } + + handler.ipAddress = ipAddress + + // split env vars to get metadata map. + for _, exposedEnv := range metadataEnvs { + if exposedEnv == "" { + // if no dockerEnvWhitelist provided, len(metadataEnvs) == 1, metadataEnvs[0] == "" + continue + } + + for _, envVar := range ctnr.Config.Env { + if envVar != "" { + splits := strings.SplitN(envVar, "=", 2) + if len(splits) == 2 && strings.HasPrefix(splits[0], exposedEnv) { + handler.envs[strings.ToLower(splits[0])] = splits[1] + } + } + } + } + + return handler, nil +} + +func (h *podmanContainerHandler) Start() { + if h.fsHandler != nil { + h.fsHandler.Start() + } +} + +func (h *podmanContainerHandler) Cleanup() { + if h.fsHandler != nil { + h.fsHandler.Stop() + } +} + +func (h *podmanContainerHandler) ContainerReference() (info.ContainerReference, error) { + return h.reference, nil +} + +func (h *podmanContainerHandler) needNet() bool { + if h.includedMetrics.Has(container.NetworkUsageMetrics) { + return !h.networkMode.IsContainer() + } + return false +} + +func (h *podmanContainerHandler) GetSpec() (info.ContainerSpec, error) { + // FIXME: collect FS metrics + hasFilesystem := false + // we optionally collect disk usage metrics + // if includedMetrics.Has(container.DiskUsageMetrics) { + // handler.fsHandler = common.NewFsHandler(common.DefaultPeriod, rootfsStorageDir, storageLogDir, fsInfo) + + spec, err := common.GetSpec(h.cgroupPaths, h.machineInfoFactory, h.needNet(), hasFilesystem) + + spec.Labels = h.labels + spec.Envs = h.envs + spec.Image = h.image + spec.CreationTime = h.creationTime + + return spec, err +} + +func (h *podmanContainerHandler) getFsStats(stats *info.ContainerStats) error { + mi, err := h.machineInfoFactory.GetMachineInfo() + if err != nil { + return err + } + + if h.includedMetrics.Has(container.DiskIOMetrics) { + common.AssignDeviceNamesToDiskStats((*common.MachineInfoNamer)(mi), &stats.DiskIo) + } + + return nil +} + +func (h *podmanContainerHandler) GetStats() (*info.ContainerStats, error) { + stats, err := h.libcontainerHandler.GetStats() + if err != nil { + return stats, err + } + // Clean up stats for containers that don't have their own network - this + // includes containers running in Kubernetes pods that use the network of the + // infrastructure container. This stops metrics being reported multiple times + // for each container in a pod. + if !h.needNet() { + stats.Network = info.NetworkStats{} + } + + // Get filesystem stats. + err = h.getFsStats(stats) + if err != nil { + return stats, err + } + + return stats, nil +} + +func (h *podmanContainerHandler) ListContainers(listType container.ListType) ([]info.ContainerReference, error) { + // No-op for Podman driver. + return []info.ContainerReference{}, nil +} + +func (h *podmanContainerHandler) GetCgroupPath(resource string) (string, error) { + path, ok := h.cgroupPaths[resource] + if !ok { + return "", fmt.Errorf("could not find path for resource %q for container %q", resource, h.reference.Name) + } + return path, nil +} + +func (h *podmanContainerHandler) GetContainerLabels() map[string]string { + return h.labels +} + +func (h *podmanContainerHandler) GetContainerIPAddress() string { + return h.ipAddress +} + +func (h *podmanContainerHandler) ListProcesses(listType container.ListType) ([]int, error) { + return h.libcontainerHandler.GetProcesses() +} + +func (h *podmanContainerHandler) Exists() bool { + return common.CgroupExists(h.cgroupPaths) +} + +func (h *podmanContainerHandler) Type() container.ContainerType { + return container.ContainerTypePodman +} diff --git a/container/podman/install/install.go b/container/podman/install/install.go new file mode 100644 index 0000000000..0ed17d9978 --- /dev/null +++ b/container/podman/install/install.go @@ -0,0 +1,30 @@ +// Copyright 2021 Google Inc. All Rights Reserved. +// +// 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. + +// The install package registers docker.NewPlugin() as the "docker" container provider when imported +package install + +import ( + "github.com/google/cadvisor/container" + "github.com/google/cadvisor/container/podman" + + "k8s.io/klog/v2" +) + +func init() { + err := container.RegisterPlugin("podman", podman.NewPlugin()) + if err != nil { + klog.Fatalf("Failed to register podman plugin: %v", err) + } +} diff --git a/container/podman/plugin.go b/container/podman/plugin.go new file mode 100644 index 0000000000..46403e420f --- /dev/null +++ b/container/podman/plugin.go @@ -0,0 +1,78 @@ +// Copyright 2021 Google Inc. All Rights Reserved. +// +// 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 podman + +import ( + "time" + + "github.com/google/cadvisor/container" + "github.com/google/cadvisor/fs" + info "github.com/google/cadvisor/info/v1" + "github.com/google/cadvisor/watcher" + + "golang.org/x/net/context" + "k8s.io/klog/v2" +) + +const podmanClientTimeout = 10 * time.Second + +// NewPlugin returns an implementation of container.Plugin suitable for passing to container.RegisterPlugin() +func NewPlugin() container.Plugin { + return &plugin{} +} + +type plugin struct{} + +func (p *plugin) InitializeFSContext(context *fs.Context) error { + SetTimeout(podmanClientTimeout) + // Try to connect to podman indefinitely on startup. + podmanStatus := retryPodmanStatus() + context.Podman = fs.PodmanContext{ + Root: RootDir(), + Driver: podmanStatus.Driver, + DriverStatus: podmanStatus.DriverStatus, + } + return nil +} + +func (p *plugin) Register(factory info.MachineInfoFactory, fsInfo fs.FsInfo, includedMetrics container.MetricSet) (watcher.ContainerWatcher, error) { + err := Register(factory, fsInfo, includedMetrics) + return nil, err +} + +func retryPodmanStatus() info.PodmanStatus { + startupTimeout := podmanClientTimeout + maxTimeout := 4 * startupTimeout + for { + ctx, _ := context.WithTimeout(context.Background(), startupTimeout) + podmanStatus, err := StatusWithContext(ctx) + if err == nil { + return podmanStatus + } + + switch err { + case context.DeadlineExceeded: + klog.Warningf("Timeout trying to communicate with podman during initialization, will retry") + default: + klog.V(5).Infof("Podman not connected: %v", err) + return info.PodmanStatus{} + } + + startupTimeout = 2 * startupTimeout + if startupTimeout > maxTimeout { + startupTimeout = maxTimeout + } + } +} diff --git a/container/podman/podman.go b/container/podman/podman.go new file mode 100644 index 0000000000..d7c239801d --- /dev/null +++ b/container/podman/podman.go @@ -0,0 +1,161 @@ +// Copyright 2021 Google Inc. All Rights Reserved. +// +// 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. + +// Provides global docker information. +package podman + +import ( + "fmt" + "regexp" + "strconv" + "time" + + dockertypes "github.com/docker/docker/api/types" + "golang.org/x/net/context" + + "github.com/google/cadvisor/info/v1" + "github.com/google/cadvisor/machine" +) + +var podmanTimeout = 10 * time.Second + +func defaultContext() context.Context { + ctx, _ := context.WithTimeout(context.Background(), podmanTimeout) + return ctx +} + +func SetTimeout(timeout time.Duration) { + podmanTimeout = timeout +} + +func Status() (v1.PodmanStatus, error) { + return StatusWithContext(defaultContext()) +} + +func StatusWithContext(ctx context.Context) (v1.PodmanStatus, error) { + client, err := NewClient() + if err != nil { + return v1.PodmanStatus{}, fmt.Errorf("unable to communicate with podman: %v", err) + } + podmanInfo, err := client.Info(ctx) + if err != nil { + return v1.PodmanStatus{}, err + } + return StatusFromPodmanInfo(podmanInfo) +} + +func StatusFromPodmanInfo(podmanInfo dockertypes.Info) (v1.PodmanStatus, error) { + out := v1.PodmanStatus{} + out.KernelVersion = machine.KernelVersion() + out.OS = podmanInfo.OperatingSystem + out.Hostname = podmanInfo.Name + out.RootDir = podmanInfo.DockerRootDir + out.Driver = podmanInfo.Driver + out.NumImages = podmanInfo.Images + out.NumContainers = podmanInfo.Containers + out.DriverStatus = make(map[string]string, len(podmanInfo.DriverStatus)) + for _, v := range podmanInfo.DriverStatus { + out.DriverStatus[v[0]] = v[1] + } + var err error + ver, err := VersionString() + if err != nil { + return out, err + } + out.Version = ver + ver, err = APIVersionString() + if err != nil { + return out, err + } + out.APIVersion = ver + return out, nil +} + +// Checks whether the podmanInfo reflects a valid docker setup, and returns it if it does, or an +// error otherwise. +func ValidateInfo() (*dockertypes.Info, error) { + client, err := NewClient() + if err != nil { + return nil, fmt.Errorf("unable to communicate with podman: %v", err) + } + + podmanInfo, err := client.Info(defaultContext()) + if err != nil { + return nil, fmt.Errorf("failed to detect podman info: %v", err) + } + + // Fall back to version API if ServerVersion is not set in info. + if podmanInfo.ServerVersion == "" { + version, err := client.ServerVersion(defaultContext()) + if err != nil { + return nil, fmt.Errorf("unable to get podman version: %v", err) + } + podmanInfo.ServerVersion = version.Version + } + + if podmanInfo.Driver == "" { + return nil, fmt.Errorf("failed to find podman storage driver") + } + + return &podmanInfo, nil +} + +func APIVersion() ([]int, error) { + ver, err := APIVersionString() + if err != nil { + return nil, err + } + return parseVersion(ver, apiVersionRe, 2) +} + +func VersionString() (string, error) { + podmanVersion := "Unknown" + client, err := NewClient() + if err == nil { + version, err := client.ServerVersion(defaultContext()) + if err == nil { + podmanVersion = version.Version + } + } + return podmanVersion, err +} + +func APIVersionString() (string, error) { + apiVersion := "Unknown" + client, err := NewClient() + if err == nil { + version, err := client.ServerVersion(defaultContext()) + if err == nil { + apiVersion = version.APIVersion + } + } + return apiVersion, err +} + +func parseVersion(versionString string, regex *regexp.Regexp, length int) ([]int, error) { + matches := regex.FindAllStringSubmatch(versionString, -1) + if len(matches) != 1 { + return nil, fmt.Errorf("version string \"%v\" doesn't match expected regular expression: \"%v\"", versionString, regex.String()) + } + versionStringArray := matches[0][1:] + versionArray := make([]int, length) + for index, versionStr := range versionStringArray { + version, err := strconv.Atoi(versionStr) + if err != nil { + return nil, fmt.Errorf("error while parsing \"%v\" in \"%v\": %w", versionStr, versionString, err) + } + versionArray[index] = version + } + return versionArray, nil +} diff --git a/fs/types.go b/fs/types.go index 93294ebcc2..4b5a8ecca7 100644 --- a/fs/types.go +++ b/fs/types.go @@ -22,6 +22,7 @@ type Context struct { // docker root directory. Docker DockerContext Crio CrioContext + Podman PodmanContext } type DockerContext struct { @@ -30,6 +31,12 @@ type DockerContext struct { DriverStatus map[string]string } +type PodmanContext struct { + Root string + Driver string + DriverStatus map[string]string +} + type CrioContext struct { Root string } diff --git a/info/v1/podman.go b/info/v1/podman.go new file mode 100644 index 0000000000..a3a0129886 --- /dev/null +++ b/info/v1/podman.go @@ -0,0 +1,38 @@ +// Copyright 2021 Google Inc. All Rights Reserved. +// +// 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. + +// Types used for podman containers. +package v1 + +type PodmanStatus struct { + Version string `json:"version"` + APIVersion string `json:"api_version"` + KernelVersion string `json:"kernel_version"` + OS string `json:"os"` + Hostname string `json:"hostname"` + RootDir string `json:"root_dir"` + Driver string `json:"driver"` + DriverStatus map[string]string `json:"driver_status"` + ExecDriver string `json:"exec_driver"` + NumImages int `json:"num_images"` + NumContainers int `json:"num_containers"` +} + +type PodmanImage struct { + ID string `json:"id"` + RepoTags []string `json:"repo_tags"` // repository name and tags. + Created int64 `json:"created"` // unix time since creation. + VirtualSize int64 `json:"virtual_size"` + Size int64 `json:"size"` +}