Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial podman support #2794

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/internal/container/install/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
1 change: 1 addition & 0 deletions container/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const (
ContainerTypeCrio
ContainerTypeContainerd
ContainerTypeMesos
ContainerTypePodman
)

// Interface for container operation handlers.
Expand Down
61 changes: 61 additions & 0 deletions container/podman/client.go
Original file line number Diff line number Diff line change
@@ -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
}
212 changes: 212 additions & 0 deletions container/podman/factory.go
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expected some context to be added before returning the error. I'm not sure if using named returned valued does not decrease code readability.

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 {
towe75 marked this conversation as resolved.
Show resolved Hide resolved
// 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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are two potentially different scenarios:

  • err != nil
  • container is not running.

Can you return more specific error, please (hint: %w is great for wrapping errors).

}

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)
)

Comment on lines +170 to +176
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if it is necessary to rely on regular expressions to parse versions. As far as I understand we expect them to be strings received from Podman API so perhaps splitting these strings an calling Atoi could work?

// 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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider wrapping the error.

}

podmanInfo, err := ValidateInfo()
if err != nil {
return fmt.Errorf("failed to validate Podman info: %v", err)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

}

// Version already validated above, assume no error here.
podmanVersion, _ := parseVersion(podmanInfo.ServerVersion, versionRe, 3)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the version is already validated then why don't you just split a string? Regular expression might be an overkill in this scenario.


podmanAPIVersion, _ := APIVersion()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why have you decided to ignore the error? You can assume this call will never fail.


cgroupSubsystems, err := libcontainer.GetCgroupSubsystems(includedMetrics)
if err != nil {
return fmt.Errorf("failed to get cgroup subsystems: %v", err)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrap an error, please.

}

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
}
Loading