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 }