github.com/inspektor-gadget/inspektor-gadget@v0.28.1/pkg/container-utils/docker/docker.go (about)

     1  // Copyright 2019-2022 The Inspektor Gadget authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package docker
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"fmt"
    21  	"strings"
    22  	"time"
    23  
    24  	dockertypes "github.com/docker/docker/api/types"
    25  	"github.com/docker/docker/api/types/container"
    26  	dockerfilters "github.com/docker/docker/api/types/filters"
    27  	"github.com/docker/docker/client"
    28  	log "github.com/sirupsen/logrus"
    29  
    30  	"github.com/inspektor-gadget/inspektor-gadget/pkg/container-utils/cgroups"
    31  	"github.com/inspektor-gadget/inspektor-gadget/pkg/container-utils/cri"
    32  	runtimeclient "github.com/inspektor-gadget/inspektor-gadget/pkg/container-utils/runtime-client"
    33  	containerutilsTypes "github.com/inspektor-gadget/inspektor-gadget/pkg/container-utils/types"
    34  	"github.com/inspektor-gadget/inspektor-gadget/pkg/types"
    35  )
    36  
    37  const (
    38  	DefaultTimeout = 2 * time.Second
    39  )
    40  
    41  // DockerClient implements the ContainerRuntimeClient interface but using the
    42  // Docker Engine API instead of the CRI plugin interface (Dockershim). It was
    43  // necessary because Dockershim does not always use the same approach of CRI-O
    44  // and containerd. For instance, Dockershim does not provide the container pid1
    45  // with the ContainerStatus() call as containerd and CRI-O do.
    46  type DockerClient struct {
    47  	client     *client.Client
    48  	socketPath string
    49  }
    50  
    51  func NewDockerClient(socketPath string, protocol string) (runtimeclient.ContainerRuntimeClient, error) {
    52  	switch protocol {
    53  	// Empty string falls back to "internal". Used by unit tests.
    54  	case "", containerutilsTypes.RuntimeProtocolInternal:
    55  		// handled below
    56  
    57  	case containerutilsTypes.RuntimeProtocolCRI:
    58  		// TODO: Configurable
    59  		socketPath = runtimeclient.CriDockerDefaultSocketPath
    60  		return cri.NewCRIClient(types.RuntimeNameDocker, socketPath, DefaultTimeout)
    61  
    62  	default:
    63  		return nil, fmt.Errorf("unknown runtime protocol %q", protocol)
    64  	}
    65  
    66  	if socketPath == "" {
    67  		socketPath = runtimeclient.DockerDefaultSocketPath
    68  	}
    69  
    70  	cli, err := client.NewClientWithOpts(
    71  		client.WithAPIVersionNegotiation(),
    72  		client.WithHost("unix://"+socketPath),
    73  		client.WithTimeout(DefaultTimeout),
    74  	)
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  
    79  	return &DockerClient{
    80  		client:     cli,
    81  		socketPath: socketPath,
    82  	}, nil
    83  }
    84  
    85  func listContainers(c *DockerClient, filter *dockerfilters.Args) ([]dockertypes.Container, error) {
    86  	opts := container.ListOptions{
    87  		// We need to request for all containers (also non-running) because
    88  		// when we are enriching a container that is being created, it is
    89  		// not in "running" state yet.
    90  		All: true,
    91  	}
    92  	if filter != nil {
    93  		opts.Filters = *filter
    94  	}
    95  
    96  	containers, err := c.client.ContainerList(context.Background(), opts)
    97  	if err != nil {
    98  		return nil, fmt.Errorf("listing containers with options %+v: %w",
    99  			opts, err)
   100  	}
   101  
   102  	// Temporarily drop pod sandbox containers. Otherwise, they will be
   103  	// considered as normal containers and EnrichByNetNs will incorrectly think
   104  	// that they are using a given network namespace. See issue
   105  	// https://github.com/inspektor-gadget/inspektor-gadget/issues/1095.
   106  	noPauseContainers := []dockertypes.Container{}
   107  	for _, c := range containers {
   108  		if c.Labels["io.kubernetes.docker.type"] == "podsandbox" {
   109  			continue
   110  		}
   111  		noPauseContainers = append(noPauseContainers, c)
   112  	}
   113  	if filter != nil && len(containers) != 0 && len(noPauseContainers) == 0 {
   114  		return nil, runtimeclient.ErrPauseContainer
   115  	}
   116  
   117  	return noPauseContainers, nil
   118  }
   119  
   120  func (c *DockerClient) GetContainers() ([]*runtimeclient.ContainerData, error) {
   121  	containers, err := listContainers(c, nil)
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  
   126  	ret := make([]*runtimeclient.ContainerData, len(containers))
   127  
   128  	for i, container := range containers {
   129  		ret[i] = DockerContainerToContainerData(&container)
   130  	}
   131  
   132  	return ret, nil
   133  }
   134  
   135  func (c *DockerClient) GetContainer(containerID string) (*runtimeclient.ContainerData, error) {
   136  	filter := dockerfilters.NewArgs()
   137  	filter.Add("id", containerID)
   138  
   139  	containers, err := listContainers(c, &filter)
   140  	if err != nil {
   141  		return nil, err
   142  	}
   143  
   144  	if len(containers) == 0 {
   145  		return nil, fmt.Errorf("container %q not found", containerID)
   146  	}
   147  	if len(containers) > 1 {
   148  		log.Warnf("DockerClient: multiple containers (%d) with ID %q. Taking the first one: %+v",
   149  			len(containers), containerID, containers)
   150  	}
   151  
   152  	return DockerContainerToContainerData(&containers[0]), nil
   153  }
   154  
   155  func (c *DockerClient) GetContainerDetails(containerID string) (*runtimeclient.ContainerDetailsData, error) {
   156  	containerID, err := runtimeclient.ParseContainerID(types.RuntimeNameDocker, containerID)
   157  	if err != nil {
   158  		return nil, err
   159  	}
   160  
   161  	containerJSON, err := c.client.ContainerInspect(context.Background(), containerID)
   162  	if err != nil {
   163  		return nil, err
   164  	}
   165  
   166  	if containerJSON.State == nil {
   167  		return nil, errors.New("container state is nil")
   168  	}
   169  	if containerJSON.State.Pid == 0 {
   170  		return nil, errors.New("got zero pid")
   171  	}
   172  	if containerJSON.Config == nil {
   173  		return nil, errors.New("container config is nil")
   174  	}
   175  	if containerJSON.HostConfig == nil {
   176  		return nil, errors.New("container host config is nil")
   177  	}
   178  
   179  	containerData := buildContainerData(
   180  		containerJSON.ID,
   181  		containerJSON.Name,
   182  		containerJSON.Config.Image,
   183  		containerJSON.State.Status,
   184  		containerJSON.Config.Labels)
   185  
   186  	containerDetailsData := runtimeclient.ContainerDetailsData{
   187  		ContainerData: *containerData,
   188  		Pid:           containerJSON.State.Pid,
   189  		CgroupsPath:   string(containerJSON.HostConfig.Cgroup),
   190  	}
   191  	if len(containerJSON.Mounts) > 0 {
   192  		containerDetailsData.Mounts = make([]runtimeclient.ContainerMountData, len(containerJSON.Mounts))
   193  		for i, containerMount := range containerJSON.Mounts {
   194  			containerDetailsData.Mounts[i] = runtimeclient.ContainerMountData{
   195  				Destination: containerMount.Destination,
   196  				Source:      containerMount.Source,
   197  			}
   198  		}
   199  	}
   200  
   201  	// Try to get cgroups information from /proc/<pid>/cgroup as a fallback.
   202  	// However, don't fail if such a file is not available, as it would prevent the
   203  	// whole feature to work on systems without this file.
   204  	if containerDetailsData.CgroupsPath == "" {
   205  		log.Debugf("cgroups info not available on Docker for container %s. Trying /proc/%d/cgroup as a fallback",
   206  			containerID, containerDetailsData.Pid)
   207  
   208  		// Get cgroup paths for V1 and V2.
   209  		cgroupPathV1, cgroupPathV2, err := cgroups.GetCgroupPaths(containerDetailsData.Pid)
   210  		if err == nil {
   211  			cgroupsPath := cgroupPathV1
   212  			if cgroupsPath == "" {
   213  				cgroupsPath = cgroupPathV2
   214  			}
   215  			containerDetailsData.CgroupsPath = cgroupsPath
   216  		} else {
   217  			log.Warnf("failed to get cgroups info of container %s from /proc/%d/cgroup: %s",
   218  				containerID, containerDetailsData.Pid, err)
   219  		}
   220  	}
   221  
   222  	return &containerDetailsData, nil
   223  }
   224  
   225  func (c *DockerClient) Close() error {
   226  	if c.client != nil {
   227  		return c.client.Close()
   228  	}
   229  
   230  	return nil
   231  }
   232  
   233  // Convert the state from container status to state of runtime client.
   234  func containerStatusStateToRuntimeClientState(containerState string) (runtimeClientState string) {
   235  	switch containerState {
   236  	case "created":
   237  		runtimeClientState = runtimeclient.StateCreated
   238  	case "running":
   239  		runtimeClientState = runtimeclient.StateRunning
   240  	case "exited":
   241  		runtimeClientState = runtimeclient.StateExited
   242  	case "dead":
   243  		runtimeClientState = runtimeclient.StateExited
   244  	default:
   245  		runtimeClientState = runtimeclient.StateUnknown
   246  	}
   247  	return
   248  }
   249  
   250  func DockerContainerToContainerData(container *dockertypes.Container) *runtimeclient.ContainerData {
   251  	return buildContainerData(
   252  		container.ID,
   253  		container.Names[0],
   254  		container.Image,
   255  		container.State,
   256  		container.Labels)
   257  }
   258  
   259  // getContainerImageNamefromImage is a helper to parse the image string we get from Docker API
   260  // and retrieve the image name if provided.
   261  func getContainerImageNamefromImage(image string) string {
   262  	// Image filed provided by Docker API may looks like e.g.
   263  	// 1. gcr.io/k8s-minikube/kicbase:v0.0.37@sha256:8bf7a0e8a062bc5e2b71d28b35bfa9cc862d9220e234e86176b3785f685d8b15
   264  	// OR
   265  	// 2. busybox@sha256:3fbc632167424a6d997e74f52b878d7cc478225cffac6bc977eedfe51c7f4e79
   266  	// These two provide both image name and digest separated by '@'.
   267  	//
   268  	// 3. docker.io/library/busybox:latest or simply busybox
   269  	// Just image name is provided.
   270  	//
   271  	// 4. sha256:aebe758cef4cd05b9f8cee39758227714d02f42ef3088023c1e3cd454f927a2b
   272  	// This fourth option provides the imageID and, following Docker example, we'll use the imageID.
   273  
   274  	// Case 1 or 2
   275  	if strings.Contains(image, "@") {
   276  		return strings.Split(image, "@")[0]
   277  	}
   278  
   279  	// Case 3 or 4
   280  	return image
   281  }
   282  
   283  // `buildContainerData` takes in basic metadata about a Docker container and
   284  // constructs a `runtimeclient.ContainerData` struct with this information. I also
   285  // enriches containers with the data and returns a pointer the created struct.
   286  func buildContainerData(containerID string, containerName string, containerImage string, state string, labels map[string]string) *runtimeclient.ContainerData {
   287  	containerData := runtimeclient.ContainerData{
   288  		Runtime: runtimeclient.RuntimeContainerData{
   289  			BasicRuntimeMetadata: types.BasicRuntimeMetadata{
   290  				ContainerID:        containerID,
   291  				ContainerName:      strings.TrimPrefix(containerName, "/"),
   292  				RuntimeName:        types.RuntimeNameDocker,
   293  				ContainerImageName: getContainerImageNamefromImage(containerImage),
   294  			},
   295  			State: containerStatusStateToRuntimeClientState(state),
   296  		},
   297  	}
   298  
   299  	// Fill K8S information.
   300  	runtimeclient.EnrichWithK8sMetadata(&containerData, labels)
   301  
   302  	return &containerData
   303  }