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

     1  // Copyright 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 cri
    16  
    17  import (
    18  	"context"
    19  	"encoding/json"
    20  	"fmt"
    21  	"net"
    22  	"strconv"
    23  	"strings"
    24  	"time"
    25  
    26  	log "github.com/sirupsen/logrus"
    27  	"google.golang.org/grpc"
    28  	"google.golang.org/grpc/credentials/insecure"
    29  	runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
    30  
    31  	runtimeclient "github.com/inspektor-gadget/inspektor-gadget/pkg/container-utils/runtime-client"
    32  	"github.com/inspektor-gadget/inspektor-gadget/pkg/types"
    33  )
    34  
    35  // podLabelFilter is a set of labels that are used to filter out pod sandbox labels
    36  // that are not user given K8s labels of the pod
    37  var podLabelFilter = map[string]struct{}{
    38  	runtimeclient.ContainerLabelK8sContainerName: {},
    39  	runtimeclient.ContainerLabelK8sPodName:       {},
    40  	runtimeclient.ContainerLabelK8sPodNamespace:  {},
    41  	runtimeclient.ContainerLabelK8sPodUID:        {},
    42  }
    43  
    44  // CRIClient implements the ContainerRuntimeClient interface using the CRI
    45  // plugin interface to communicate with the different container runtimes.
    46  type CRIClient struct {
    47  	Name        types.RuntimeName
    48  	SocketPath  string
    49  	ConnTimeout time.Duration
    50  
    51  	conn   *grpc.ClientConn
    52  	client runtime.RuntimeServiceClient
    53  }
    54  
    55  func NewCRIClient(name types.RuntimeName, socketPath string, timeout time.Duration) (*CRIClient, error) {
    56  	conn, err := grpc.Dial(
    57  		socketPath,
    58  		grpc.WithTransportCredentials(insecure.NewCredentials()),
    59  		grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
    60  			d := net.Dialer{Timeout: timeout}
    61  			return d.DialContext(ctx, "unix", socketPath)
    62  		}),
    63  	)
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  
    68  	return &CRIClient{
    69  		Name:        name,
    70  		SocketPath:  socketPath,
    71  		ConnTimeout: timeout,
    72  		conn:        conn,
    73  		client:      runtime.NewRuntimeServiceClient(conn),
    74  	}, nil
    75  }
    76  
    77  func listContainers(c *CRIClient, filter *runtime.ContainerFilter) ([]*runtime.Container, error) {
    78  	request := &runtime.ListContainersRequest{}
    79  	if filter != nil {
    80  		request.Filter = filter
    81  	}
    82  
    83  	res, err := c.client.ListContainers(context.Background(), request)
    84  	if err != nil {
    85  		return nil, fmt.Errorf("listing containers with request %+v: %w",
    86  			request, err)
    87  	}
    88  
    89  	return res.GetContainers(), nil
    90  }
    91  
    92  func listPodSandboxes(c *CRIClient, filter *runtime.PodSandboxFilter) ([]*runtime.PodSandbox, error) {
    93  	podRequest := &runtime.ListPodSandboxRequest{
    94  		Filter: filter,
    95  	}
    96  
    97  	podRes, err := c.client.ListPodSandbox(context.Background(), podRequest)
    98  	if err != nil {
    99  		return nil, fmt.Errorf("listing pod sandboxes with request %+v: %w", podRequest, err)
   100  	}
   101  
   102  	return podRes.Items, nil
   103  }
   104  
   105  func getPodSandbox(c *CRIClient, podSandboxID string) (*runtime.PodSandbox, error) {
   106  	podSandboxes, err := listPodSandboxes(c, &runtime.PodSandboxFilter{
   107  		Id: podSandboxID,
   108  	})
   109  	if err != nil {
   110  		return nil, err
   111  	}
   112  
   113  	if len(podSandboxes) == 0 {
   114  		return nil, fmt.Errorf("pod sandbox %q not found", podSandboxID)
   115  	}
   116  	if len(podSandboxes) > 1 {
   117  		log.Errorf("CRIClient: found multiple pod sandboxes (%d) with ID %q. Taking the first one: %+v",
   118  			len(podSandboxes), podSandboxID, podSandboxes)
   119  	}
   120  	return podSandboxes[0], nil
   121  }
   122  
   123  func (c *CRIClient) GetContainers() ([]*runtimeclient.ContainerData, error) {
   124  	containers, err := listContainers(c, nil)
   125  	if err != nil {
   126  		return nil, err
   127  	}
   128  
   129  	podSandboxes, err := listPodSandboxes(c, nil)
   130  	if err != nil {
   131  		return nil, err
   132  	}
   133  	podSandboxesMap := make(map[string]*runtime.PodSandbox, len(podSandboxes))
   134  	for _, podSandbox := range podSandboxes {
   135  		podSandboxesMap[podSandbox.Id] = podSandbox
   136  	}
   137  
   138  	ret := make([]*runtimeclient.ContainerData, len(containers))
   139  
   140  	for i, container := range containers {
   141  		podSandbox, ok := podSandboxesMap[container.PodSandboxId]
   142  		if !ok {
   143  			return nil, fmt.Errorf("pod sandbox %q not found for container %q", container.PodSandboxId, container.Id)
   144  		}
   145  		ret[i] = buildContainerData(c.Name, container, podSandbox)
   146  	}
   147  
   148  	return ret, nil
   149  }
   150  
   151  func (c *CRIClient) GetContainer(containerID string) (*runtimeclient.ContainerData, error) {
   152  	containers, err := listContainers(c, &runtime.ContainerFilter{
   153  		Id: containerID,
   154  	})
   155  	if err != nil {
   156  		return nil, err
   157  	}
   158  
   159  	if len(containers) == 0 {
   160  		// Test if the containerID belongs to a pause container
   161  		_, err := getPodSandbox(c, containerID)
   162  		if err != nil {
   163  			// It is not a pause container or we got an error
   164  			return nil, fmt.Errorf("container %q not found", containerID)
   165  		}
   166  		return nil, runtimeclient.ErrPauseContainer
   167  	}
   168  	if len(containers) > 1 {
   169  		log.Errorf("CRIClient: multiple containers (%d) with ID %q. Taking the first one: %+v",
   170  			len(containers), containerID, containers)
   171  	}
   172  
   173  	podSandbox, err := getPodSandbox(c, containers[0].PodSandboxId)
   174  	if err != nil {
   175  		return nil, err
   176  	}
   177  
   178  	containerData := buildContainerData(c.Name, containers[0], podSandbox)
   179  	return containerData, nil
   180  }
   181  
   182  func (c *CRIClient) GetContainerDetails(containerID string) (*runtimeclient.ContainerDetailsData, error) {
   183  	containerID, err := runtimeclient.ParseContainerID(c.Name, containerID)
   184  	if err != nil {
   185  		return nil, err
   186  	}
   187  
   188  	request := &runtime.ContainerStatusRequest{
   189  		ContainerId: containerID,
   190  		Verbose:     true,
   191  	}
   192  
   193  	res, err := c.client.ContainerStatus(context.Background(), request)
   194  	if err != nil {
   195  		return nil, err
   196  	}
   197  
   198  	podSandbox, err := c.getPodSandboxFromContainerID(containerID)
   199  	if err != nil {
   200  		return nil, err
   201  	}
   202  
   203  	return parseContainerDetailsData(c.Name, res.Status, res.Info, podSandbox)
   204  }
   205  
   206  func (c *CRIClient) GetPodLabels(sandboxId string) (map[string]string, error) {
   207  	podSandbox, err := getPodSandbox(c, sandboxId)
   208  	if err != nil {
   209  		return nil, err
   210  	}
   211  
   212  	return getFilteredPodLabels(podSandbox), nil
   213  }
   214  
   215  func (c *CRIClient) getPodSandboxFromContainerID(containerID string) (*runtime.PodSandbox, error) {
   216  	containerID, err := runtimeclient.ParseContainerID(c.Name, containerID)
   217  	if err != nil {
   218  		return nil, err
   219  	}
   220  
   221  	containers, err := listContainers(c, &runtime.ContainerFilter{
   222  		Id: containerID,
   223  	})
   224  	if err != nil {
   225  		return nil, err
   226  	}
   227  
   228  	if len(containers) == 0 {
   229  		return nil, fmt.Errorf("container %q not found", containerID)
   230  	}
   231  	if len(containers) > 1 {
   232  		log.Warnf("CRIClient: multiple containers (%d) with ID %q. Taking the first one: %+v",
   233  			len(containers), containerID, containers)
   234  	}
   235  
   236  	return getPodSandbox(c, containers[0].PodSandboxId)
   237  }
   238  
   239  func (c *CRIClient) Close() error {
   240  	if c.conn != nil {
   241  		return c.conn.Close()
   242  	}
   243  
   244  	return nil
   245  }
   246  
   247  // parseContainerDetailsData parses the container status and extra information
   248  // returned by ContainerStatus() into a ContainerDetailsData structure.
   249  func parseContainerDetailsData(runtimeName types.RuntimeName, containerStatus CRIContainer,
   250  	extraInfo map[string]string, podSandbox *runtime.PodSandbox,
   251  ) (*runtimeclient.ContainerDetailsData, error) {
   252  	containerData := buildContainerData(runtimeName, containerStatus, podSandbox)
   253  
   254  	// Create container details structure to be filled.
   255  	containerDetailsData := &runtimeclient.ContainerDetailsData{
   256  		ContainerData: *containerData,
   257  	}
   258  
   259  	// Parse the extra info and fill the data.
   260  	err := parseExtraInfo(extraInfo, containerDetailsData)
   261  	if err != nil {
   262  		return nil, err
   263  	}
   264  
   265  	return containerDetailsData, nil
   266  }
   267  
   268  // parseExtraInfo parses the extra information returned by ContainerStatus()
   269  // into a ContainerDetailsData structure. It keeps backward compatibility after
   270  // the ContainerInfo format was modified in:
   271  // cri-o v1.18.0: https://github.com/cri-o/cri-o/commit/be8e876cdabec4e055820502fed227aa44971ddc
   272  // containerd v1.6.0-beta.1: https://github.com/containerd/containerd/commit/85b943eb47bc7abe53b9f9e3d953566ed0f65e6c
   273  // NOTE: CRI-O does not have runtime spec prior to 1.18.0
   274  func parseExtraInfo(extraInfo map[string]string,
   275  	containerDetailsData *runtimeclient.ContainerDetailsData,
   276  ) error {
   277  	// Define the info content (only required fields).
   278  	type RuntimeSpecContent struct {
   279  		Mounts []struct {
   280  			Destination string `json:"destination"`
   281  			Source      string `json:"source,omitempty"`
   282  		} `json:"mounts,omitempty"`
   283  		Linux *struct {
   284  			CgroupsPath string `json:"cgroupsPath,omitempty"`
   285  		} `json:"linux,omitempty" platform:"linux"`
   286  	}
   287  	type InfoContent struct {
   288  		Pid         int                `json:"pid"`
   289  		RuntimeSpec RuntimeSpecContent `json:"runtimeSpec"`
   290  	}
   291  
   292  	// Set invalid value to PID.
   293  	pid := -1
   294  	containerDetailsData.Pid = pid
   295  
   296  	// Get the extra info from the map.
   297  	var runtimeSpec *RuntimeSpecContent
   298  	info, ok := extraInfo["info"]
   299  	if ok {
   300  		// Unmarshal the JSON to fields.
   301  		var infoContent InfoContent
   302  		err := json.Unmarshal([]byte(info), &infoContent)
   303  		if err != nil {
   304  			return fmt.Errorf("extracting pid from container status reply: %w", err)
   305  		}
   306  
   307  		// Set the PID value.
   308  		pid = infoContent.Pid
   309  
   310  		// Set the runtime spec pointer, to be copied below.
   311  		runtimeSpec = &infoContent.RuntimeSpec
   312  
   313  		// Legacy parsing.
   314  	} else {
   315  		// Extract the PID.
   316  		pidStr, ok := extraInfo["pid"]
   317  		if !ok {
   318  			return fmt.Errorf("container status reply from runtime doesn't contain pid")
   319  		}
   320  		var err error
   321  		pid, err = strconv.Atoi(pidStr)
   322  		if err != nil {
   323  			return fmt.Errorf("parsing pid %q: %w", pidStr, err)
   324  		}
   325  
   326  		// Extract the runtime spec (may not exist).
   327  		runtimeSpecStr, ok := extraInfo["runtimeSpec"]
   328  		if ok {
   329  			// Unmarshal the JSON to fields.
   330  			runtimeSpec = &RuntimeSpecContent{}
   331  			err := json.Unmarshal([]byte(runtimeSpecStr), runtimeSpec)
   332  			if err != nil {
   333  				return fmt.Errorf("extracting runtime spec from container status reply: %w", err)
   334  			}
   335  		}
   336  	}
   337  
   338  	// Validate extracted fields.
   339  	if pid == 0 {
   340  		return fmt.Errorf("got zero pid")
   341  	}
   342  
   343  	// Set the PID value.
   344  	containerDetailsData.Pid = pid
   345  
   346  	// Copy the runtime spec fields.
   347  	if runtimeSpec != nil {
   348  		if runtimeSpec.Linux != nil {
   349  			containerDetailsData.CgroupsPath = runtimeSpec.Linux.CgroupsPath
   350  		}
   351  		if len(runtimeSpec.Mounts) > 0 {
   352  			containerDetailsData.Mounts = make([]runtimeclient.ContainerMountData, len(runtimeSpec.Mounts))
   353  			for i, specMount := range runtimeSpec.Mounts {
   354  				containerDetailsData.Mounts[i] = runtimeclient.ContainerMountData{
   355  					Destination: specMount.Destination,
   356  					Source:      specMount.Source,
   357  				}
   358  			}
   359  		}
   360  	}
   361  
   362  	return nil
   363  }
   364  
   365  // Convert the state from container status to state of runtime client.
   366  func containerStatusStateToRuntimeClientState(containerStatusState runtime.ContainerState) (runtimeClientState string) {
   367  	switch containerStatusState {
   368  	case runtime.ContainerState_CONTAINER_CREATED:
   369  		runtimeClientState = runtimeclient.StateCreated
   370  	case runtime.ContainerState_CONTAINER_RUNNING:
   371  		runtimeClientState = runtimeclient.StateRunning
   372  	case runtime.ContainerState_CONTAINER_EXITED:
   373  		runtimeClientState = runtimeclient.StateExited
   374  	case runtime.ContainerState_CONTAINER_UNKNOWN:
   375  		runtimeClientState = runtimeclient.StateUnknown
   376  	default:
   377  		runtimeClientState = runtimeclient.StateUnknown
   378  	}
   379  	return
   380  }
   381  
   382  // CRIContainer is an interface that contains the methods required to get
   383  // the information of a container from the responses of the CRI. In particular,
   384  // from runtime.ContainerStatus and runtime.Container.
   385  type CRIContainer interface {
   386  	GetId() string
   387  	GetState() runtime.ContainerState
   388  	GetMetadata() *runtime.ContainerMetadata
   389  	GetLabels() map[string]string
   390  	GetImage() *runtime.ImageSpec
   391  	GetImageRef() string
   392  }
   393  
   394  func digestFromRef(imageRef string) string {
   395  	splitted := strings.Split(imageRef, "@")
   396  	if len(splitted) == 1 {
   397  		return imageRef
   398  	} else {
   399  		return splitted[1]
   400  	}
   401  }
   402  
   403  func getFilteredPodLabels(podSandbox *runtime.PodSandbox) map[string]string {
   404  	labels := map[string]string{}
   405  	for k, v := range podSandbox.GetLabels() {
   406  		if _, ok := podLabelFilter[k]; !ok {
   407  			labels[k] = v
   408  		}
   409  	}
   410  	return labels
   411  }
   412  
   413  func buildContainerData(runtimeName types.RuntimeName, container CRIContainer, podSandbox *runtime.PodSandbox) *runtimeclient.ContainerData {
   414  	containerMetadata := container.GetMetadata()
   415  	image := container.GetImage()
   416  	imageRef := container.GetImageRef()
   417  
   418  	containerData := &runtimeclient.ContainerData{
   419  		Runtime: runtimeclient.RuntimeContainerData{
   420  			BasicRuntimeMetadata: types.BasicRuntimeMetadata{
   421  				ContainerID:          container.GetId(),
   422  				ContainerName:        strings.TrimPrefix(containerMetadata.GetName(), "/"),
   423  				RuntimeName:          runtimeName,
   424  				ContainerImageName:   image.GetImage(),
   425  				ContainerImageDigest: digestFromRef(imageRef),
   426  			},
   427  			State: containerStatusStateToRuntimeClientState(container.GetState()),
   428  		},
   429  	}
   430  
   431  	// Fill K8S information.
   432  	runtimeclient.EnrichWithK8sMetadata(containerData, container.GetLabels())
   433  
   434  	// Initial labels are stored in the pod sandbox
   435  	containerData.K8s.BasicK8sMetadata.PodLabels = getFilteredPodLabels(podSandbox)
   436  
   437  	// CRI-O does not use the same container name of Kubernetes as containerd.
   438  	// Instead, it uses a composed name as Docker does, but such name is not
   439  	// available in the container metadata.
   440  	if runtimeName == types.RuntimeNameCrio {
   441  		containerData.Runtime.ContainerName = fmt.Sprintf("k8s_%s_%s_%s_%s_%d",
   442  			containerData.K8s.ContainerName,
   443  			containerData.K8s.PodName,
   444  			containerData.K8s.Namespace,
   445  			containerData.K8s.PodUID,
   446  			containerMetadata.GetAttempt())
   447  	}
   448  
   449  	return containerData
   450  }