github.com/inspektor-gadget/inspektor-gadget@v0.28.1/pkg/container-utils/containerd/containerd.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 containerd
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"time"
    21  
    22  	"github.com/containerd/containerd"
    23  	"github.com/containerd/containerd/errdefs"
    24  	"github.com/containerd/containerd/namespaces"
    25  	"github.com/containerd/containerd/pkg/cri/constants"
    26  	"github.com/sirupsen/logrus"
    27  	"google.golang.org/grpc"
    28  	"google.golang.org/grpc/credentials/insecure"
    29  
    30  	"github.com/inspektor-gadget/inspektor-gadget/pkg/container-utils/cri"
    31  	runtimeclient "github.com/inspektor-gadget/inspektor-gadget/pkg/container-utils/runtime-client"
    32  	containerutilsTypes "github.com/inspektor-gadget/inspektor-gadget/pkg/container-utils/types"
    33  	"github.com/inspektor-gadget/inspektor-gadget/pkg/types"
    34  )
    35  
    36  const (
    37  	DefaultTimeout = 2 * time.Second
    38  
    39  	LabelK8sContainerName         = "io.kubernetes.container.name"
    40  	LabelK8sContainerdKind        = "io.cri-containerd.kind"
    41  	LabelK8sContainerdKindSandbox = "sandbox"
    42  )
    43  
    44  var log = logrus.WithField("container-client", "containerd")
    45  
    46  type ContainerdClient struct {
    47  	client *containerd.Client
    48  	ctx    context.Context
    49  }
    50  
    51  func NewContainerdClient(socketPath string, protocol string, config *containerutilsTypes.ExtraConfig) (runtimeclient.ContainerRuntimeClient, error) {
    52  	if socketPath == "" {
    53  		socketPath = runtimeclient.ContainerdDefaultSocketPath
    54  	}
    55  
    56  	switch protocol {
    57  	// Empty string falls back to "internal". Used by unit tests.
    58  	case "", containerutilsTypes.RuntimeProtocolInternal:
    59  		// handled below
    60  
    61  	case containerutilsTypes.RuntimeProtocolCRI:
    62  		return cri.NewCRIClient(types.RuntimeNameContainerd, socketPath, DefaultTimeout)
    63  
    64  	default:
    65  		return nil, fmt.Errorf("unknown runtime protocol %q", protocol)
    66  	}
    67  
    68  	namespace := constants.K8sContainerdNamespace
    69  	if config != nil && config.Namespace != "" {
    70  		namespace = config.Namespace
    71  	}
    72  
    73  	dialCtx, cancelFunc := context.WithTimeout(context.TODO(), DefaultTimeout)
    74  	defer cancelFunc()
    75  	grpcConn, err := grpc.DialContext(
    76  		dialCtx,
    77  		"unix:"+socketPath,
    78  		grpc.WithTransportCredentials(insecure.NewCredentials()),
    79  	)
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  
    84  	client, err := containerd.NewWithConn(grpcConn)
    85  	if err != nil {
    86  		return nil, err
    87  	}
    88  
    89  	containerdCtx := namespaces.WithNamespace(
    90  		context.TODO(),
    91  		namespace,
    92  	)
    93  
    94  	return &ContainerdClient{
    95  		client: client,
    96  		ctx:    containerdCtx,
    97  	}, nil
    98  }
    99  
   100  func (c *ContainerdClient) Close() error {
   101  	if c.client != nil {
   102  		return c.client.Close()
   103  	}
   104  	return nil
   105  }
   106  
   107  func (c *ContainerdClient) GetContainers() ([]*runtimeclient.ContainerData, error) {
   108  	containers, err := c.client.Containers(c.ctx)
   109  	if err != nil {
   110  		return nil, fmt.Errorf("listing containers: %w", err)
   111  	}
   112  
   113  	ret := make([]*runtimeclient.ContainerData, 0, len(containers))
   114  	for _, container := range containers {
   115  		if c.isSandboxContainer(container) {
   116  			log.Debugf("container %q is a sandbox container. Temporary skipping it", container.ID())
   117  			continue
   118  		}
   119  
   120  		task, err := c.getContainerTask(container)
   121  		if err != nil {
   122  			log.Debugf("getting containerTask for container %q: %s", container.ID(), err)
   123  			continue
   124  		}
   125  
   126  		containerData, err := c.taskAndContainerToContainerData(task, container)
   127  		if err != nil {
   128  			log.Debugf("creating containerData for container %q: %s", container.ID(), err)
   129  			continue
   130  		}
   131  
   132  		ret = append(ret, containerData)
   133  	}
   134  
   135  	return ret, nil
   136  }
   137  
   138  func (c *ContainerdClient) GetContainer(containerID string) (*runtimeclient.ContainerData, error) {
   139  	container, err := c.getContainer(containerID)
   140  	if err != nil {
   141  		return nil, err
   142  	}
   143  
   144  	return c.buildContainerData(container, nil)
   145  }
   146  
   147  func (c *ContainerdClient) GetContainerDetails(containerID string) (*runtimeclient.ContainerDetailsData, error) {
   148  	containerID, err := runtimeclient.ParseContainerID(types.RuntimeNameContainerd, containerID)
   149  	if err != nil {
   150  		return nil, err
   151  	}
   152  
   153  	containerData, container, task, err := c.getContainerDataAndContainerAndTask(containerID)
   154  	if err != nil {
   155  		return nil, err
   156  	}
   157  	if task.pid == 0 {
   158  		return nil, fmt.Errorf("got zero pid")
   159  	}
   160  
   161  	spec, err := container.Spec(c.ctx)
   162  	if err != nil {
   163  		return nil, fmt.Errorf("getting spec for container %q: %w", containerID, err)
   164  	}
   165  
   166  	mountData := make([]runtimeclient.ContainerMountData, len(spec.Mounts))
   167  	for i := range spec.Mounts {
   168  		mount := spec.Mounts[i]
   169  		mountData[i] = runtimeclient.ContainerMountData{
   170  			Source:      mount.Source,
   171  			Destination: mount.Destination,
   172  		}
   173  	}
   174  
   175  	return &runtimeclient.ContainerDetailsData{
   176  		ContainerData: *containerData,
   177  		Pid:           int(task.pid),
   178  		CgroupsPath:   spec.Linux.CgroupsPath,
   179  		Mounts:        mountData,
   180  	}, nil
   181  }
   182  
   183  func (c *ContainerdClient) getContainerDataAndContainerAndTask(containerID string) (*runtimeclient.ContainerData, containerd.Container, *containerTask, error) {
   184  	container, err := c.getContainer(containerID)
   185  	if err != nil {
   186  		return nil, nil, nil, err
   187  	}
   188  
   189  	task, err := c.getContainerTask(container)
   190  	if err != nil {
   191  		return nil, nil, nil, err
   192  	}
   193  
   194  	containerData, err := c.taskAndContainerToContainerData(task, container)
   195  	if err != nil {
   196  		return nil, nil, nil, err
   197  	}
   198  
   199  	return containerData, container, task, nil
   200  }
   201  
   202  // getContainer returns the corresponding container.Container instance to
   203  // the given id
   204  func (c *ContainerdClient) getContainer(id string) (containerd.Container, error) {
   205  	container, err := c.client.LoadContainer(c.ctx, id)
   206  	if err != nil {
   207  		return nil, fmt.Errorf("loading container with id %q: %w", id, err)
   208  	}
   209  
   210  	if c.isSandboxContainer(container) {
   211  		log.Debugf("container %q is a sandbox container. Temporary skipping it", container.ID())
   212  		return nil, runtimeclient.ErrPauseContainer
   213  	}
   214  
   215  	return container, nil
   216  }
   217  
   218  // containerTask represents the task information for a given container.
   219  type containerTask struct {
   220  	status string
   221  	pid    uint32
   222  }
   223  
   224  // getContainerTask returns the containerTask information for a given container.
   225  // If the container is not running yet, it returns a containerTask with status
   226  // StateCreated and pid 0.
   227  func (c *ContainerdClient) getContainerTask(container containerd.Container) (*containerTask, error) {
   228  	task, err := container.Task(c.ctx, nil)
   229  	if err != nil {
   230  		// According to nerdctl, if there is no task, we can assume the
   231  		// container was just created but it is not running yet:
   232  		// https://github.com/containerd/nerdctl/blob/b0a75d880ef9e6d1f1a8752804c9087ee2d02f73/pkg/formatter/formatter.go#L48-L55
   233  		if !errdefs.IsNotFound(err) {
   234  			return nil, fmt.Errorf("getting task for container %q: %w", container.ID(), err)
   235  		}
   236  
   237  		t := &containerTask{
   238  			status: runtimeclient.StateCreated,
   239  			pid:    0,
   240  		}
   241  		log.Debugf("No task for %q. Assuming it is in %q status", container.ID(), t.status)
   242  
   243  		return t, nil
   244  	}
   245  
   246  	containerdStatus, err := task.Status(c.ctx)
   247  	if err != nil {
   248  		return nil, fmt.Errorf("getting status of task for container %q: %w", container.ID(), err)
   249  	}
   250  
   251  	return &containerTask{
   252  		status: processStatusStateToRuntimeClientState(containerdStatus.Status),
   253  		pid:    task.Pid(),
   254  	}, nil
   255  }
   256  
   257  // Constructs a ContainerData from a containerTask and containerd.Container
   258  // The extra containerd.Container parameter saves an additional call to the API
   259  func (c *ContainerdClient) taskAndContainerToContainerData(task *containerTask, container containerd.Container) (*runtimeclient.ContainerData, error) {
   260  	return c.buildContainerData(container, task)
   261  }
   262  
   263  // Checks if the K8s Label for the Containerkind equals to sandbox
   264  func (c *ContainerdClient) isSandboxContainer(container containerd.Container) bool {
   265  	labels, err := container.Labels(c.ctx)
   266  	if err != nil {
   267  		return false
   268  	}
   269  
   270  	if kind, ok := labels[LabelK8sContainerdKind]; ok {
   271  		return kind == LabelK8sContainerdKindSandbox
   272  	}
   273  
   274  	return false
   275  }
   276  
   277  // Convert the state from container status to state of runtime client.
   278  func processStatusStateToRuntimeClientState(status containerd.ProcessStatus) string {
   279  	switch status {
   280  	case containerd.Created:
   281  		return runtimeclient.StateCreated
   282  	case containerd.Running:
   283  		return runtimeclient.StateRunning
   284  	case containerd.Stopped:
   285  		return runtimeclient.StateExited
   286  	default:
   287  		return runtimeclient.StateUnknown
   288  	}
   289  }
   290  
   291  // getContainerName returns the name of the container. If the container is
   292  // managed by Kubernetes, it returns the name of the container as defined in
   293  // Kubernetes. Otherwise, it returns the container ID.
   294  func getContainerName(container containerd.Container, labels map[string]string) string {
   295  	if k8sName, ok := labels[LabelK8sContainerName]; ok {
   296  		return k8sName
   297  	}
   298  
   299  	return container.ID()
   300  }
   301  
   302  // buildContainerData  retrieves and sets basic runtime metadata for a given container,
   303  // including its ID, name, runtime name, image name, and image digest. It also retrieves and sets
   304  // the container's state (which is set to "Running" by default, unless a container task is provided).
   305  func (c *ContainerdClient) buildContainerData(container containerd.Container, task *containerTask) (*runtimeclient.ContainerData, error) {
   306  	labels, err := container.Labels(c.ctx)
   307  	if err != nil {
   308  		return nil, fmt.Errorf("listing labels of container %q: %w", container.ID(), err)
   309  	}
   310  
   311  	image, err := container.Image(c.ctx)
   312  	if err != nil {
   313  		return nil, fmt.Errorf("getting image of container %q: %w", container.ID(), err)
   314  	}
   315  
   316  	// When `task` is nil, state is getting set to `Running` for the following reasons:
   317  	// 1. `buildContainerData` is called by `GetContainer`, which is only getting called on
   318  	//    new created containers
   319  	// 2. We would need to get the Task for the Container. containerd needs to acquire a mutex
   320  	//    that is currently hold by the creating process, which we interrupted -> deadlock
   321  	taskState := runtimeclient.StateRunning
   322  	if task != nil {
   323  		taskState = task.status
   324  	}
   325  
   326  	containerData := &runtimeclient.ContainerData{
   327  		Runtime: runtimeclient.RuntimeContainerData{
   328  			BasicRuntimeMetadata: types.BasicRuntimeMetadata{
   329  				ContainerID:          container.ID(),
   330  				ContainerName:        getContainerName(container, labels),
   331  				RuntimeName:          types.RuntimeNameContainerd,
   332  				ContainerImageName:   image.Name(),
   333  				ContainerImageDigest: image.Metadata().Target.Digest.String(),
   334  			},
   335  			State: taskState,
   336  		},
   337  	}
   338  	runtimeclient.EnrichWithK8sMetadata(containerData, labels)
   339  
   340  	return containerData, nil
   341  }