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 }