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 }