├── .golangci.yaml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── cmd ├── commands │ ├── drift.go │ ├── env.go │ ├── export.go │ ├── info.go │ ├── list.go │ ├── mount.go │ ├── mount_all.go │ └── utils.go └── main.go ├── explorers ├── container.go ├── containerd │ ├── bucket.go │ ├── containerd.go │ ├── content.go │ ├── export.go │ └── snapshot.go ├── content.go ├── docker │ ├── config.go │ ├── docker.go │ └── export.go ├── explorers.go ├── fileinfo.go ├── image.go ├── runtime.go ├── snapshot.go ├── state.go ├── support_containers.go └── task.go ├── go.mod ├── go.sum ├── script └── setup.sh ├── supportcontainer.yaml └── utils ├── container.go └── file.go /.golangci.yaml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | misspell: 3 | locale: US 4 | linters: 5 | disable-all: true 6 | enable: 7 | - staticcheck 8 | - unconvert 9 | - gofmt 10 | - goimports 11 | - gosimple 12 | - govet 13 | - revive 14 | - ineffassign 15 | - vet 16 | - unused 17 | - misspell 18 | - whitespace 19 | - gosec 20 | run: 21 | timeout: 5m 22 | go: '1.18' 23 | skip-dirs: 24 | - script 25 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are just a few small guidelines you need to follow. 4 | 5 | ## Contributor License Agreement 6 | 7 | Contributions to this project must be accompanied by a Contributor License Agreement. You (or your employer) retain the copyright to your contribution; this simply gives us permission to use and redistribute your contributions as part of the project. Head over to https://cla.developers.google.com/ to see your current agreements on file or to sign a new one. 8 | 9 | You generally only need to submit a CLA once, so if you‘ve already submitted one (even if it was for a different project), you probably don’t need to do it again. 10 | 11 | ## Code reviews 12 | 13 | All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult GitHub Help for more information on using pull requests. 14 | 15 | ## Community Guidelines 16 | 17 | This project follows Google's Open Source Community Guidelines. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Container Explorer 2 | 3 | Container Explorer (container-explorer) is a tool to explore containers of a 4 | disk image. Container Explorer supports exploring containers managed using 5 | containerd and docker container runtimes. Container Explorer attempts to 6 | provide the familiar output generated by tools like ctr and docker. 7 | 8 | Container Explorer provides the following functionalities: 9 | 10 | - Exploring namespaces 11 | - Exploring containers 12 | - Exploring images 13 | - Exploring snapshots 14 | - Exploring contents 15 | - Exploring container drift 16 | - Mounting containers 17 | - Support JSON output 18 | 19 | You can build the Container Explorer using the instruction at 20 | [Build Container Explorer](#build-container-explorer). 21 | 22 | If you don't want to build, the binaries are available on 23 | . 24 | 25 | ## Usage 26 | 27 | The figure below shows the output of the container-explorer --help command. 28 | 29 | ```text 30 | NAME: 31 | container-explorer - A standalone utility to explore container details 32 | 33 | USAGE: 34 | main [global options] command [command options] [arguments...] 35 | 36 | VERSION: 37 | 0.4.0 38 | 39 | DESCRIPTION: 40 | A standalone utility to explore container details. 41 | 42 | Container explorer supports exploring containers managed using containerd and 43 | docker. The utility also supports exploring containers created and managed using 44 | Kubernetes. 45 | 46 | 47 | COMMANDS: 48 | list, ls lists container related information 49 | info show internal information 50 | mount mount a container to a mount point 51 | mount-all, mount_all mount all containers 52 | drift, diff identifies container filesystem changes 53 | export export a container 54 | export-all, export_all export all containers as image or archive 55 | help, h Shows a list of commands or help for one command 56 | 57 | GLOBAL OPTIONS: 58 | --debug enable debug messages 59 | --containerd-root value, -c value specify containerd root directory 60 | --image-root value, -i value specify mount point for a disk image 61 | --metadata-file value, -m value specify the path to containerd metadata file i.e. meta.db 62 | --snapshot-metadata-file value, -s value specify the path to containerd snapshot metadata file i.e. metadata.db. 63 | --use-layer-cache attempt to use cached layers where layers are symlinks 64 | --layer-cache value cached layer folder within the snapshot root (default: "layers") 65 | --namespace value, -n value specify container namespace (default: "default") 66 | --docker-managed specify docker manages standalone or Kubernetes containers 67 | --docker-root value specify docker root directory. This is only used with flag --docker-managed 68 | --support-container-data value a yaml file containing information about support containers 69 | --output value output format in json, table. Default is table (default: "table") 70 | --output-file value, -o value output file to save the content 71 | --help, -h show help 72 | --version, -v print the version 73 | ``` 74 | 75 | Container Explorer helps you explore containers on a mounted disk image. Let's 76 | assume we have a clone of the Google Kubernetes Engine (GKE) node attached on a 77 | forensic VM as `/dev/sdb`. 78 | 79 | 1. List the disk partition table. 80 | 81 | ```shell 82 | sudo fdisk -l /dev/sdb 83 | ``` 84 | 85 | The output of the `fdisk` command. 86 | 87 | ```text 88 | Disk /dev/sdb: 10 GiB, 10737418240 bytes, 20971520 sectors 89 | Units: sectors of 1 * 512 = 512 bytes 90 | Sector size (logical/physical): 512 bytes / 512 bytes 91 | I/O size (minimum/optimal): 512 bytes / 512 bytes 92 | Disklabel type: gpt 93 | Disk identifier: 7C818738-EDF0-B246-960D-0E7EE8655B06 94 | 95 | Device Start End Sectors Size Type 96 | /dev/sdb1 8704000 20971486 12267487 5.8G Linux filesystem 97 | /dev/sdb2 20480 53247 32768 16M ChromeOS kernel 98 | /dev/sdb3 4509696 8703999 4194304 2G ChromeOS root fs 99 | /dev/sdb4 53248 86015 32768 16M ChromeOS kernel 100 | /dev/sdb5 315392 4509695 4194304 2G ChromeOS root fs 101 | /dev/sdb6 16448 16448 1 512B ChromeOS kernel 102 | /dev/sdb7 16449 16449 1 512B ChromeOS root fs 103 | /dev/sdb8 86016 118783 32768 16M Linux filesystem 104 | /dev/sdb9 16450 16450 1 512B ChromeOS reserved 105 | /dev/sdb10 16451 16451 1 512B ChromeOS reserved 106 | /dev/sdb11 64 16447 16384 8M BIOS boot 107 | /dev/sdb12 249856 315391 65536 32M EFI System 108 | ``` 109 | 110 | 2. Mount the `/dev/sdb1` as read-only disk on mount point `/mnt/case`. 111 | 112 | ```shell 113 | sudo mount -o ro,noload,noexec /dev/sdb1 /mnt/case 114 | ``` 115 | 116 | 3. Use `container-explorer` to explore the mounted image. 117 | 118 | ```shell 119 | sudo ce -i /mnt/case --support-container-data supportcontainer.yaml list containers 120 | ``` 121 | 122 | 4. Mount an individual container or all containers 123 | 124 | Mount a container to mount point `/mnt/container`. 125 | 126 | ```shell 127 | sudo ce -i /mnt/case –support-container-data supportcontainer.yaml -n k8s.io mount f3c910583a81e7441e2cbd209b72afa4740e676ff8d82f2c74fdc5c78e179c10 /container 128 | ``` 129 | 130 | Mount all containers to mount point `/mnt/container`. Mounting all 131 | containers will create sub-directories using container ID as directory name. 132 | 133 | ```shell 134 | sudo ce -i /mnt/case –support-container-data supportcontainer.yaml mount-all /mnt/container 135 | ``` 136 | 137 | 5. List the mounted containers within `/mnt/container/`. 138 | 139 | ```shell 140 | sudo ls -l /mnt/container 141 | ``` 142 | 143 | The output of the command. 144 | 145 | ```text 146 | drwxr-xr-x 1 root root 4096 Feb 5 08:55 3544209cfda893703458d7d0a6a65970bfb46e9be6a60faa1e4e9d0adae11b55 147 | drwxr-xr-x 1 root root 4096 Feb 5 08:54 3646fe81507be0510e9191d7e34adbeb751e7ecd86f7e1657289968828c5c8e3 148 | drwxr-xr-x 1 root root 4096 Feb 5 08:54 68a04caa81f9a4265e53a83b50874faca5a7c8400ee0c064d40d81cde6f03b86 149 | drwxr-xr-x 1 root root 4096 Feb 5 09:14 6f68aeae9c0288c2412f793d3a7b85efac189786ed8da2bdce9f88d39827fb80 150 | drwxr-xr-x 1 root root 4096 Feb 5 08:55 7227972ec83761790a65c137239c48817a26b8ad85be74b1ecf751656a2a61be 151 | drwxr-xr-x 1 root root 4096 Feb 5 09:13 cc9bc4f6c6b35b8a3616d8b4586741d8dc148c62b394d276dfab7572ee5aa542 152 | drwxr-xr-x 1 root root 4096 Feb 5 09:13 d3d1ff8c4ef39acbdf0a44bee6c326786309e408942d6a2d42cbaa1661bac77f 153 | drwxr-xr-x 1 root root 4096 Feb 5 08:54 f3c910583a81e7441e2cbd209b72afa4740e676ff8d82f2c74fdc5c78e179c10 154 | ``` 155 | 156 | 6. See filesystem changes 157 | 158 | ```shell 159 | sudo ce -i /mnt/case/ --output json --support-container-data supportcontainer.yaml drift 160 | ``` 161 | 162 | In order to see drift of a particular container, supply the container ID with drift/diff 163 | 164 | ```shell 165 | sudo ce -i /mnt/case/ --output json --support-container-data supportcontainer.yaml drift f3c910583a81e7441e2cbd209b72afa4740e676ff8d82f2c74fdc5c78e179c10 166 | ``` 167 | 168 | 6. Use your favorite forensic tool to process mounted containers. 169 | 170 | ## Mounting Disk Image 171 | 172 | Let's assume you have a GKE node disk image as `clone-gke-wp-cluster-default-pool-b4e5d97b-btxm.img`. 173 | 174 | 1. List the partition table. 175 | 176 | ```shell 177 | sudo fdisk -l clone-gke-wp-cluster-default-pool-b4e5d97b-btxm.img 178 | ``` 179 | 180 | The output of the `fdisk -l` command. 181 | 182 | ```text 183 | Disk clone-gke-wp-cluster-default-pool-b4e5d97b-btxm.img: 10 GiB, 10737418240 bytes, 20971520 sectors 184 | Units: sectors of 1 * 512 = 512 bytes 185 | Sector size (logical/physical): 512 bytes / 512 bytes 186 | I/O size (minimum/optimal): 512 bytes / 512 bytes 187 | Disklabel type: gpt 188 | Disk identifier: 7C818738-EDF0-B246-960D-0E7EE8655B06 189 | 190 | Device Start End Sectors Size Type 191 | clone-gke-wp-cluster-default-pool-b4e5d97b-btxm.img1 8704000 20971486 12267487 5.8G Linux filesystem 192 | clone-gke-wp-cluster-default-pool-b4e5d97b-btxm.img2 20480 53247 32768 16M ChromeOS kernel 193 | clone-gke-wp-cluster-default-pool-b4e5d97b-btxm.img3 4509696 8703999 4194304 2G ChromeOS root fs 194 | clone-gke-wp-cluster-default-pool-b4e5d97b-btxm.img4 53248 86015 32768 16M ChromeOS kernel 195 | clone-gke-wp-cluster-default-pool-b4e5d97b-btxm.img5 315392 4509695 4194304 2G ChromeOS root fs 196 | clone-gke-wp-cluster-default-pool-b4e5d97b-btxm.img6 16448 16448 1 512B ChromeOS kernel 197 | clone-gke-wp-cluster-default-pool-b4e5d97b-btxm.img7 16449 16449 1 512B ChromeOS root fs 198 | clone-gke-wp-cluster-default-pool-b4e5d97b-btxm.img8 86016 118783 32768 16M Linux filesystem 199 | clone-gke-wp-cluster-default-pool-b4e5d97b-btxm.img9 16450 16450 1 512B ChromeOS reserved 200 | clone-gke-wp-cluster-default-pool-b4e5d97b-btxm.img10 16451 16451 1 512B ChromeOS reserved 201 | clone-gke-wp-cluster-default-pool-b4e5d97b-btxm.img11 64 16447 16384 8M BIOS boot 202 | clone-gke-wp-cluster-default-pool-b4e5d97b-btxm.img12 249856 315391 65536 32M EFI System 203 | ``` 204 | 205 | 2. Mount the first partition (Linux Filesystem) 206 | 207 | ```shell 208 | sudo mount -o ro,noload,noexec,offset=$((8704000*512)) clone-gke-wp-cluster-default-pool-b4e5d97b-btxm.img /mnt/case 209 | ``` 210 | 211 | ## Docker Containers 212 | 213 | Container Explorer supports exploring Docker managed containers. Use 214 | `--docker-managed` global flag to explore Docker containers. 215 | 216 | ```shell 217 | sudo ce -i /mnt/case --support-container-data supportcontainer.yaml --docker-managed list containers 218 | ``` 219 | 220 | Container Explorer supports the following operation on Docker containers: 221 | 222 | - Listing containers 223 | - Listing images 224 | - Mounting an individual container 225 | - Mounting all containers 226 | - Excluding containers by image, hostname, and labels 227 | 228 | ## Excluding Containers 229 | 230 | When a GKE cluster is created, several containers are created to support the 231 | Kubernetes. These clusters are used to support Kubernetes only and may not be 232 | interesting for the investigation. 233 | 234 | The Kubernetes support containers are hidden by default when the global flag `--support-container-data=supportcontainer.yaml` is used. 235 | 236 | The `supportcontainer.yaml` contains the commonly known hostname, image, and 237 | labels used to identify the support containers. 238 | 239 | When `--support-container-data` is used, the `list` and `mount-all` commands 240 | automatically ignores the known support containers where applicable. You can use 241 | `--show-support-containers` and `--mount-support-containers` to display and 242 | mount the support containers. 243 | 244 | ### Filtering Containers 245 | 246 | Container Explorer supports filtering containers using the labels. This is particularly handy while reviewing GKE containers. Filter supports comma separated key/value pairs. The filter `--filter io.cri-containerd.kind=container` lists containerd containers. 247 | 248 | The command below shows containers in pod namespace `default`. 249 | 250 | ```shell 251 | /opt/container-explorer/bin/ce -i /mnt list containers --filter io.cri-containerd.kind=container,io.kubernetes.pod.namespace=default 252 | ``` 253 | 254 | ## Installing Container Explorer 255 | 256 | Follow the steps below to install a pre-compiled Container Explorer on Linux systems. 257 | 258 | 1. Download setup script `setup.sh` which is located at `https://github.com/google/container-explorer/blob/main/script/setup.sh` 259 | 260 | ```shell 261 | wget https://raw.githubusercontent.com/google/container-explorer/main/script/setup.sh 262 | ``` 263 | 264 | 2. Run the script with `root` privileges. 265 | 266 | ```shell 267 | sudo bash setup.sh install 268 | ``` 269 | 270 | Container Explorer files will be created at `/opt/container-explorer` 271 | 272 | 3. Run Container Explorer 273 | 274 | ```shell 275 | /opt/container-explorer/bin/ce -h 276 | ``` 277 | 278 | **Note**: `supportcontainer.yaml` is located at `/opt/container-explorer/etc/supportcontainer.yaml` 279 | 280 | ## Build Container Explorer 281 | 282 | Follow the steps below to compile the Container Explorer. 283 | 284 | 1. Verify Golang version is 1.20 or above 285 | 286 | ```shell 287 | go version 288 | ``` 289 | 290 | 2. Clone Container Explorer github project 291 | 292 | ```shell 293 | git clone https://github.com/google/container-explorer 294 | ``` 295 | 296 | 3. Compile the code 297 | 298 | ```shell 299 | cd container-explorer 300 | go build -ldflags '-s -w' -o $HOME/ce cmd/main.go 301 | ``` 302 | 303 | 4. Run container-explorer 304 | 305 | ```bash 306 | $HOME/ce -h 307 | ``` 308 | -------------------------------------------------------------------------------- /cmd/commands/drift.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package commands 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "runtime" 23 | "strings" 24 | "text/tabwriter" 25 | 26 | log "github.com/sirupsen/logrus" 27 | "github.com/urfave/cli" 28 | ) 29 | 30 | var DriftCommand = cli.Command{ 31 | Name: "drift", 32 | Aliases: []string{"diff"}, 33 | Usage: "identifies container filesystem changes", 34 | Description: "identifies container filesystem changes for all containers", 35 | ArgsUsage: "[containerID]", 36 | Flags: []cli.Flag{ 37 | cli.StringFlag{ 38 | Name: "filter", 39 | Usage: "comma separated label filter using key=value pair", 40 | }, 41 | cli.BoolFlag{ 42 | Name: "mount-support-containers", 43 | Usage: "mount Kubernetes supporting containers", 44 | }, 45 | }, 46 | Action: func(clictx *cli.Context) error { 47 | // Mounting a container is only supported on a Linux operating system. 48 | if runtime.GOOS != "linux" { 49 | return fmt.Errorf("feature is only supported on Linux") 50 | } 51 | output := clictx.GlobalString("output") 52 | outputfile := clictx.GlobalString("output-file") 53 | filter := clictx.String("filter") 54 | 55 | // Getting container ID positional arg 56 | var containerID string 57 | if clictx.Args().Present() { 58 | containerID = clictx.Args().First() 59 | } 60 | 61 | ctx, exp, cancel, err := explorerEnvironment(clictx) 62 | if err != nil { 63 | return err 64 | } 65 | defer cancel() 66 | 67 | drifts, err := exp.ContainerDrift(ctx, filter, !clictx.Bool("mount-support-containers"), containerID) 68 | if err != nil { 69 | log.WithField("message", err).Error("retrieving container drift") 70 | if output == "json" && outputfile != "" { 71 | data := []string{} 72 | writeOutputFile(data, outputfile) 73 | } 74 | return nil 75 | } 76 | // Handle output formats 77 | if strings.ToLower(output) == "json" { 78 | if outputfile != "" { 79 | writeOutputFile(drifts, outputfile) 80 | } else { 81 | printAsJSON(drifts) 82 | } 83 | return nil 84 | } 85 | 86 | // Default to table output 87 | tw := tabwriter.NewWriter(os.Stdout, 1, 8, 1, '\t', 0) 88 | defer tw.Flush() 89 | 90 | if output == "table" { 91 | // Define the header 92 | fmt.Fprintf(tw, "CONTAINER ID\tADDED/MODIFIED\tDELETED\n") 93 | } 94 | 95 | for _, drift := range drifts { 96 | switch strings.ToLower(output) { 97 | case "json_line": 98 | printAsJSONLine(drift) 99 | default: 100 | // Prepare the data for display 101 | var addedOrModifiedFiles []string 102 | var inaccessibleFiles []string 103 | 104 | for _, fileinfo := range drift.AddedOrModified { 105 | if fileinfo.FileType == "executable" { 106 | addedOrModifiedFiles = append(addedOrModifiedFiles, fileinfo.FullPath+" (executable)") 107 | } else { 108 | addedOrModifiedFiles = append(addedOrModifiedFiles, fileinfo.FullPath) 109 | } 110 | } 111 | 112 | for _, fileinfo := range drift.InaccessibleFiles { 113 | inaccessibleFiles = append(inaccessibleFiles, fileinfo.FullPath) 114 | } 115 | 116 | displayAddedOrModifiedFiles := strings.Join(addedOrModifiedFiles, ", ") 117 | displayInaccessibleFiles := strings.Join(inaccessibleFiles, ", ") 118 | 119 | displayValues := fmt.Sprintf("%s\t%s\t%s", 120 | drift.ContainerID, 121 | displayAddedOrModifiedFiles, 122 | displayInaccessibleFiles, 123 | ) 124 | 125 | fmt.Fprintf(tw, "%v\n", displayValues) 126 | } 127 | } 128 | 129 | // default 130 | return nil 131 | }, 132 | } 133 | -------------------------------------------------------------------------------- /cmd/commands/env.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package commands 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "os" 23 | "path/filepath" 24 | "strings" 25 | 26 | "github.com/containerd/containerd/namespaces" 27 | "github.com/google/container-explorer/explorers" 28 | "github.com/google/container-explorer/explorers/containerd" 29 | "github.com/google/container-explorer/explorers/docker" 30 | "github.com/urfave/cli" 31 | 32 | log "github.com/sirupsen/logrus" 33 | ) 34 | 35 | const ( 36 | containerdRootDir = "/var/lib/containerd" 37 | dockerRootDir = "/var/lib/docker" 38 | ) 39 | 40 | // explorerEnvironment returns a ContainerExplorer interface. 41 | // Containers managed using containerd and docker implement ContainerExplorer 42 | // interface. 43 | func explorerEnvironment(clictx *cli.Context) (context.Context, explorers.ContainerExplorer, func(), error) { 44 | ctx, cancel := context.WithCancel(context.Background()) 45 | 46 | imageroot := clictx.GlobalString("image-root") 47 | containerdroot := clictx.GlobalString("containerd-root") 48 | dockerroot := clictx.GlobalString("docker-root") 49 | metadatafile := clictx.GlobalString("metadata-file") 50 | snapshotfile := clictx.GlobalString("snapshot-metadata-file") 51 | layercache := clictx.GlobalString("layer-cache") 52 | 53 | // Read support container data if provided using global switch. 54 | var sc *explorers.SupportContainer 55 | if clictx.GlobalString("support-container-data") != "" { 56 | var err error 57 | sc, err = explorers.NewSupportContainer(clictx.GlobalString("support-container-data")) 58 | if err != nil { 59 | log.Errorf("getting new support container: %v", err) 60 | } 61 | } 62 | 63 | // Handle docker managed containers. 64 | // 65 | // Use the global flag --docker-managed to specify container 66 | // managed using docker. This includes Kubernetes containers 67 | // managed using docker. 68 | if clictx.GlobalBool("docker-managed") { 69 | if dockerroot == "" && imageroot == "" { 70 | fmt.Printf("Missing required argument. Use --image-root or --docker-root\n") 71 | os.Exit(1) 72 | } 73 | 74 | if imageroot != "" && dockerroot == "" { 75 | dockerroot = filepath.Join( 76 | imageroot, 77 | strings.Replace(dockerRootDir, "/", "", 1), 78 | ) 79 | } 80 | 81 | log.WithFields(log.Fields{ 82 | "imageroot": imageroot, 83 | "containerdroot": containerdroot, 84 | "dockerroot": dockerroot, 85 | "manifestfile": metadatafile, 86 | "snapshotfile": snapshotfile, 87 | "sc": &sc, 88 | }).Debug("docker container environment") 89 | 90 | de, _ := docker.NewExplorer(dockerroot, containerdroot, metadatafile, snapshotfile, sc) 91 | return ctx, de, func() { 92 | cancel() 93 | }, nil 94 | } 95 | 96 | // Handle containerd managed containers. 97 | // 98 | // The default is containerd managed containers. This includes 99 | // Kubernetes managed containers. 100 | if containerdroot == "" && imageroot == "" { 101 | fmt.Printf("Missing required arguments. Use --image-root or --containerd-root\n") 102 | os.Exit(1) 103 | } 104 | 105 | if imageroot != "" && containerdroot == "" { 106 | containerdroot = filepath.Join( 107 | imageroot, 108 | strings.Replace(containerdRootDir, "/", "", 1), 109 | ) 110 | } 111 | 112 | if metadatafile == "" { 113 | metadatafile = filepath.Join(containerdroot, "io.containerd.metadata.v1.bolt", "meta.db") 114 | } 115 | 116 | log.WithFields(log.Fields{ 117 | "imageroot": imageroot, 118 | "containerdroot": containerdroot, 119 | "dockerroot": dockerroot, 120 | "manifestfile": metadatafile, 121 | "snapshotfile": snapshotfile, 122 | }).Debug("containerd container environment") 123 | 124 | if !clictx.GlobalBool("use-layer-cache") { 125 | layercache = "" 126 | } 127 | cde, err := containerd.NewExplorer(imageroot, containerdroot, metadatafile, snapshotfile, layercache, sc) 128 | if err != nil { 129 | return ctx, nil, func() { cancel() }, err 130 | } 131 | return ctx, cde, func() { 132 | cancel() 133 | }, nil 134 | } 135 | 136 | func parseRuntimeConfig(clictx *cli.Context) (context.Context, map[string]interface{}, error) { 137 | // Global options 138 | namespace := clictx.GlobalString("namespace") 139 | imageRootDir := clictx.GlobalString("image-root") 140 | containerdRootDir := clictx.GlobalString("containerd-root") 141 | dockerRootDir := clictx.GlobalString("docker-root") 142 | metadataFile := clictx.GlobalString("metadata-file") 143 | snapshotFile := clictx.GlobalString("snapshot-metadata-file") 144 | layerCache := clictx.GlobalString("layer-cache") 145 | useLayerCache := clictx.GlobalBool("use-layer-cache") 146 | supportDataFile := clictx.GlobalString("support-container-data") 147 | 148 | ctx := context.Background() 149 | ctx = namespaces.WithNamespace(ctx, namespace) 150 | 151 | if imageRootDir == "" && containerdRootDir == "" && dockerRootDir == "" { 152 | return ctx, nil, fmt.Errorf("Missing required arguments. Use --image-root, --containerd-root or --docker-root") 153 | } 154 | 155 | if containerdRootDir == "" && imageRootDir != "" { 156 | containerdRootDir = filepath.Join(imageRootDir, "var", "lib", "containerd") 157 | } 158 | 159 | if dockerRootDir == "" && imageRootDir != "" { 160 | dockerRootDir = filepath.Join(imageRootDir, "var", "lib", "docker") 161 | } 162 | 163 | if metadataFile == "" { 164 | metadataFile = filepath.Join(containerdRootDir, "io.containerd.metadata.v1.bolt", "meta.db") 165 | } 166 | 167 | if !useLayerCache { 168 | layerCache = "" 169 | } 170 | 171 | log.WithFields(log.Fields{ 172 | "imageRootDir": imageRootDir, 173 | "containerdRootDir": containerdRootDir, 174 | "dockerRootDir": dockerRootDir, 175 | "metadataFile": metadataFile, 176 | "snapshotFile": snapshotFile, 177 | "layerCache": layerCache, 178 | "useLayerCache": useLayerCache, 179 | "supportDataFile": supportDataFile, 180 | }).Debug("container-explorer runtime configuration settings") 181 | 182 | runtimeConfig := make(map[string]interface{}) 183 | runtimeConfig["namespace"] = namespace 184 | runtimeConfig["imageRootDir"] = imageRootDir 185 | runtimeConfig["containerdRootDir"] = containerdRootDir 186 | runtimeConfig["dockerRootDir"] = dockerRootDir 187 | runtimeConfig["metadataFile"] = metadataFile 188 | runtimeConfig["snapshotFile"] = snapshotFile 189 | runtimeConfig["layerCache"] = layerCache 190 | 191 | var err error 192 | var sc *explorers.SupportContainer 193 | if supportDataFile != "" { 194 | sc, err = explorers.NewSupportContainer(clictx.GlobalString("support-container-data")) 195 | if err != nil { 196 | log.Errorf("getting new support container: %v", err) 197 | } 198 | } 199 | runtimeConfig["supportContainer"] = sc 200 | 201 | return ctx, runtimeConfig, nil 202 | } -------------------------------------------------------------------------------- /cmd/commands/export.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package commands 18 | 19 | import ( 20 | "fmt" 21 | "runtime" 22 | 23 | "github.com/google/container-explorer/explorers" 24 | "github.com/google/container-explorer/explorers/containerd" 25 | "github.com/google/container-explorer/explorers/docker" 26 | 27 | log "github.com/sirupsen/logrus" 28 | "github.com/urfave/cli" 29 | ) 30 | 31 | var ExportCommand = cli.Command{ 32 | Name: "export", 33 | Usage: "export a container as image or archive", 34 | Description: "export a container as image or archive", 35 | ArgsUsage: "ID OUTPUTDIR", 36 | Flags: []cli.Flag{ 37 | cli.BoolFlag{ 38 | Name: "image", 39 | Usage: "output container as raw image", 40 | }, 41 | cli.BoolFlag{ 42 | Name: "archive", 43 | Usage: "output container as archive", 44 | }, 45 | }, 46 | Action: func(clictx *cli.Context) error { 47 | 48 | // Export a container is only supported on a Linux operating system. 49 | if runtime.GOOS != "linux" { 50 | return fmt.Errorf("exporting a container is only supported on Linux") 51 | } 52 | 53 | if clictx.NArg() < 2 { 54 | return fmt.Errorf("container ID and output directory are required") 55 | } 56 | 57 | containerID := clictx.Args().First() 58 | outputDir := clictx.Args().Get(1) 59 | 60 | exportAsImage := clictx.Bool("image") 61 | exportAsArchive := clictx.Bool("archive") 62 | 63 | // At least one options is required. If not provided by user 64 | // export as image file. 65 | if !exportAsArchive && !exportAsImage { 66 | exportAsImage = true 67 | } 68 | 69 | exportOptions := make(map[string]bool) 70 | exportOptions["image"] = exportAsImage 71 | exportOptions["archive"] = exportAsArchive 72 | 73 | // Process container-explorer runtime arguments 74 | ctx, runtimeConfig, err := parseRuntimeConfig(clictx) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | namespace := runtimeConfig["namespace"].(string) 80 | imageRootDir := runtimeConfig["imageRootDir"].(string) 81 | containerdRootDir := runtimeConfig["containerdRootDir"].(string) 82 | dockerRootDir := runtimeConfig["dockerRootDir"].(string) 83 | metadataFile := runtimeConfig["metadataFile"].(string) 84 | snapshotFile := runtimeConfig["snapshotFile"].(string) 85 | layercache := runtimeConfig["layerCache"].(string) 86 | sc := runtimeConfig["supportContainer"].(*explorers.SupportContainer) 87 | 88 | log.WithFields(log.Fields{ 89 | "namespace": namespace, 90 | "containerID": containerID, 91 | "outputDir": outputDir, 92 | "exportAsImage": exportAsImage, 93 | "exportAsArchive": exportAsArchive, 94 | }).Debug("Processing export request") 95 | 96 | cXplr, err := containerd.NewExplorer(imageRootDir, containerdRootDir, metadataFile, snapshotFile, layercache, sc) 97 | if err == nil { 98 | if err := cXplr.ExportContainer(ctx, containerID, outputDir, exportOptions); err != nil { 99 | log.Errorf("exporting %s as containerd container: %v", containerID, err) 100 | } 101 | } else { 102 | log.Errorf("getting containerd explorer: %v", err) 103 | } 104 | 105 | dXplr, err := docker.NewExplorer(dockerRootDir, containerdRootDir, metadataFile, snapshotFile, sc) 106 | if err == nil { 107 | if err := dXplr.ExportContainer(ctx, containerID, outputDir, exportOptions); err != nil { 108 | log.Errorf("exporting %s as Docker container: %v", containerID, err) 109 | } 110 | } else { 111 | log.Errorf("getting Docker explorer: %v", err) 112 | } 113 | 114 | // default return 115 | return nil 116 | }, 117 | } 118 | 119 | var ExportAllCommand = cli.Command{ 120 | Name: "export-all", 121 | Aliases: []string{"export_all"}, 122 | Usage: "export all containers as image or archive", 123 | Description: "export all containers as image or archive", 124 | ArgsUsage: "OUTPUTDIR", 125 | Flags: []cli.Flag{ 126 | cli.BoolFlag{ 127 | Name: "image", 128 | Usage: "output container as raw image", 129 | }, 130 | cli.BoolFlag{ 131 | Name: "archive", 132 | Usage: "output container as archive", 133 | }, 134 | cli.StringFlag{ 135 | Name: "filter", 136 | Usage: "comma separated label filter using key=value", 137 | }, 138 | cli.BoolFlag{ 139 | Name: "export-support-containers", 140 | Usage: "export Kubernetes supporting containers", 141 | }, 142 | }, 143 | Action: func(clictx *cli.Context) error { 144 | // Exporting containers only supported on a Linux operating system. 145 | if runtime.GOOS != "linux" { 146 | return fmt.Errorf("exporting containers is only supported on Linux") 147 | } 148 | 149 | if clictx.NArg() < 1 { 150 | return fmt.Errorf("output directory is required") 151 | } 152 | outputDir := clictx.Args().First() 153 | 154 | exportAsImage := clictx.Bool("image") 155 | exportAsArchive := clictx.Bool("archive") 156 | 157 | // At least one options is required. If not provided by user 158 | // export as image file. 159 | if !exportAsArchive && !exportAsImage { 160 | exportAsImage = true 161 | } 162 | 163 | exportOptions := make(map[string]bool) 164 | exportOptions["image"] = exportAsImage 165 | exportOptions["archive"] = exportAsArchive 166 | 167 | filterString := clictx.String("filter") 168 | filterMap := getFilterMap(filterString) 169 | 170 | exportSupportContainers := clictx.Bool("export-support-containers") 171 | 172 | // Process container-explorer runtime arguments 173 | ctx, runtimeConfig, err := parseRuntimeConfig(clictx) 174 | if err != nil { 175 | return err 176 | } 177 | 178 | namespace := runtimeConfig["namespace"].(string) 179 | imageRootDir := runtimeConfig["imageRootDir"].(string) 180 | containerdRootDir := runtimeConfig["containerdRootDir"].(string) 181 | dockerRootDir := runtimeConfig["dockerRootDir"].(string) 182 | metadataFile := runtimeConfig["metadataFile"].(string) 183 | snapshotFile := runtimeConfig["snapshotFile"].(string) 184 | layercache := runtimeConfig["layerCache"].(string) 185 | sc := runtimeConfig["supportContainer"].(*explorers.SupportContainer) 186 | 187 | log.WithFields(log.Fields{ 188 | "namespace": namespace, 189 | "outputDir": outputDir, 190 | "exportAsImage": exportAsImage, 191 | "exportAsArchive": exportAsArchive, 192 | }).Debug("Processing export-all request") 193 | 194 | // Exporting all containerd containers 195 | cXplr, err := containerd.NewExplorer(imageRootDir, containerdRootDir, metadataFile, snapshotFile, layercache, sc) 196 | if err == nil { 197 | if err := cXplr.ExportAllContainers(ctx, outputDir, exportOptions, filterMap, exportSupportContainers); err != nil { 198 | log.Errorf("exporting all containerd containers as image or archive: %v", err) 199 | } 200 | } else { 201 | log.Errorf("getting containerd explorer: %v", err) 202 | } 203 | 204 | // Exporting all Docker containers 205 | dXplr, err := docker.NewExplorer(dockerRootDir, containerdRootDir, metadataFile, snapshotFile, sc) 206 | if err == nil { 207 | if err := dXplr.ExportAllContainers(ctx, outputDir, exportOptions, filterMap, exportSupportContainers); err != nil { 208 | log.Errorf("exporting all Docker containers as image or archive: %v", err) 209 | } 210 | } else { 211 | log.Errorf("getting Docker explorer: %v", err) 212 | } 213 | 214 | return nil 215 | }, 216 | } -------------------------------------------------------------------------------- /cmd/commands/info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package commands 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | 23 | log "github.com/sirupsen/logrus" 24 | 25 | "github.com/containerd/containerd/namespaces" 26 | "github.com/urfave/cli" 27 | ) 28 | 29 | var InfoCommand = cli.Command{ 30 | Name: "info", 31 | Usage: "show internal information", 32 | Description: "show internal information", 33 | Subcommands: cli.Commands{ 34 | infoContainer, 35 | }, 36 | } 37 | 38 | var infoContainer = cli.Command{ 39 | Name: "container", 40 | Usage: "show container internal information", 41 | Description: "show container internal information", 42 | Flags: []cli.Flag{ 43 | cli.BoolFlag{ 44 | Name: "spec", 45 | Usage: "show only container spec", 46 | }, 47 | }, 48 | Action: func(clictx *cli.Context) error { 49 | 50 | if clictx.NArg() < 1 { 51 | return fmt.Errorf("container id is required") 52 | } 53 | 54 | var ( 55 | namespace string 56 | containerid string 57 | ) 58 | 59 | namespace = clictx.GlobalString("namespace") 60 | containerid = clictx.Args().First() 61 | 62 | ctx, exp, cancel, err := explorerEnvironment(clictx) 63 | if err != nil { 64 | log.Fatal(err) 65 | } 66 | defer cancel() 67 | 68 | ctx = namespaces.WithNamespace(ctx, namespace) 69 | 70 | info, err := exp.InfoContainer(ctx, containerid, clictx.Bool("spec")) 71 | if err != nil { 72 | log.Fatal(err) 73 | } 74 | 75 | printAsJSON(info) 76 | 77 | return nil 78 | }, 79 | } 80 | 81 | func printAsJSON(v interface{}) { 82 | b, err := json.MarshalIndent(v, "", " ") 83 | if err != nil { 84 | log.Error("error marshaling to JSON", err) 85 | return 86 | } 87 | 88 | fmt.Println(string(b)) 89 | } 90 | 91 | func printAsJSONLine(v interface{}) { 92 | b, err := json.Marshal(v) 93 | if err != nil { 94 | log.Error("error marshaling to json_line", err) 95 | return 96 | } 97 | fmt.Println(string(b)) 98 | } 99 | -------------------------------------------------------------------------------- /cmd/commands/list.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package commands 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "io/ioutil" 23 | "os" 24 | "path/filepath" 25 | "strings" 26 | "text/tabwriter" 27 | 28 | log "github.com/sirupsen/logrus" 29 | 30 | "github.com/urfave/cli" 31 | ) 32 | 33 | const tsLayout = "2006-01-02T15:04:05Z" 34 | 35 | var ListCommand = cli.Command{ 36 | Name: "list", 37 | Aliases: []string{"ls"}, 38 | Usage: "lists container related information", 39 | Subcommands: cli.Commands{ 40 | listNamespaces, 41 | listContainers, 42 | listContent, 43 | listImages, 44 | listSnapshots, 45 | listTasks, 46 | }, 47 | } 48 | 49 | var listNamespaces = cli.Command{ 50 | Name: "namespaces", 51 | Aliases: []string{"namespace", "ns"}, 52 | Usage: "list all namespaces", 53 | Description: "list all namespaces", 54 | Action: func(clictx *cli.Context) error { 55 | 56 | ctx, exp, cancel, err := explorerEnvironment(clictx) 57 | if err != nil { 58 | log.Fatal(err) 59 | } 60 | defer cancel() 61 | 62 | nss, err := exp.ListNamespaces(ctx) 63 | if err != nil { 64 | log.Fatal(err) 65 | } 66 | 67 | fmt.Println("NAMESPACE") 68 | for _, ns := range nss { 69 | fmt.Println(ns) 70 | } 71 | 72 | return nil 73 | }, 74 | } 75 | 76 | var listContainers = cli.Command{ 77 | Name: "containers", 78 | Aliases: []string{"container"}, 79 | Usage: "list containers for all namespaces", 80 | Description: "list containers for all namespaces", 81 | Flags: []cli.Flag{ 82 | cli.StringFlag{ 83 | Name: "filter", 84 | Usage: "comma separated label filter using key=value pair", 85 | }, 86 | cli.BoolFlag{ 87 | Name: "show-support-containers", 88 | Usage: "show supporting containers created by Kubernetes", 89 | }, 90 | cli.BoolFlag{ 91 | Name: "no-labels", 92 | Usage: "hide container labels", 93 | }, 94 | cli.BoolFlag{ 95 | Name: "updated", 96 | Usage: "show updated timestamp", 97 | }, 98 | cli.BoolFlag{ 99 | Name: "ports", 100 | Usage: "show exposed ports", 101 | }, 102 | cli.BoolFlag{ 103 | Name: "running", 104 | Usage: "show running docker managed containers", 105 | }, 106 | }, 107 | Action: func(clictx *cli.Context) error { 108 | output := clictx.GlobalString("output") 109 | outputfile := clictx.GlobalString("output-file") 110 | filters := clictx.String("filter") 111 | 112 | ctx, exp, cancel, err := explorerEnvironment(clictx) 113 | if err != nil { 114 | log.WithField("message", err).Error("setting environment") 115 | if output == "json" && outputfile != "" { 116 | data := []string{} 117 | writeOutputFile(data, outputfile) 118 | } 119 | return nil 120 | } 121 | defer cancel() 122 | 123 | containers, err := exp.ListContainers(ctx) 124 | if err != nil { 125 | log.WithField("message", err).Error("listing containers") 126 | if output == "json" && outputfile != "" { 127 | data := []string{} 128 | writeOutputFile(data, outputfile) 129 | } 130 | return nil 131 | } 132 | 133 | // Filter containers 134 | filteredContainers := containers[:0] 135 | if filters != "" { 136 | labelFilters := strings.Split(filters, ",") 137 | 138 | for _, container := range containers { 139 | include := true 140 | 141 | for _, f := range labelFilters { 142 | if !strings.Contains(f, "=") { 143 | continue 144 | } 145 | key := strings.Split(f, "=")[0] 146 | value := strings.Split(f, "=")[1] 147 | labelValue, ok := container.Labels[key] 148 | if !ok { 149 | include = false 150 | break 151 | } 152 | 153 | if labelValue != value { 154 | include = false 155 | break 156 | } 157 | } 158 | if include { 159 | filteredContainers = append(filteredContainers, container) 160 | } 161 | } 162 | containers = filteredContainers 163 | } 164 | 165 | if strings.ToLower(output) == "json" { 166 | if outputfile != "" { 167 | writeOutputFile(containers, outputfile) 168 | } else { 169 | printAsJSON(containers) 170 | } 171 | return nil 172 | } 173 | 174 | tw := tabwriter.NewWriter(os.Stdout, 1, 8, 1, '\t', 0) 175 | defer tw.Flush() 176 | 177 | if output == "table" { 178 | displayFields := "NAMESPACE\tTYPE\tCONTAINER ID\tCONTAINER HOSTNAME\tIMAGE\tCREATED AT\tPID\tSTATUS" 179 | // show updated timestamp 180 | if clictx.Bool("updated") { 181 | displayFields = fmt.Sprintf("%v\tUPDATED AT", displayFields) 182 | } 183 | // show exposed ports 184 | if clictx.Bool("ports") { 185 | displayFields = fmt.Sprintf("%v\tEXPOSED PORTS", displayFields) 186 | } 187 | // display docker container name 188 | if clictx.GlobalBool("docker-managed") { 189 | displayFields = fmt.Sprintf("%v\tNAME", displayFields) 190 | } 191 | // show labels 192 | if !clictx.Bool("no-labels") { 193 | displayFields = fmt.Sprintf("%v\tLABELS", displayFields) 194 | } 195 | fmt.Fprintf(tw, "%v\n", displayFields) 196 | } 197 | 198 | for _, container := range containers { 199 | // Show Kubernetes support containers created 200 | // by GKE, EKS, and AKS 201 | if !clictx.Bool("show-support-containers") && container.SupportContainer { 202 | log.WithFields(log.Fields{ 203 | "namespace": container.Namespace, 204 | "containerid": container.ID, 205 | "supportcontainer": container.SupportContainer, 206 | }).Info("skip support container") 207 | 208 | continue 209 | } 210 | 211 | // Show only running containers. 212 | // 213 | // This is currently supported only on a docker managed containers. 214 | if clictx.GlobalBool("docker-managed") && clictx.Bool("running") { 215 | if !container.Running { 216 | log.WithFields(log.Fields{ 217 | "containerid": container.ID, 218 | "image": container.Image, 219 | }).Info("skip container that was not running") 220 | 221 | continue 222 | } 223 | } 224 | 225 | switch strings.ToLower(output) { 226 | case "json_line": 227 | printAsJSONLine(container) 228 | default: 229 | displayValues := fmt.Sprintf("%s\t%s\t%s\t%s\t%s\t%s\t%d\t%s", 230 | container.Namespace, 231 | container.ContainerType, 232 | container.ID, 233 | container.Hostname, 234 | container.Image, 235 | container.CreatedAt.Format(tsLayout), 236 | container.ProcessID, 237 | container.Status, 238 | ) 239 | // show updated timestamp value 240 | if clictx.Bool("updated") { 241 | displayValues = fmt.Sprintf("%v\t%s", displayValues, container.UpdatedAt.Format(tsLayout)) 242 | } 243 | // show exposed ports value 244 | if clictx.Bool("ports") { 245 | displayValues = fmt.Sprintf("%v\t%s", displayValues, arrayToString(container.ExposedPorts)) 246 | } 247 | // show docker container name 248 | if clictx.GlobalBool("docker-managed") { 249 | displayValues = fmt.Sprintf("%v\t%s", displayValues, strings.Replace(container.Runtime.Name, "/", "", 1)) 250 | } 251 | // show labels values 252 | if !clictx.Bool("no-labels") { 253 | displayValues = fmt.Sprintf("%v\t%v", displayValues, labelString(container.Labels)) 254 | } 255 | fmt.Fprintf(tw, "%v\n", displayValues) 256 | } 257 | 258 | } 259 | 260 | return nil 261 | }, 262 | } 263 | 264 | var listImages = cli.Command{ 265 | Name: "images", 266 | Aliases: []string{"image"}, 267 | Usage: "list images for all namespaces", 268 | Description: "list images for all namespaces", 269 | Flags: []cli.Flag{ 270 | cli.BoolFlag{ 271 | Name: "show-support-containers", 272 | Usage: "show Kubernetes support container images", 273 | }, 274 | cli.BoolFlag{ 275 | Name: "updated", 276 | Usage: "show updated timestamp", 277 | }, 278 | cli.BoolFlag{ 279 | Name: "no-labels", 280 | Usage: "hide image labels", 281 | }, 282 | }, 283 | Action: func(clictx *cli.Context) error { 284 | output := clictx.GlobalString("output") 285 | outputfile := clictx.GlobalString("output-file") 286 | 287 | ctx, exp, cancel, err := explorerEnvironment(clictx) 288 | if err != nil { 289 | log.WithField("message", err).Error("setting environment") 290 | if output == "json" && outputfile != "" { 291 | data := []string{} 292 | writeOutputFile(data, outputfile) 293 | } 294 | return nil 295 | } 296 | defer cancel() 297 | 298 | images, err := exp.ListImages(ctx) 299 | if err != nil { 300 | log.WithField("message", err).Error("listing images") 301 | if output == "json" && outputfile != "" { 302 | data := []string{} 303 | writeOutputFile(data, outputfile) 304 | } 305 | return nil 306 | } 307 | 308 | if strings.ToLower(output) == "json" { 309 | if outputfile != "" { 310 | writeOutputFile(images, outputfile) 311 | } else { 312 | printAsJSON(images) 313 | } 314 | return nil 315 | } 316 | 317 | tw := tabwriter.NewWriter(os.Stdout, 1, 8, 1, '\t', 0) 318 | defer tw.Flush() 319 | 320 | // Setting table output 321 | if strings.ToLower(output) == "table" { 322 | displayFields := "NAMESPACE\tNAME\tCREATED AT\tDIGEST\tTYPE" 323 | if clictx.Bool("updated") { 324 | displayFields = fmt.Sprintf("%v\tUPDATED AT", displayFields) 325 | } 326 | if !clictx.Bool("no-labels") { 327 | displayFields = fmt.Sprintf("%v\tLABELS", displayFields) 328 | } 329 | 330 | fmt.Fprintf(tw, "%v\n", displayFields) 331 | } 332 | 333 | for _, image := range images { 334 | if !clictx.Bool("show-support-containers") && image.SupportContainerImage { 335 | log.WithFields(log.Fields{ 336 | "namespace": image.Namespace, 337 | "image": image.Name, 338 | }).Debug("skipping Kubernetes support container image") 339 | continue 340 | } 341 | 342 | switch strings.ToLower(output) { 343 | case "json_line": 344 | printAsJSONLine(image) 345 | default: 346 | displayValues := fmt.Sprintf("%s\t%s\t%s\t%s\t%s", 347 | image.Namespace, 348 | image.Name, 349 | image.CreatedAt.Format(tsLayout), 350 | string(image.Target.Digest), 351 | image.Target.MediaType, 352 | ) 353 | if clictx.Bool("updated") { 354 | displayValues = fmt.Sprintf("%v\t%s", displayValues, image.UpdatedAt.Format(tsLayout)) 355 | } 356 | if !clictx.Bool("no-labels") { 357 | displayValues = fmt.Sprintf("%v\t%s", displayValues, labelString(image.Labels)) 358 | } 359 | fmt.Fprintf(tw, "%v\n", displayValues) 360 | } 361 | } 362 | return nil 363 | }, 364 | } 365 | 366 | var listContent = cli.Command{ 367 | Name: "content", 368 | Aliases: []string{"content"}, 369 | Usage: "list content for all namespaces", 370 | Description: "list content for all namespaces", 371 | Action: func(clictx *cli.Context) error { 372 | output := clictx.GlobalString("output") 373 | outputfile := clictx.GlobalString("outputfile") 374 | 375 | ctx, exp, cancel, err := explorerEnvironment(clictx) 376 | if err != nil { 377 | log.WithField("message", err).Error("setting environment") 378 | if output == "json" && outputfile != "" { 379 | data := []string{} 380 | writeOutputFile(data, outputfile) 381 | } 382 | return nil 383 | } 384 | defer cancel() 385 | 386 | content, err := exp.ListContent(ctx) 387 | if err != nil { 388 | log.WithField("message", err).Error("listing content") 389 | if output == "json" && outputfile != "" { 390 | data := []string{} 391 | writeOutputFile(data, outputfile) 392 | } 393 | return nil 394 | } 395 | 396 | if strings.ToLower(output) == "json" { 397 | if outputfile != "" { 398 | writeOutputFile(content, outputfile) 399 | } else { 400 | printAsJSON(content) 401 | } 402 | return nil 403 | } 404 | 405 | tw := tabwriter.NewWriter(os.Stdout, 1, 8, 1, '\t', 0) 406 | defer tw.Flush() 407 | 408 | if strings.ToLower(output) == "table" { 409 | fmt.Fprintf(tw, "NAMESPACE\tDIGEST\tSIZE\tCREATED AT\tUPDATED AT\tLABELS\n") 410 | } 411 | 412 | for _, c := range content { 413 | switch strings.ToLower(output) { 414 | case "json_line": 415 | printAsJSONLine(c) 416 | default: 417 | fmt.Fprintf(tw, "%s\t%s\t%v\t%v\t%v\t%s\n", 418 | c.Namespace, 419 | c.Digest, 420 | c.Size, 421 | c.CreatedAt.Format(tsLayout), 422 | c.UpdatedAt.Format(tsLayout), 423 | labelString(c.Labels), 424 | ) 425 | } 426 | } 427 | 428 | return nil 429 | }, 430 | } 431 | 432 | var listSnapshots = cli.Command{ 433 | Name: "snapshots", 434 | Aliases: []string{"snapshot"}, 435 | Usage: "list snapshots for all namespaces", 436 | Description: "list snapshots for all namespaces", 437 | Flags: []cli.Flag{ 438 | cli.BoolFlag{ 439 | Name: "no-labels", 440 | Usage: "hide snapshot labels", 441 | }, 442 | cli.BoolFlag{ 443 | Name: "full-overlay-path", 444 | Usage: "show overlay full path", 445 | }, 446 | }, 447 | Action: func(clictx *cli.Context) error { 448 | output := clictx.GlobalString("output") 449 | outputfile := clictx.GlobalString("outputfile") 450 | 451 | ctx, exp, cancel, err := explorerEnvironment(clictx) 452 | if err != nil { 453 | //log.Fatal(err) 454 | log.WithField("message", err).Error("setting environment") 455 | if output == "json" && outputfile != "" { 456 | data := []string{} 457 | writeOutputFile(data, outputfile) 458 | } 459 | return nil 460 | } 461 | defer cancel() 462 | 463 | ss, err := exp.ListSnapshots(ctx) 464 | if err != nil { 465 | //log.Fatal(err) 466 | log.WithField("message", err).Error("listing snapshot") 467 | if output == "json" && outputfile != "" { 468 | data := []string{} 469 | writeOutputFile(data, outputfile) 470 | } 471 | return nil 472 | } 473 | 474 | if strings.ToLower(output) == "json" { 475 | if outputfile != "" { 476 | writeOutputFile(ss, outputfile) 477 | } else { 478 | printAsJSON(ss) 479 | } 480 | return nil 481 | } 482 | 483 | tw := tabwriter.NewWriter(os.Stdout, 1, 8, 1, '\t', 0) 484 | defer tw.Flush() 485 | 486 | // Setting table output header 487 | if strings.ToLower(output) == "table" { 488 | displayFields := "NAMESPACE\tSNAPSHOTTER\tCREATED AT\tUPDATED AT\tKIND\tNAME\tPARENT\tLAYER PATH" 489 | if !clictx.Bool("no-labels") { 490 | displayFields = fmt.Sprintf("%s\tLABELS", displayFields) 491 | } 492 | fmt.Fprintf(tw, "%v\n", displayFields) 493 | } 494 | 495 | for _, s := range ss { 496 | ssfilepath := filepath.Join(exp.SnapshotRoot(s.Snapshotter), s.OverlayPath) 497 | 498 | switch strings.ToLower(output) { 499 | case "json_line": 500 | s.OverlayPath = ssfilepath 501 | printAsJSONLine(s) 502 | default: 503 | if clictx.Bool("full-overlay-path") { 504 | s.OverlayPath = ssfilepath 505 | } 506 | 507 | displayValue := fmt.Sprintf("%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v", 508 | s.Namespace, 509 | s.Snapshotter, 510 | s.CreatedAt.Format(tsLayout), 511 | s.UpdatedAt.Format(tsLayout), 512 | s.Kind, 513 | s.Key, 514 | s.Parent, 515 | s.OverlayPath, 516 | ) 517 | 518 | if !clictx.Bool("no-labels") { 519 | displayValue = fmt.Sprintf("%v\t%v", displayValue, labelString(s.Labels)) 520 | } 521 | fmt.Fprintf(tw, "%v\n", displayValue) 522 | } 523 | } 524 | 525 | return nil 526 | }, 527 | } 528 | 529 | var listTasks = cli.Command{ 530 | Name: "tasks", 531 | Aliases: []string{"task"}, 532 | Usage: "list tasks", 533 | Description: "list container tasks", 534 | Action: func(clictx *cli.Context) error { 535 | output := clictx.GlobalString("output") 536 | outputfile := clictx.GlobalString("outputfile") 537 | 538 | ctx, exp, cancel, err := explorerEnvironment(clictx) 539 | if err != nil { 540 | log.WithField("message", err).Error("setting environment") 541 | if outputfile != "" { 542 | data := []string{} 543 | writeOutputFile(data, outputfile) 544 | } 545 | return nil 546 | } 547 | defer cancel() 548 | 549 | tasks, err := exp.ListTasks(ctx) 550 | if err != nil { 551 | log.WithField("message", err).Error("listing task") 552 | if outputfile != "" { 553 | data := []string{} 554 | writeOutputFile(data, outputfile) 555 | } 556 | return nil 557 | } 558 | 559 | if strings.ToLower(output) == "json" { 560 | printAsJSON(tasks) 561 | return nil 562 | } 563 | 564 | tw := tabwriter.NewWriter(os.Stdout, 1, 8, 1, '\t', 0) 565 | defer tw.Flush() 566 | 567 | displayFields := "NAMESPACE\tCONTAINER ID\tCONTAINER TYPE\tPID\tSTATUS" 568 | fmt.Fprintf(tw, "%v\n", displayFields) 569 | 570 | for _, t := range tasks { 571 | switch strings.ToLower(output) { 572 | case "json_line": 573 | printAsJSONLine(t) 574 | default: 575 | displayValues := fmt.Sprintf("%v\t%v\t%v\t%v\t%v", 576 | t.Namespace, 577 | t.Name, 578 | t.ContainerType, 579 | t.PID, 580 | t.Status, 581 | ) 582 | fmt.Fprintf(tw, "%v\n", displayValues) 583 | } 584 | } 585 | return nil 586 | }, 587 | } 588 | 589 | // labelString retruns a string of comma separated key-value pairs. 590 | func labelString(labels map[string]string) string { 591 | var lablestrings []string 592 | 593 | for k, v := range labels { 594 | lablestrings = append(lablestrings, strings.Join([]string{k, v}, "=")) 595 | } 596 | return strings.Join(lablestrings, ",") 597 | } 598 | 599 | // arrayToString returns a string of comma separated value of an array. 600 | func arrayToString(array []string) string { 601 | var result string 602 | 603 | for i, val := range array { 604 | if i == 0 { 605 | result = val 606 | continue 607 | } 608 | result = fmt.Sprintf("%s,%s", result, val) 609 | } 610 | 611 | return result 612 | } 613 | 614 | // writeOutputFile writes JSON data to specified file. 615 | func writeOutputFile(v interface{}, outputfile string) { 616 | data, _ := json.Marshal(v) 617 | ioutil.WriteFile(outputfile, data, 0644) 618 | } 619 | -------------------------------------------------------------------------------- /cmd/commands/mount.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package commands 18 | 19 | import ( 20 | "fmt" 21 | "runtime" 22 | 23 | "github.com/containerd/containerd/namespaces" 24 | log "github.com/sirupsen/logrus" 25 | "github.com/urfave/cli" 26 | ) 27 | 28 | var MountCommand = cli.Command{ 29 | Name: "mount", 30 | Usage: "mount a container to a mount point", 31 | Description: "mount a container to a mount point", 32 | ArgsUsage: "ID MOUNTPOINT", 33 | Action: func(clictx *cli.Context) error { 34 | 35 | // Mounting a container is only supported on a Linux operating system. 36 | if runtime.GOOS != "linux" { 37 | return fmt.Errorf("mounting a container is only supported on Linux") 38 | } 39 | 40 | if clictx.NArg() < 2 { 41 | return fmt.Errorf("container id and mount point are required") 42 | } 43 | 44 | namespace := clictx.GlobalString("namespace") 45 | containerid := clictx.Args().First() 46 | mountpoint := clictx.Args().Get(1) 47 | 48 | log.WithFields(log.Fields{ 49 | "namespace": namespace, 50 | "containerid": containerid, 51 | "mountpoint": mountpoint, 52 | }).Debug("user provided mount options") 53 | 54 | ctx, exp, cancel, err := explorerEnvironment(clictx) 55 | if err != nil { 56 | return err 57 | } 58 | defer cancel() 59 | 60 | ctx = namespaces.WithNamespace(ctx, namespace) 61 | 62 | if err := exp.MountContainer(ctx, containerid, mountpoint); err != nil { 63 | return err 64 | } 65 | 66 | // default return 67 | return nil 68 | }, 69 | } 70 | -------------------------------------------------------------------------------- /cmd/commands/mount_all.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package commands 18 | 19 | import ( 20 | "fmt" 21 | "runtime" 22 | 23 | "github.com/urfave/cli" 24 | ) 25 | 26 | var MountAllCommand = cli.Command{ 27 | Name: "mount-all", 28 | Aliases: []string{"mount_all"}, 29 | Usage: "mount all containers", 30 | Description: "mount all containers to subdirectories with the specified mount point", 31 | ArgsUsage: "[flag] MOUNT_POINT", 32 | Flags: []cli.Flag{ 33 | cli.StringFlag{ 34 | Name: "filter", 35 | Usage: "comma separated label filter using key=value pair", 36 | }, 37 | cli.BoolFlag{ 38 | Name: "mount-support-containers", 39 | Usage: "mount Kubernetes supporting containers", 40 | }, 41 | }, 42 | Action: func(clictx *cli.Context) error { 43 | // Mounting a container is only supported on a Linux operating system. 44 | if runtime.GOOS != "linux" { 45 | return fmt.Errorf("mounting a container is only supported on Linux") 46 | } 47 | 48 | if clictx.NArg() < 1 { 49 | return fmt.Errorf("mount point is required") 50 | } 51 | 52 | mountpoint := clictx.Args().First() 53 | filter := clictx.String("filter") 54 | 55 | ctx, exp, cancel, err := explorerEnvironment(clictx) 56 | if err != nil { 57 | return err 58 | } 59 | defer cancel() 60 | 61 | if err := exp.MountAllContainers(ctx, mountpoint, filter, !clictx.Bool("mount-support-containers")); err != nil { 62 | return err 63 | } 64 | // default 65 | return nil 66 | }, 67 | } 68 | -------------------------------------------------------------------------------- /cmd/commands/utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package commands 18 | 19 | import "strings" 20 | 21 | func getFilterMap(filter string) map[string]string { 22 | if filter == "" { 23 | return nil 24 | } 25 | 26 | filterMap := make(map[string]string) 27 | filters := strings.Split(filter, ",") 28 | for _, filter := range filters { 29 | parts := strings.Split(filter, "=") 30 | if len(parts) == 2 { 31 | filterMap[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) 32 | } 33 | } 34 | return filterMap 35 | } -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "os" 21 | 22 | cecommands "github.com/google/container-explorer/cmd/commands" 23 | log "github.com/sirupsen/logrus" 24 | "github.com/urfave/cli" 25 | ) 26 | 27 | const ( 28 | VERSION = "0.4.0" 29 | ) 30 | 31 | func init() { 32 | log.SetFormatter(&log.TextFormatter{}) 33 | log.SetOutput(os.Stdout) 34 | log.SetLevel(log.WarnLevel) 35 | } 36 | 37 | func main() { 38 | app := cli.NewApp() 39 | 40 | app.Name = "container-explorer" 41 | app.Version = VERSION 42 | app.Usage = "A standalone utility to explore container details" 43 | app.Description = `A standalone utility to explore container details. 44 | 45 | Container explorer supports exploring containers managed using containerd and 46 | docker. The utility also supports exploring containers created and managed using 47 | Kubernetes. 48 | ` 49 | app.Flags = []cli.Flag{ 50 | cli.BoolFlag{ 51 | Name: "debug", 52 | Usage: "enable debug messages", 53 | }, 54 | 55 | // Removing the default containerd-root value 56 | // 57 | // A bug was discovered when analyzing docker managed containers and 58 | // the `containerd-root` default value was set. 59 | // 60 | // The bug occurs only when the analysis host had containerd running 61 | // and docker-root path is specified rather than image path. 62 | // 63 | // i.e container-explorer --docker-managed --docker-root 64 | // 65 | // Since the default containerd-root is set to /var/lib/containerd, 66 | // container-explorer attempts access manifest in /var/lib/containerd. 67 | // This leads to inaccurate information or issue accessing locked file. 68 | // 69 | // Workaround: Remove the default values in flag and specify in env.go 70 | // as required. 71 | cli.StringFlag{ 72 | Name: "containerd-root, c", 73 | Usage: "specify containerd root directory", 74 | }, 75 | cli.StringFlag{ 76 | Name: "image-root, i", 77 | Usage: "specify mount point for a disk image", 78 | }, 79 | cli.StringFlag{ 80 | Name: "metadata-file, m", 81 | Usage: "specify the path to containerd metadata file i.e. meta.db", 82 | }, 83 | cli.StringFlag{ 84 | Name: "snapshot-metadata-file, s", 85 | Usage: "specify the path to containerd snapshot metadata file i.e. metadata.db.", 86 | }, 87 | cli.BoolFlag{ 88 | Name: "use-layer-cache", 89 | Usage: "attempt to use cached layers where layers are symlinks", 90 | }, 91 | cli.StringFlag{ 92 | Name: "layer-cache", 93 | Usage: "cached layer folder within the snapshot root", 94 | Value: "layers", 95 | }, 96 | cli.StringFlag{ 97 | Name: "namespace, n", 98 | Usage: "specify container namespace", 99 | Value: "default", 100 | }, 101 | cli.BoolFlag{ 102 | Name: "docker-managed", 103 | Usage: "specify docker manages standalone or Kubernetes containers", 104 | }, 105 | cli.StringFlag{ 106 | Name: "docker-root", 107 | Usage: "specify docker root directory. This is only used with flag --docker-managed", 108 | }, 109 | cli.StringFlag{ 110 | Name: "support-container-data", 111 | Usage: "a yaml file containing information about support containers", 112 | }, 113 | cli.StringFlag{ 114 | Name: "output", 115 | Usage: "output format in json, table. Default is table", 116 | Value: "table", 117 | }, 118 | cli.StringFlag{ 119 | Name: "output-file, o", 120 | Usage: "output file to save the content", 121 | }, 122 | } 123 | 124 | app.Commands = []cli.Command{ 125 | cecommands.ListCommand, 126 | cecommands.InfoCommand, 127 | cecommands.MountCommand, 128 | cecommands.MountAllCommand, 129 | cecommands.DriftCommand, 130 | cecommands.ExportCommand, 131 | cecommands.ExportAllCommand, 132 | } 133 | 134 | app.Before = func(context *cli.Context) error { 135 | if context.GlobalBool("debug") { 136 | log.SetLevel(log.DebugLevel) 137 | } 138 | return nil 139 | } 140 | 141 | err := app.Run(os.Args) 142 | if err != nil { 143 | log.Fatal(err) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /explorers/container.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package explorers 18 | 19 | import "github.com/containerd/containerd/containers" 20 | 21 | // Container provides information about a container. 22 | type Container struct { 23 | Namespace string 24 | Hostname string 25 | ImageBase string 26 | SupportContainer bool 27 | ContainerType string 28 | ProcessID int 29 | Status string 30 | 31 | // containerd specific fields 32 | containers.Container 33 | 34 | // docker specific fields 35 | Running bool 36 | ExposedPorts []string 37 | } 38 | 39 | // Drift provides information about container drift. 40 | type Drift struct { 41 | ContainerID string 42 | ContainerType string 43 | AddedOrModified []FileInfo 44 | InaccessibleFiles []FileInfo 45 | } 46 | -------------------------------------------------------------------------------- /explorers/containerd/bucket.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package containerd 18 | 19 | import ( 20 | bolt "go.etcd.io/bbolt" 21 | ) 22 | 23 | var ( 24 | bucketKeyVersion = []byte("v1") 25 | bucketKeyObjectSnapshots = []byte("snapshots") // stores snapshot references 26 | bucketKeyObjectContent = []byte("content") // stores content references 27 | bucketKeyObjectBlob = []byte("blob") // stores content links 28 | bucketKeySize = []byte("size") 29 | bucketKeyName = []byte("name") 30 | bucketKeyParent = []byte("parent") 31 | bucketKeyKind = []byte("kind") 32 | bucketKeyID = []byte("id") 33 | ) 34 | 35 | func getBucket(tx *bolt.Tx, keys ...[]byte) *bolt.Bucket { 36 | bkt := tx.Bucket(keys[0]) 37 | 38 | for _, key := range keys[1:] { 39 | if bkt == nil { 40 | break 41 | } 42 | bkt = bkt.Bucket(key) 43 | } 44 | 45 | return bkt 46 | } 47 | 48 | func getBlobsBucket(tx *bolt.Tx, namespace string) *bolt.Bucket { 49 | return getBucket(tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectContent, bucketKeyObjectBlob) 50 | } 51 | 52 | func getSnapshottersBucket(tx *bolt.Tx, namespace string) *bolt.Bucket { 53 | return getBucket(tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectSnapshots) 54 | } 55 | 56 | func getSnapshotKeyBucket(tx *bolt.Tx, namespace, snapshotter, snapshotkey string) *bolt.Bucket { 57 | return getBucket(tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectSnapshots, []byte(snapshotter), []byte(snapshotkey)) 58 | } 59 | 60 | func getOverlaySnapshotBucket(tx *bolt.Tx, snapshotkey string) *bolt.Bucket { 61 | return getBucket(tx, bucketKeyVersion, bucketKeyObjectSnapshots, []byte(snapshotkey)) 62 | } 63 | -------------------------------------------------------------------------------- /explorers/containerd/containerd.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package containerd 18 | 19 | import ( 20 | "context" 21 | "encoding/json" 22 | "fmt" 23 | "io/fs" 24 | "os" 25 | "os/exec" 26 | "path/filepath" 27 | "strings" 28 | "syscall" 29 | 30 | "github.com/containerd/containerd/containers" 31 | "github.com/containerd/containerd/metadata" 32 | "github.com/containerd/containerd/namespaces" 33 | "github.com/google/container-explorer/explorers" 34 | 35 | spec "github.com/opencontainers/runtime-spec/specs-go" 36 | 37 | log "github.com/sirupsen/logrus" 38 | bolt "go.etcd.io/bbolt" 39 | ) 40 | 41 | type explorer struct { 42 | imageroot string // mounted image path 43 | root string // containerd root 44 | manifest string // path to manifest database file i.e. meta.db 45 | snapshot string // path to snapshot database file i.e. metadata.db 46 | layercache string // layer cache folder within snapshot root 47 | mdb *bolt.DB // manifest database 48 | sc *explorers.SupportContainer // support container structure object 49 | } 50 | 51 | // NewExplorer returns a ContainerExplorer interface to explore containerd. 52 | func NewExplorer(imageroot string, root string, manifest string, snapshot string, layercache string, sc *explorers.SupportContainer) (explorers.ContainerExplorer, error) { 53 | opt := &bolt.Options{ 54 | ReadOnly: true, 55 | } 56 | db, err := bolt.Open(manifest, 0444, opt) 57 | if err != nil { 58 | return &explorer{}, err 59 | } 60 | 61 | return &explorer{ 62 | imageroot: imageroot, 63 | root: root, 64 | manifest: manifest, 65 | snapshot: snapshot, 66 | layercache: layercache, 67 | mdb: db, 68 | sc: sc, 69 | }, nil 70 | } 71 | 72 | // SnapshotRoot returns the root directory containing snapshot information. 73 | // 74 | // Containerd requires snapshot database metadata.db which is stored within 75 | // the snapshot root directory. 76 | // 77 | // The default snapshot root directrion location for containerd is 78 | // /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs 79 | func (e *explorer) SnapshotRoot(snapshotter string) string { 80 | dirs, _ := filepath.Glob(filepath.Join(e.root, "*")) 81 | snapshotRoot := "" 82 | for _, dir := range dirs { 83 | if strings.Contains(strings.ToLower(dir), strings.ToLower(snapshotter)) { 84 | filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { 85 | if strings.Contains(path, "metadata.db") { 86 | snapshotRoot, _ = filepath.Split(path) 87 | log.WithFields(log.Fields{ 88 | "path": path, 89 | "snapshotRoot": snapshotRoot, 90 | }).Debug("snapshot root") 91 | return fs.SkipAll 92 | } 93 | return nil 94 | }) 95 | return snapshotRoot 96 | } 97 | } 98 | return "unknown" 99 | } 100 | 101 | // ListNamespace returns namespaces. 102 | // 103 | // In containerd the namespace information is stored in metadata file meta.db. 104 | func (e *explorer) ListNamespaces(ctx context.Context) ([]string, error) { 105 | var nss []string 106 | 107 | err := e.mdb.View(func(tx *bolt.Tx) error { 108 | store := metadata.NewNamespaceStore(tx) 109 | results, err := store.List(ctx) 110 | if err != nil { 111 | return err 112 | } 113 | nss = results 114 | return nil 115 | }) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | return nss, nil 121 | } 122 | 123 | // ListContainers returns the information about containers. 124 | // 125 | // In containerd the container information is stored in metadata file meta.db. 126 | func (e *explorer) ListContainers(ctx context.Context) ([]explorers.Container, error) { 127 | var cecontainers []explorers.Container 128 | 129 | nss, err := e.ListNamespaces(ctx) 130 | if err != nil { 131 | return nil, err 132 | } 133 | 134 | store := metadata.NewContainerStore(metadata.NewDB(e.mdb, nil, nil)) 135 | 136 | for _, ns := range nss { 137 | ctx = namespaces.WithNamespace(ctx, ns) 138 | 139 | results, err := store.List(ctx) 140 | if err != nil { 141 | return nil, err 142 | } 143 | 144 | for _, result := range results { 145 | cectr := convertToContainerExplorerContainer(ns, result) 146 | cectr.ImageBase = imageBasename(cectr.Image) 147 | cectr.SupportContainer = e.sc.IsSupportContainer(cectr) 148 | 149 | task, err := e.GetContainerTask(ctx, cectr) 150 | if err != nil { 151 | log.WithField("containerid", cectr.ID).Error("failed getting container task") 152 | } 153 | cectr.ProcessID = task.PID 154 | cectr.ContainerType = task.ContainerType 155 | cectr.Status = task.Status 156 | 157 | cecontainers = append(cecontainers, cectr) 158 | } 159 | } 160 | return cecontainers, nil 161 | } 162 | 163 | // ListImages returns the information about content. 164 | // 165 | // In containerd, the image information is stored in metadata file meta.db. 166 | func (e *explorer) ListImages(ctx context.Context) ([]explorers.Image, error) { 167 | var ceimages []explorers.Image 168 | 169 | nss, err := e.ListNamespaces(ctx) 170 | if err != nil { 171 | return nil, err 172 | } 173 | 174 | store := metadata.NewImageStore(metadata.NewDB(e.mdb, nil, nil)) 175 | 176 | for _, ns := range nss { 177 | ctx = namespaces.WithNamespace(ctx, ns) 178 | 179 | results, err := store.List(ctx) 180 | if err != nil { 181 | return nil, err 182 | } 183 | 184 | for _, result := range results { 185 | ceimages = append(ceimages, explorers.Image{ 186 | Namespace: ns, 187 | SupportContainerImage: e.sc.SupportContainerImage(imageBasename(result.Name)), 188 | Image: result, 189 | }) 190 | } 191 | } 192 | return ceimages, nil 193 | } 194 | 195 | // ListContent returns the information about content. 196 | // 197 | // In containerd, the content information is stored in metadata file meta.db. 198 | func (e *explorer) ListContent(ctx context.Context) ([]explorers.Content, error) { 199 | var cecontent []explorers.Content 200 | 201 | nss, err := e.ListNamespaces(ctx) 202 | if err != nil { 203 | return nil, err 204 | } 205 | 206 | store := NewBlobStore(e.mdb) 207 | 208 | for _, ns := range nss { 209 | ctx = namespaces.WithNamespace(ctx, ns) 210 | 211 | results, err := store.List(ctx) 212 | if err != nil { 213 | return nil, err 214 | } 215 | 216 | for _, result := range results { 217 | cecontent = append(cecontent, explorers.Content{ 218 | Namespace: ns, 219 | Info: result, 220 | }) 221 | } 222 | } 223 | 224 | return cecontent, nil 225 | } 226 | 227 | // ListSnapshots returns the snapshot information. 228 | // 229 | // In containerd, the snapshot information is stored in two different files: 230 | // - metadata file (meta.db) 231 | // - snapshot file (metadata.db) 232 | // 233 | // These files contain some overlapping fields. 234 | // 235 | // The metadata file meta.db contains snapshot information and container 236 | // references the the snapshot information. 237 | // 238 | // The snapshot file metadata.db contains information about the snapshots only 239 | // without reference to a container. This file also containers informations 240 | // that are more relevant to manage snapshots. 241 | // 242 | // For Examples: 243 | // - Snapshot type i.e. active or committed 244 | // - Snapshot ID that refers to overlay path i.e /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots//fs 245 | // 246 | // Snapshot ID is required when mounting the container. 247 | func (e *explorer) ListSnapshots(ctx context.Context) ([]explorers.SnapshotKeyInfo, error) { 248 | var cesnapshots []explorers.SnapshotKeyInfo 249 | 250 | nss, err := e.ListNamespaces(ctx) 251 | if err != nil { 252 | return nil, err 253 | } 254 | 255 | // snapshot database 256 | opts := bolt.Options{ 257 | ReadOnly: true, 258 | } 259 | ssdb, err := bolt.Open(e.snapshot, 0444, &opts) 260 | if err != nil { 261 | log.WithFields(log.Fields{ 262 | "snapshotfile": e.snapshot, 263 | }).Error(err) 264 | } 265 | 266 | store := NewSnaptshotStore(e.root, e.layercache, e.mdb, ssdb) 267 | 268 | for _, ns := range nss { 269 | ctx = namespaces.WithNamespace(ctx, ns) 270 | 271 | results, err := store.List(ctx) 272 | if err != nil { 273 | return nil, err 274 | } 275 | 276 | cesnapshots = append(cesnapshots, results...) 277 | } 278 | 279 | return cesnapshots, nil 280 | } 281 | 282 | // ListTasks returns container tasks status 283 | func (e *explorer) ListTasks(ctx context.Context) ([]explorers.Task, error) { 284 | if e.imageroot == "" { 285 | log.Error("image-root is empty. Unable to list tasks.") 286 | return nil, nil 287 | } 288 | 289 | // Holds container task information. 290 | var cetasks []explorers.Task 291 | 292 | ctrs, err := e.ListContainers(ctx) 293 | if err != nil { 294 | return nil, fmt.Errorf("failed to list containers: %w", err) 295 | } 296 | for _, ctr := range ctrs { 297 | cetask, err := e.GetContainerTask(ctx, ctr) 298 | if err != nil { 299 | return nil, fmt.Errorf("failed getting a container's task: %w", err) 300 | } 301 | 302 | cetasks = append(cetasks, cetask) 303 | } 304 | 305 | return cetasks, nil 306 | } 307 | 308 | // GetContainerTask returns container task 309 | func (e *explorer) GetContainerTask(ctx context.Context, ctr explorers.Container) (explorers.Task, error) { 310 | ctx = namespaces.WithNamespace(ctx, ctr.Namespace) 311 | 312 | // Only return container spec 313 | v, err := e.InfoContainer(ctx, ctr.ID, true) 314 | if err != nil { 315 | return explorers.Task{}, fmt.Errorf("failed getting container spec for %s container: %w", ctr.ID, err) 316 | } 317 | ctrspec := v.(spec.Spec) 318 | 319 | var cgroupspath string 320 | var containertype string 321 | 322 | // Compute cgroup path for docker and containerd containers 323 | if strings.Contains(ctrspec.Linux.CgroupsPath, "docker") { 324 | containertype = "docker" 325 | 326 | // compute for docker 327 | // 328 | // Spec file `config.json` contains key cgroupsPath as `system.slice:docker:`. 329 | // The path maps on file system to `/sys/fs/cgroup/system.slice/docker-.scope`. 330 | m := strings.Split(ctrspec.Linux.CgroupsPath, ":") 331 | if len(m) != 3 { 332 | return explorers.Task{}, fmt.Errorf("expecting pattern system.slice:docker: and got %d fields", len(m)) 333 | } 334 | 335 | // docker cgroup directory i.e. system.slice 336 | cgroupns := m[0] 337 | // container cgroup information 338 | cgroupctrdir := fmt.Sprintf("%s-%s.scope", m[1], m[2]) 339 | // abolute path to container cgroup directory 340 | cgroupspath = filepath.Join(e.imageroot, "sys", "fs", "cgroup", cgroupns, cgroupctrdir) 341 | } else { 342 | containertype = "containerd" 343 | 344 | // compute for containerd 345 | // 346 | // Spec file contains "cgroupsPath": "/default/", 347 | cgroupspath = filepath.Join(e.imageroot, "sys", "fs", "cgroup", ctrspec.Linux.CgroupsPath) 348 | } 349 | 350 | // Verify the path actually exist on the system. 351 | // If a container is deleted then cgroup may not exist for the container 352 | if !explorers.PathExists(cgroupspath, false) { 353 | log.WithFields(log.Fields{ 354 | "containerid": ctr.ID, 355 | "cgroupspath": cgroupspath, 356 | }).Debug("container cgroup path does not exit") 357 | 358 | return explorers.Task{ 359 | Namespace: ctr.Namespace, 360 | Name: ctr.ID, 361 | ContainerType: containertype, 362 | Status: "UNKNOWN", 363 | }, nil 364 | } 365 | 366 | status, err := explorers.GetTaskStatus(cgroupspath) 367 | if err != nil { 368 | // Only print the error message. 369 | // The default return should contain status UNKNOWN 370 | log.WithField("containerid", ctr.ID).Error("failed getting container status for container: ", err) 371 | } 372 | 373 | // Get container process ID 374 | ctrpid := explorers.GetTaskPID(cgroupspath) 375 | if ctrpid == -1 && containertype == "containerd" { 376 | state, err := e.GetContainerState(ctx, ctr) 377 | if err != nil { 378 | log.WithField("containerid", ctr.ID).Error("failed getting container state") 379 | } 380 | if state.InitProcessPid != 0 { 381 | ctrpid = state.InitProcessPid 382 | } 383 | } 384 | 385 | return explorers.Task{ 386 | Namespace: ctr.Namespace, 387 | Name: ctr.ID, 388 | PID: ctrpid, 389 | ContainerType: containertype, 390 | Status: status, 391 | }, nil 392 | } 393 | 394 | // GetContainerState returns container runtime state 395 | func (e *explorer) GetContainerState(ctx context.Context, ctr explorers.Container) (explorers.State, error) { 396 | statedir := filepath.Join(e.imageroot, "run", "containerd", "runc", ctr.Namespace, ctr.ID) 397 | if !explorers.PathExists(statedir, false) { 398 | return explorers.State{}, fmt.Errorf("container state directory %s did not exist", statedir) 399 | } 400 | 401 | statefile := filepath.Join(statedir, "state.json") 402 | if !explorers.PathExists(statefile, true) { 403 | return explorers.State{}, fmt.Errorf("container state file %s did not exist", statefile) 404 | } 405 | 406 | data, err := os.ReadFile(statefile) 407 | if err != nil { 408 | return explorers.State{}, err 409 | } 410 | 411 | var state explorers.State 412 | if err := json.Unmarshal(data, &state); err != nil { 413 | return explorers.State{}, fmt.Errorf("unmarshalling state data: %w", err) 414 | } 415 | return state, nil 416 | } 417 | 418 | // InfoContainer returns container internal information. 419 | func (e *explorer) InfoContainer(ctx context.Context, containerid string, spec bool) (interface{}, error) { 420 | store := metadata.NewContainerStore(metadata.NewDB(e.mdb, nil, nil)) 421 | 422 | container, err := store.Get(ctx, containerid) 423 | if err != nil { 424 | return nil, err 425 | } 426 | 427 | if container.Spec != nil && container.Spec.GetValue() != nil { 428 | v, err := parseSpec(container.Spec.GetValue()) 429 | if err != nil { 430 | return nil, err 431 | } 432 | 433 | // Only return spec 434 | if spec { 435 | return v, nil 436 | } 437 | 438 | // Return container and spec info 439 | return struct { 440 | containers.Container 441 | Spec interface{} `json:"Spec,omitempty"` 442 | }{ 443 | Container: container, 444 | Spec: v, 445 | }, nil 446 | } 447 | 448 | // default return 449 | return nil, nil 450 | } 451 | 452 | // MountContainer mounts a container to the specified path 453 | func (e *explorer) MountContainer(ctx context.Context, containerid string, mountpoint string) error { 454 | store := metadata.NewContainerStore(metadata.NewDB(e.mdb, nil, nil)) 455 | 456 | container, err := store.Get(ctx, containerid) 457 | if err != nil { 458 | return fmt.Errorf("failed getting container information %v", err) 459 | } 460 | 461 | // Snapshot database metadata.db access 462 | opts := bolt.Options{ 463 | ReadOnly: true, 464 | } 465 | 466 | if e.snapshot == "" { 467 | snapshotterFolder := e.SnapshotRoot(container.Snapshotter) 468 | if snapshotterFolder != "unknown" { 469 | e.snapshot = filepath.Join(snapshotterFolder, "metadata.db") 470 | } 471 | } 472 | 473 | log.WithFields(log.Fields{ 474 | "snapshotter": container.Snapshotter, 475 | "snapshotKey": container.SnapshotKey, 476 | "image": container.Image, 477 | "snapshotterFolder": e.snapshot, 478 | }).Debug("container snapshotter") 479 | 480 | ssdb, err := bolt.Open(e.snapshot, 0444, &opts) 481 | if err != nil { 482 | return fmt.Errorf("failed to open snapshot database %v", err) 483 | } 484 | 485 | // snapshot store 486 | ssstore := NewSnaptshotStore(e.root, e.layercache, e.mdb, ssdb) 487 | var mountArgs []string 488 | hasWorkDir := false 489 | snapshotRoot, _ := filepath.Split(e.snapshot) 490 | matches, _ := filepath.Glob(filepath.Join(snapshotRoot, "snapshots/*/work")) 491 | if len(matches) > 0 { 492 | hasWorkDir = true 493 | } 494 | if container.Snapshotter == "native" { 495 | upperdir, err := ssstore.NativePath(ctx, container) 496 | log.WithFields(log.Fields{ 497 | "upperdir": upperdir, 498 | }).Debug("native directories") 499 | if err != nil { 500 | return fmt.Errorf("failed to get native path %v", err) 501 | } 502 | mountArgs = []string{"-t", "bind", upperdir, mountpoint, "-o", "rbind,ro"} 503 | } else if hasWorkDir { 504 | lowerdir, upperdir, workdir, err := ssstore.OverlayPath(ctx, container) 505 | log.WithFields(log.Fields{ 506 | "lowerdir": lowerdir, 507 | "upperdir": upperdir, 508 | "workdir": workdir, 509 | }).Debug("overlay directories") 510 | if err != nil { 511 | return fmt.Errorf("failed to get overlay path %v", err) 512 | } 513 | 514 | if lowerdir == "" { 515 | return fmt.Errorf("lowerdir is empty") 516 | } 517 | 518 | // TODO(rmaskey): Use github.com/containerd/containerd/mount.Mount to mount 519 | // a container 520 | mountopts := fmt.Sprintf("ro,lowerdir=%s:%s", upperdir, lowerdir) 521 | mountArgs = []string{"-t", "overlay", "overlay", "-o", mountopts, mountpoint} 522 | } else { 523 | log.Error("Unsupported snapshotter ", container.Snapshotter) 524 | } 525 | 526 | log.Debug("container mount command ", mountArgs) 527 | 528 | cmd := exec.Command("mount", mountArgs...) 529 | out, err := cmd.CombinedOutput() 530 | if err != nil { 531 | log.Errorf("running mount command %v", err) 532 | 533 | if strings.Contains(err.Error(), " 32") { 534 | log.Error("invalid overlayfs lowerdir path. Use --debug to view lowerdir path") 535 | } 536 | 537 | return err 538 | } 539 | 540 | if string(out) != "" { 541 | log.Info("mount command output ", string(out)) 542 | } 543 | 544 | // default 545 | return nil 546 | } 547 | 548 | // MountAllContainers mounts all the containers 549 | func (e *explorer) MountAllContainers(ctx context.Context, mountpoint string, filter string, skipsupportcontainers bool) error { 550 | ctrs, err := e.ListContainers(ctx) 551 | if err != nil { 552 | return err 553 | } 554 | 555 | filters := strings.Split(filter, ",") 556 | 557 | for _, ctr := range ctrs { 558 | // Skip Kubernetes suppot containers 559 | if skipsupportcontainers && ctr.SupportContainer { 560 | log.WithFields(log.Fields{ 561 | "namespace": ctr.Namespace, 562 | "containerid": ctr.ID, 563 | }).Info("skip mounting Kubernetes containers") 564 | 565 | continue 566 | } 567 | 568 | // Only mount containers matching the filter. 569 | mount := true 570 | for _, f := range filters { 571 | if !strings.Contains(f, "=") { 572 | continue 573 | } 574 | 575 | key := strings.Split(f, "=")[0] 576 | value := strings.Split(f, "=")[1] 577 | 578 | labelValue, ok := ctr.Labels[key] 579 | if !ok { 580 | mount = false 581 | break 582 | } 583 | 584 | if labelValue != value { 585 | mount = false 586 | break 587 | } 588 | } 589 | 590 | if !mount { 591 | continue 592 | } 593 | 594 | // Create a subdirectory within the specified mountpoint 595 | ctrmountpoint := filepath.Join(mountpoint, ctr.ID) 596 | if err := os.MkdirAll(ctrmountpoint, 0755); err != nil { 597 | log.WithFields(log.Fields{ 598 | "namespace": ctr.Namespace, 599 | "containerid": ctr.ID, 600 | "mountpoint": mountpoint, 601 | }).Error("creating mount point for a container") 602 | 603 | log.WithField("containerid", ctr.ID).Warn("skipping container mount") 604 | continue 605 | } 606 | 607 | // Clear snapshot database for each container 608 | e.snapshot = "" 609 | ctx = namespaces.WithNamespace(ctx, ctr.Namespace) 610 | if err := e.MountContainer(ctx, ctr.ID, ctrmountpoint); err != nil { 611 | return err 612 | } 613 | } 614 | 615 | // default 616 | return nil 617 | } 618 | 619 | // ScanDiffDirectory identifies added or modified files in the diff directory 620 | func ScanDiffDirectory(diffDir string) (addedOrModified []explorers.FileInfo, inaccessibleFiles []explorers.FileInfo, err error) { 621 | err = filepath.Walk(diffDir, func(path string, info os.FileInfo, err error) error { 622 | if err != nil { 623 | if fileinfo, err := explorers.GetFileInfo(info, path, diffDir); err == nil { 624 | inaccessibleFiles = append(inaccessibleFiles, *fileinfo) 625 | } 626 | 627 | return nil // Continue walking despite the error 628 | } 629 | if !info.IsDir() { 630 | fileinfo, err := explorers.GetFileInfo(info, path, diffDir) 631 | if err != nil { 632 | return err 633 | } 634 | 635 | // Check if the file is a whiteout files 636 | if info.Mode()&os.ModeCharDevice != 0 { 637 | if stat, ok := info.Sys().(*syscall.Stat_t); ok { 638 | rdev := stat.Rdev 639 | 640 | // Extract major and minor device numbers 641 | major := (rdev >> 8) & 0xfff 642 | minor := (rdev & 0xff) | ((rdev >> 12) & 0xfff00) 643 | 644 | if major == 0 && minor == 0 { 645 | // This is a whiteout file 646 | inaccessibleFiles = append(inaccessibleFiles, *fileinfo) 647 | 648 | return nil 649 | } 650 | } 651 | } 652 | 653 | // Check if the file is not a symbolic link 654 | if info.Mode()&os.ModeSymlink == 0 { 655 | // Check if the file has executable permissions 656 | mode := info.Mode().Perm() 657 | if mode&0111 != 0 { 658 | // The file is executable by owner, group, or others 659 | fileinfo.FileType = "executable" 660 | } 661 | } 662 | 663 | addedOrModified = append(addedOrModified, *fileinfo) 664 | } 665 | return nil 666 | }) 667 | 668 | return 669 | } 670 | 671 | // ContainerDrift finds drifted files from all the containers 672 | func (e *explorer) ContainerDrift(ctx context.Context, filter string, skipsupportcontainers bool, containerID string) ([]explorers.Drift, error) { 673 | var drifts []explorers.Drift 674 | ctrs, err := e.ListContainers(ctx) 675 | if err != nil { 676 | return nil, err 677 | } 678 | 679 | filters := strings.Split(filter, ",") 680 | 681 | for _, ctr := range ctrs { 682 | // If containerID is supplied & doesn't match skip 683 | if containerID != "" && ctr.ID != containerID { 684 | continue 685 | } 686 | 687 | // Skip Kubernetes suppot containers 688 | if skipsupportcontainers && ctr.SupportContainer { 689 | log.WithFields(log.Fields{ 690 | "namespace": ctr.Namespace, 691 | "containerid": ctr.ID, 692 | }).Info("skip mounting Kubernetes containers") 693 | 694 | continue 695 | } 696 | 697 | // Only analyze containers matching the filter. 698 | analyze := true 699 | for _, f := range filters { 700 | if !strings.Contains(f, "=") { 701 | continue 702 | } 703 | 704 | key := strings.Split(f, "=")[0] 705 | value := strings.Split(f, "=")[1] 706 | 707 | labelValue, ok := ctr.Labels[key] 708 | if !ok { 709 | analyze = false 710 | break 711 | } 712 | 713 | if labelValue != value { 714 | analyze = false 715 | break 716 | } 717 | } 718 | 719 | if !analyze { 720 | continue 721 | } 722 | 723 | e.snapshot = "" 724 | ctx = namespaces.WithNamespace(ctx, ctr.Namespace) 725 | store := metadata.NewContainerStore(metadata.NewDB(e.mdb, nil, nil)) 726 | 727 | container, err := store.Get(ctx, ctr.ID) 728 | if err != nil { 729 | return nil, fmt.Errorf("failed getting container information %v", err) 730 | } 731 | // Snapshot database metadata.db access 732 | opts := bolt.Options{ 733 | ReadOnly: true, 734 | } 735 | if e.snapshot == "" { 736 | snapshotterFolder := e.SnapshotRoot(container.Snapshotter) 737 | if snapshotterFolder != "unknown" { 738 | e.snapshot = filepath.Join(snapshotterFolder, "metadata.db") 739 | } 740 | } 741 | log.WithFields(log.Fields{ 742 | "snapshotter": container.Snapshotter, 743 | "snapshotKey": container.SnapshotKey, 744 | "image": container.Image, 745 | "snapshotterFolder": e.snapshot, 746 | }).Debug("container snapshotter") 747 | ssdb, err := bolt.Open(e.snapshot, 0444, &opts) 748 | if err != nil { 749 | return nil, fmt.Errorf("failed to open snapshot database %v", err) 750 | } 751 | // snapshot store 752 | ssstore := NewSnaptshotStore(e.root, e.layercache, e.mdb, ssdb) 753 | hasWorkDir := false 754 | snapshotRoot, _ := filepath.Split(e.snapshot) 755 | matches, _ := filepath.Glob(filepath.Join(snapshotRoot, "snapshots/*/work")) 756 | if len(matches) > 0 { 757 | hasWorkDir = true 758 | } 759 | if container.Snapshotter == "native" { 760 | upperdir, err := ssstore.NativePath(ctx, container) 761 | log.WithFields(log.Fields{ 762 | "upperdir": upperdir, 763 | }).Debug("native directories") 764 | if err != nil { 765 | return nil, fmt.Errorf("failed to get native path %v", err) 766 | } 767 | } else if hasWorkDir { 768 | lowerdir, upperdir, workdir, err := ssstore.OverlayPath(ctx, container) 769 | log.WithFields(log.Fields{ 770 | "lowerdir": lowerdir, 771 | "upperdir": upperdir, 772 | "workdir": workdir, 773 | }).Debug("overlay directories") 774 | 775 | log.WithFields(log.Fields{ 776 | "container ID": ctr.ID, 777 | }).Debug("Checking drift for container") 778 | if err != nil { 779 | return nil, fmt.Errorf("failed to get overlay path %v", err) 780 | } 781 | if lowerdir == "" { 782 | return nil, fmt.Errorf("lowerdir is empty") 783 | } 784 | 785 | // Scan upperdir 786 | addedOrModified, inaccessibleFiles, err := ScanDiffDirectory(upperdir) 787 | if err != nil { 788 | return nil, fmt.Errorf("failed to scan diff directory: %v", err) 789 | } 790 | 791 | drift := explorers.Drift{ 792 | ContainerID: ctr.ID, 793 | ContainerType: ctr.ContainerType, 794 | AddedOrModified: addedOrModified, 795 | InaccessibleFiles: inaccessibleFiles, 796 | } 797 | 798 | drifts = append(drifts, drift) 799 | 800 | for _, path := range addedOrModified { 801 | log.WithFields(log.Fields{ 802 | "A ": path}).Debug("added or modified files") 803 | } 804 | if len(inaccessibleFiles) > 0 { 805 | for _, path := range inaccessibleFiles { 806 | log.WithFields(log.Fields{ 807 | "D ": path}).Debug("deleted files") 808 | } 809 | } 810 | } else { 811 | log.Error("Unsupported snapshotter ", container.Snapshotter) 812 | } 813 | } 814 | 815 | // default 816 | return drifts, nil 817 | } 818 | 819 | // Close releases the internal resources 820 | func (e *explorer) Close() error { 821 | return e.mdb.Close() 822 | } 823 | 824 | // convertToContainerExplorerContainer returns a Container object which is 825 | // superset of containers.Container object. 826 | func convertToContainerExplorerContainer(ns string, ctr containers.Container) explorers.Container { 827 | var hostname string 828 | 829 | // Try using io.kubernetes.pod.name as the hostname. 830 | // 831 | // TODO(rmaskey): Research if EKS and AKS has similar labels used 832 | // for storing hostname. 833 | if value, match := ctr.Labels["io.kubernetes.pod.name"]; match { 834 | hostname = value 835 | } 836 | 837 | // Get hostname from runtime fields 838 | if hostname == "" && ctr.Spec != nil && ctr.Spec.GetValue() != nil { 839 | var v spec.Spec 840 | json.Unmarshal(ctr.Spec.GetValue(), &v) 841 | 842 | if v.Hostname != "" { 843 | hostname = v.Hostname 844 | } else { 845 | // Using HOSTNAME from environment as last resort. 846 | // HOSTNAME contains node's hostname. 847 | for _, kv := range v.Process.Env { 848 | if strings.HasPrefix(kv, "HOSTNAME=") { 849 | hostname = strings.TrimSpace(strings.Split(kv, "=")[1]) 850 | break 851 | } 852 | } 853 | } 854 | } 855 | 856 | return explorers.Container{ 857 | Namespace: ns, 858 | Hostname: hostname, 859 | Container: ctr, 860 | } 861 | } 862 | 863 | // parseSpec parses containerd spec and returns the information as JSON. 864 | func parseSpec(data []byte) (interface{}, error) { 865 | var v spec.Spec 866 | json.Unmarshal(data, &v) 867 | return v, nil 868 | } 869 | 870 | // imageBasename returns the image base name without version information to 871 | // match with supportcontainer.yaml configuration. 872 | func imageBasename(image string) string { 873 | imagebase := image 874 | 875 | if strings.Contains(imagebase, "@") { 876 | imagebase = strings.Split(imagebase, "@")[0] 877 | } 878 | 879 | if strings.Contains(imagebase, ":") { 880 | imagebase = strings.Split(imagebase, ":")[0] 881 | } 882 | return imagebase 883 | } 884 | -------------------------------------------------------------------------------- /explorers/containerd/content.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package containerd 18 | 19 | import ( 20 | "context" 21 | "encoding/binary" 22 | 23 | "github.com/containerd/containerd/content" 24 | "github.com/containerd/containerd/metadata/boltutil" 25 | "github.com/containerd/containerd/namespaces" 26 | "github.com/opencontainers/go-digest" 27 | bolt "go.etcd.io/bbolt" 28 | ) 29 | 30 | type blobStore struct { 31 | db *bolt.DB 32 | } 33 | 34 | // NewBlobStore returns blob store used for content operation 35 | // 36 | // In containerd, content information is stored in metadata file meta.db. 37 | // i.e. meta.db/v1//content/blob/ 38 | func NewBlobStore(db *bolt.DB) *blobStore { 39 | return &blobStore{ 40 | db: db, 41 | } 42 | } 43 | 44 | // List returns contents information. 45 | func (c *blobStore) List(ctx context.Context) ([]content.Info, error) { 46 | namespace, err := namespaces.NamespaceRequired(ctx) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | var infos []content.Info 52 | 53 | if err := c.db.View(func(tx *bolt.Tx) error { 54 | bkt := getBlobsBucket(tx, namespace) 55 | if bkt == nil { 56 | return nil // empty blob 57 | } 58 | 59 | return bkt.ForEach(func(k, v []byte) error { 60 | var ( 61 | info = content.Info{ 62 | Digest: digest.Digest(string(k)), 63 | } 64 | kbkt = bkt.Bucket(k) 65 | ) 66 | 67 | if err := readBlob(&info, kbkt); err != nil { 68 | return err 69 | } 70 | 71 | infos = append(infos, info) 72 | return nil 73 | }) 74 | }); err != nil { 75 | return nil, err 76 | } 77 | 78 | return infos, nil 79 | } 80 | 81 | func readBlob(info *content.Info, bkt *bolt.Bucket) error { 82 | if err := boltutil.ReadTimestamps(bkt, &info.CreatedAt, &info.UpdatedAt); err != nil { 83 | return err 84 | } 85 | 86 | labels, err := boltutil.ReadLabels(bkt) 87 | if err != nil { 88 | return err 89 | } 90 | info.Labels = labels 91 | 92 | if v := bkt.Get(bucketKeySize); len(v) > 0 { 93 | info.Size, _ = binary.Varint(v) 94 | } 95 | 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /explorers/containerd/export.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package containerd 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "os" 23 | "os/exec" 24 | "path/filepath" 25 | "strings" 26 | 27 | "github.com/containerd/containerd/namespaces" 28 | "github.com/google/container-explorer/explorers" 29 | "github.com/google/container-explorer/utils" 30 | log "github.com/sirupsen/logrus" 31 | ) 32 | 33 | // ExportContainer exports a container either as a raw image or an archive. 34 | func (e *explorer) ExportContainer(ctx context.Context, containerID string, outputDir string, exportOption map[string]bool) error { 35 | // Check if the specified containerID exists. 36 | containerExists := false 37 | 38 | containerNamespaces, err := e.ListNamespaces(ctx) 39 | if err != nil { 40 | return fmt.Errorf("listing namespaces: %w", err) 41 | } 42 | 43 | for _, containerNamespace := range containerNamespaces { 44 | ctx = namespaces.WithNamespace(ctx, containerNamespace) 45 | 46 | containers, err := e.ListContainers(ctx) 47 | if err != nil { 48 | log.WithFields(log.Fields{ 49 | "namespace": containerNamespace, 50 | "error": err, 51 | }).Warnf("error listing containers in namespace") 52 | 53 | continue 54 | } 55 | 56 | var targetContainer explorers.Container 57 | 58 | for _, container := range containers { 59 | if container.ID == containerID { 60 | targetContainer = container // Found the container 61 | containerExists = true 62 | break 63 | } 64 | } 65 | 66 | if !containerExists { 67 | log.WithFields(log.Fields{ 68 | "containerID": containerID, 69 | "namespace": containerNamespace, 70 | }).Debug("no container in namespace") 71 | 72 | continue 73 | } 74 | 75 | // Continue the following if a matching containerID is found. 76 | log.WithFields(log.Fields{ 77 | "containerID": targetContainer.ID, 78 | "name": targetContainer.Runtime.Name, 79 | "namespace": targetContainer.Namespace, 80 | "containerType": targetContainer.ContainerType, 81 | }).Info("container found") 82 | 83 | // Ensure outputDir exists 84 | if err := os.MkdirAll(outputDir, 0755); err != nil { 85 | return fmt.Errorf("failed to create output directory %s: %w", outputDir, err) 86 | } 87 | 88 | // Mount the container 89 | var mountpoint string 90 | for { 91 | mountpoint = utils.GetMountPoint() 92 | exists, _ := utils.PathExists(mountpoint) 93 | if !exists { 94 | // Create the mountpoint directory 95 | if err := os.MkdirAll(mountpoint, 0755); err != nil { 96 | return fmt.Errorf("failed to create mountpoint directory %s: %w", mountpoint, err) 97 | } 98 | break 99 | } 100 | } 101 | log.Infof("Attempting to mount container %s to %s", targetContainer.ID, mountpoint) 102 | 103 | if err := e.MountContainer(ctx, targetContainer.ID, mountpoint); err != nil { 104 | // If mountpoint was created, attempt to clean it up. 105 | _ = os.Remove(mountpoint) // Best effort removal 106 | return fmt.Errorf("failed to mount container %s: %w", targetContainer.ID, err) 107 | } 108 | log.Infof("Successfully mounted container %s to %s", targetContainer.ID, mountpoint) 109 | 110 | // Defer unmount and cleanup of the mountpoint 111 | defer func() { 112 | log.Infof("Cleaning up mountpoint %s for container %s", mountpoint, targetContainer.ID) 113 | unmountCmd := exec.Command("umount", mountpoint) 114 | unmountCmdOutput, unmountErr := unmountCmd.CombinedOutput() // Run and get output/error 115 | if unmountErr != nil { 116 | log.Warnf("Failed to unmount %s: %v. Output: %s", mountpoint, unmountErr, string(unmountCmdOutput)) 117 | } else { 118 | log.Infof("Successfully unmounted %s. Output: %s", mountpoint, string(unmountCmdOutput)) 119 | } 120 | 121 | if rmErr := os.Remove(mountpoint); rmErr != nil { 122 | log.Warnf("Failed to remove temporary mountpoint directory %s: %v", mountpoint, rmErr) 123 | } else { 124 | log.Infof("Successfully removed mountpoint directory %s", mountpoint) 125 | } 126 | }() 127 | 128 | if exportOption["image"] { 129 | log.Infof("Exporting container %s as a raw image to %s", targetContainer.ID, outputDir) 130 | if err := exportContainerImage(ctx, targetContainer.ID, mountpoint, outputDir); err != nil { 131 | return fmt.Errorf("failed to export container %s as raw image: %w", targetContainer.ID, err) 132 | } 133 | log.Infof("Successfully exported container %s as a raw image.", targetContainer.ID) 134 | } 135 | 136 | if exportOption["archive"] { 137 | log.Infof("Exporting container %s as an archive to %s", targetContainer.ID, outputDir) 138 | if err := exportContainerArchive(ctx, targetContainer.ID, mountpoint, outputDir); err != nil { 139 | return fmt.Errorf("failed to export container %s as archive: %w", targetContainer.ID, err) 140 | } 141 | log.Infof("Successfully exported container %s as an archive.", targetContainer.ID) 142 | } 143 | } 144 | 145 | if !containerExists { 146 | log.Infof("Container %s not found containerd containers", containerID) 147 | } 148 | 149 | return nil 150 | } 151 | 152 | // ExportAllContainers exports all containerd containers to specified output directory. 153 | func (e *explorer) ExportAllContainers(ctx context.Context, outputDir string, exportOption map[string]bool, filter map[string]string, exportSupportContainers bool) error { 154 | containerNamespaces, err := e.ListNamespaces(ctx) 155 | if err != nil { 156 | return fmt.Errorf("listing namespaces: %w", err) 157 | } 158 | 159 | for _, containerNamespace := range containerNamespaces { 160 | ctx = namespaces.WithNamespace(ctx, containerNamespace) 161 | 162 | containers, err := e.ListContainers(ctx) 163 | if err != nil { 164 | log.WithFields(log.Fields{ 165 | "namespace": containerNamespace, 166 | "error": err, 167 | }).Warnf("error listing containers in namespace") 168 | continue 169 | } 170 | 171 | log.WithFields(log.Fields{ 172 | "namespace": containerNamespace, 173 | "container_count": len(containers), 174 | }).Debug("containerd containers in namespace") 175 | 176 | for _, container := range containers { 177 | log.WithFields(log.Fields{ 178 | "containerID": container.ID, 179 | "name": container.Runtime.Name, 180 | "namespace": container.Namespace, 181 | "containerType": container.ContainerType, 182 | }).Debug("processing containerd container for export") 183 | 184 | if !exportSupportContainers && container.SupportContainer{ 185 | log.WithFields(log.Fields{ 186 | "containerID": container.ID, 187 | "name": container.Runtime.Name, 188 | "namespace": container.Namespace, 189 | "containerType": container.ContainerType, 190 | }).Debug("skipping Kubernetes support containers") 191 | continue 192 | } 193 | 194 | if utils.IncludeContainer(container, filter) { 195 | log.WithFields(log.Fields{ 196 | "containerID": container.ID, 197 | "name": container.Runtime.Name, 198 | "namespace": container.Namespace, 199 | "containerType": container.ContainerType, 200 | }).Debug("ignoring containerd container for export") 201 | 202 | err := e.ExportContainer(ctx, container.ID, outputDir, exportOption) 203 | if err != nil { 204 | log.WithFields(log.Fields{ 205 | "containerID": container.ID, 206 | "name": container.Runtime.Name, 207 | "namespace": container.Namespace, 208 | "containerType": container.ContainerType, 209 | "error": err, 210 | }).Error("error exporting containerd container") 211 | } 212 | } 213 | } 214 | } 215 | 216 | // Default 217 | return nil 218 | } 219 | 220 | // exportContainerImage creates a raw disk image file of a calculated size based on 221 | // the content of the mountpoint, formats it to ext4, and saves it to outputDir. 222 | func exportContainerImage(ctx context.Context, containerID string, mountpoint string, outputDir string) error { 223 | // 1. Calculate the required size for the image. 224 | contentSize, err := utils.CalculateDirectorySize(mountpoint) 225 | if err != nil { 226 | return fmt.Errorf("failed to calculate content size for %s: %w", mountpoint, err) 227 | } 228 | log.Infof("Calculated content size for %s: %d bytes", mountpoint, contentSize) 229 | 230 | // Add overhead for filesystem structures (e.g., 20MB base + 5% of content size for inodes, metadata) 231 | overhead := int64(20*1024*1024) + (contentSize / 20) 232 | imageSize := contentSize + overhead 233 | log.Infof("Target image size for %s: %d bytes (content: %d, overhead: %d)", containerID, imageSize, contentSize, overhead) 234 | 235 | imageFileName := fmt.Sprintf("%s.raw", containerID) 236 | imageFilePath := filepath.Join(outputDir, imageFileName) 237 | 238 | log.WithFields(log.Fields{ 239 | "containerID": containerID, 240 | "imageFilePath": imageFilePath, 241 | "imageSize": imageSize, 242 | }).Info("Preparing to create and format disk image") 243 | 244 | // 2. Create the image file 245 | imgFile, err := os.Create(imageFilePath) 246 | if err != nil { 247 | return fmt.Errorf("failed to create image file %s: %w", imageFilePath, err) 248 | } 249 | 250 | // 3. Set the image file size 251 | if err := imgFile.Truncate(imageSize); err != nil { 252 | imgFile.Close() // Attempt to close before returning 253 | return fmt.Errorf("failed to truncate image file %s to size %d: %w", imageFilePath, imageSize, err) 254 | } 255 | 256 | // 4. Sync and Close the file before formatting 257 | if err := imgFile.Sync(); err != nil { 258 | imgFile.Close() // Attempt to close before returning 259 | log.Warnf("failed to sync image file %s after truncation: %v", imageFilePath, err) 260 | } 261 | if err := imgFile.Close(); err != nil { 262 | return fmt.Errorf("failed to close image file %s before formatting: %w", imageFilePath, err) 263 | } 264 | log.Infof("Successfully created and sized image file: %s", imageFilePath) 265 | 266 | // 5. Format the image file as ext4 267 | log.WithFields(log.Fields{ 268 | "imageFilePath": imageFilePath, 269 | }).Info("Formatting image as ext4...") 270 | 271 | mkfsCmd := exec.CommandContext(ctx, "mkfs.ext4", "-F", "-q", imageFilePath) 272 | mkfsOutput, err := mkfsCmd.CombinedOutput() 273 | if err != nil { 274 | log.WithFields(log.Fields{ 275 | "command": mkfsCmd.String(), 276 | "output": string(mkfsOutput), 277 | "error": err, 278 | }).Error("mkfs.ext4 command failed") 279 | return fmt.Errorf("mkfs.ext4 failed for %s: %w. Output: %s", imageFilePath, err, string(mkfsOutput)) 280 | } 281 | 282 | log.WithFields(log.Fields{ 283 | "imageFilePath": imageFilePath, 284 | "output": string(mkfsOutput), 285 | }).Info("Successfully formatted image as ext4") 286 | 287 | // 6. Mount the formatted image, copy data, then unmount. 288 | log.Infof("Preparing to copy data from %s to image %s", mountpoint, imageFilePath) 289 | 290 | imageMountDir, err := os.MkdirTemp(outputDir, fmt.Sprintf("%s-img-mount-*.d", containerID)) 291 | if err != nil { 292 | return fmt.Errorf("failed to create temporary mount directory for image %s: %w", imageFilePath, err) 293 | } 294 | log.Infof("Created temporary image mount directory: %s", imageMountDir) 295 | 296 | var loopDevice string 297 | var imageSuccessfullyMounted bool = false 298 | 299 | // Defer cleanup actions in LIFO order (unmount image, detach loop, remove temp dir) 300 | defer func() { 301 | if imageSuccessfullyMounted { 302 | log.Infof("Unmounting image from %s", imageMountDir) 303 | umountCmd := exec.Command("umount", imageMountDir) // Use non-contextual command for cleanup 304 | // Best effort unmount 305 | if umountErr := umountCmd.Run(); umountErr != nil { 306 | umountOutput, _ := umountCmd.CombinedOutput() // Get output for logging 307 | log.Warnf("Failed to unmount image filesystem from %s: %v. Output: %s", imageMountDir, umountErr, string(umountOutput)) 308 | } else { 309 | log.Infof("Successfully unmounted image filesystem from %s", imageMountDir) 310 | } 311 | } 312 | 313 | if loopDevice != "" { 314 | log.Infof("Detaching loop device %s for image %s", loopDevice, imageFilePath) 315 | losetupDetachCmd := exec.Command("losetup", "-d", loopDevice) // Use non-contextual command for cleanup 316 | // Best effort detach 317 | if detachErr := losetupDetachCmd.Run(); detachErr != nil { 318 | detachOutput, _ := losetupDetachCmd.CombinedOutput() // Get output for logging 319 | log.Warnf("Failed to detach loop device %s: %v. Output: %s", loopDevice, detachErr, string(detachOutput)) 320 | } else { 321 | log.Infof("Successfully detached loop device %s", loopDevice) 322 | } 323 | } 324 | 325 | log.Infof("Removing temporary image mount directory %s", imageMountDir) 326 | if err := os.RemoveAll(imageMountDir); err != nil { 327 | log.Warnf("Failed to remove temporary image mount directory %s: %v", imageMountDir, err) 328 | } 329 | }() 330 | 331 | // 6.1. Setup loop device 332 | log.Infof("Setting up loop device for %s", imageFilePath) 333 | losetupCmd := exec.CommandContext(ctx, "losetup", "-f", "--show", imageFilePath) 334 | loopDeviceBytes, err := losetupCmd.Output() // Use Output to capture stdout, which is the loop device path 335 | if err != nil { 336 | // If Output() fails, CombinedOutput() can give more info if stderr was involved 337 | losetupCombinedOutput, _ := exec.CommandContext(ctx, "losetup", "-f", "--show", imageFilePath).CombinedOutput() 338 | log.Errorf("losetup -f --show %s failed: %v. Output: %s", imageFilePath, err, string(losetupCombinedOutput)) 339 | return fmt.Errorf("losetup -f --show %s failed: %w. Output: %s", imageFilePath, err, string(losetupCombinedOutput)) 340 | } 341 | loopDevice = strings.TrimSpace(string(loopDeviceBytes)) 342 | if loopDevice == "" { 343 | log.Errorf("losetup -f --show %s returned an empty loop device path.", imageFilePath) 344 | return fmt.Errorf("losetup -f --show %s returned an empty loop device path", imageFilePath) 345 | } 346 | log.Infof("Image %s associated with loop device %s", imageFilePath, loopDevice) 347 | 348 | // 6.2. Mount the loop device 349 | log.Infof("Mounting loop device %s to %s", loopDevice, imageMountDir) 350 | mountImageCmd := exec.CommandContext(ctx, "mount", loopDevice, imageMountDir) 351 | mountImageOutput, err := mountImageCmd.CombinedOutput() 352 | if err != nil { 353 | log.Errorf("Failed to mount %s to %s: %v. Output: %s", loopDevice, imageMountDir, err, string(mountImageOutput)) 354 | return fmt.Errorf("failed to mount loop device %s to %s: %w. Output: %s", loopDevice, imageMountDir, err, string(mountImageOutput)) 355 | } 356 | imageSuccessfullyMounted = true // Set flag for deferred cleanup 357 | log.Infof("Successfully mounted %s to %s. Output: %s", loopDevice, imageMountDir, string(mountImageOutput)) 358 | 359 | // 6.3. Copy content from container's mountpoint to the image's mountpoint 360 | // Source path: mountpoint + "/." to copy contents of the directory, not the directory itself. 361 | sourcePathFiles, _ := filepath.Glob(filepath.Join(mountpoint, "*")) 362 | 363 | for _, sourcePathForCopy := range sourcePathFiles { 364 | log.Infof("Copying contents from %s to %s using 'cp -a'", sourcePathForCopy, imageMountDir) 365 | 366 | copyCmd := exec.Command("cp", "-a", sourcePathForCopy, imageMountDir) 367 | copyOutput, err := copyCmd.CombinedOutput() 368 | if err != nil { 369 | log.Errorf("Failed to copy data from %s to %s: %v. Output: %s", sourcePathForCopy, imageMountDir, err, string(copyOutput)) 370 | return fmt.Errorf("failed to copy data from %s to %s: %w. Output: %s", sourcePathForCopy, imageMountDir, err, string(copyOutput)) 371 | } 372 | log.Infof("Successfully copied data from %s to %s. Output: %s", sourcePathForCopy, imageMountDir, string(copyOutput)) 373 | } 374 | 375 | // 6.4. Sync filesystem buffers to ensure all data is written to the image 376 | log.Info("Syncing filesystem buffers for the image.") 377 | syncCmd := exec.CommandContext(ctx, "sync") 378 | if syncErr := syncCmd.Run(); syncErr != nil { 379 | // This is usually not fatal but good to log. 380 | syncOutput, _ := syncCmd.CombinedOutput() // Get output for logging 381 | log.Warnf("sync command failed after copying to image: %v. Output: %s", syncErr, string(syncOutput)) 382 | } else { 383 | log.Info("Filesystem buffers synced.") 384 | } 385 | 386 | log.Infof("Image %s successfully created, formatted, and populated.", imageFilePath) 387 | 388 | return nil 389 | } 390 | 391 | // exportContainerArchive creates a .tar.gz archive of the content of the mountpoint. 392 | func exportContainerArchive(ctx context.Context, containerID string, mountpoint string, outputDir string) error { 393 | archiveFileName := fmt.Sprintf("%s.tar.gz", containerID) 394 | archiveFilePath := filepath.Join(outputDir, archiveFileName) 395 | 396 | log.WithFields(log.Fields{ 397 | "containerID": containerID, 398 | "mountpoint": mountpoint, 399 | "archiveFilePath": archiveFilePath, 400 | }).Info("Preparing to create container archive") 401 | 402 | // Command: tar -czf -C . 403 | // -c: create 404 | // -z: gzip 405 | // -f: file 406 | // -C : change to directory before processing files 407 | // .: process all files in the current directory (which is due to -C) 408 | tarCmd := exec.CommandContext(ctx, "tar", "-czf", archiveFilePath, "-C", mountpoint, ".") 409 | 410 | tarOutput, err := tarCmd.CombinedOutput() 411 | if err != nil { 412 | log.WithFields(log.Fields{ 413 | "command": tarCmd.String(), 414 | "output": string(tarOutput), 415 | "error": err, 416 | }).Error("tar command failed") 417 | return fmt.Errorf("failed to create archive %s: %w. Output: %s", archiveFilePath, err, string(tarOutput)) 418 | } 419 | 420 | log.WithFields(log.Fields{ 421 | "archiveFilePath": archiveFilePath, 422 | "output": string(tarOutput), 423 | }).Info("Successfully created container archive") 424 | 425 | return nil 426 | } -------------------------------------------------------------------------------- /explorers/containerd/snapshot.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package containerd 18 | 19 | import ( 20 | "context" 21 | "encoding/binary" 22 | "fmt" 23 | "io/fs" 24 | "os" 25 | "path/filepath" 26 | "strings" 27 | 28 | "github.com/containerd/containerd/containers" 29 | "github.com/containerd/containerd/metadata/boltutil" 30 | "github.com/containerd/containerd/namespaces" 31 | "github.com/containerd/containerd/snapshots" 32 | "github.com/google/container-explorer/explorers" 33 | log "github.com/sirupsen/logrus" 34 | bolt "go.etcd.io/bbolt" 35 | ) 36 | 37 | type snapshotStore struct { 38 | root string // containerd root directory 39 | layercache string 40 | db *bolt.DB 41 | sdb *bolt.DB 42 | } 43 | 44 | // NewSnapshotStore returns snapshotStore which handles viewing of snapshot information 45 | // 46 | // In containerd, snapshot information is stored in metadata file meta.db and snapshot 47 | // database file metadata.db. 48 | // 49 | // The meta.db file contains the following information: 50 | // - Container reference to container snapshot: meta.db/v1//containers/ 51 | // - snapshotter 52 | // - snapshotKey 53 | // - Snapshot information in meta.db/v1//snapshots// 54 | // 55 | // The metadata.db file contains additional information about a snapshot. 56 | // Snapshot path in snapshot database: metadata.db/v1/snapshots/ 57 | // - id - Snapshot file system ID i.e. /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots//fs 58 | // - kind - ACTIVE vs COMMITTED 59 | func NewSnaptshotStore(root string, layercache string, db *bolt.DB, sdb *bolt.DB) *snapshotStore { 60 | return &snapshotStore{ 61 | root: root, 62 | layercache: layercache, 63 | db: db, 64 | sdb: sdb, 65 | } 66 | } 67 | 68 | // List returns a structure that contains combined information from metadata 69 | // and snapshot database snapshot key. 70 | func (s *snapshotStore) List(ctx context.Context) ([]explorers.SnapshotKeyInfo, error) { 71 | namespace, err := namespaces.NamespaceRequired(ctx) 72 | if err != nil { 73 | return nil, fmt.Errorf("failed to get namespace from context %v", err) 74 | } 75 | 76 | // Overlay snapshot bucket 77 | if s.sdb == nil { 78 | log.Warn("handle to snapshot database does not exist") 79 | } 80 | 81 | var skinfos []explorers.SnapshotKeyInfo 82 | 83 | // Read metadata database i.e. meta.db and extract relevant information 84 | // about the snapshots. 85 | if err := s.db.View(func(tx *bolt.Tx) error { 86 | bkt := getSnapshottersBucket(tx, namespace) 87 | if bkt == nil { 88 | return nil // empty store 89 | } 90 | 91 | // Handle each snapshotter 92 | // meta.db/v1/// 93 | bkt.ForEach(func(k, v []byte) error { 94 | ssbkt := bkt.Bucket(k) 95 | if ssbkt == nil { 96 | return nil // empty snapshotter 97 | } 98 | 99 | // Handle each snapshot key 100 | // meta.db/v1//snapshots// 101 | return ssbkt.ForEach(func(k1, v1 []byte) error { 102 | var ( 103 | skinfo = explorers.SnapshotKeyInfo{ 104 | Namespace: namespace, 105 | Snapshotter: string(k), 106 | Key: string(k1), 107 | } 108 | 109 | // snapshot key bucket that contains information about a 110 | // snapshot 111 | kbkt = ssbkt.Bucket(k1) 112 | ) 113 | 114 | if err := readMetaSnapshotKey(&skinfo, kbkt); err != nil { 115 | return err 116 | } 117 | 118 | // Reading additional snapshot key information from metadata.db 119 | // snapshot key 120 | if s.sdb != nil { 121 | s.sdb.View(func(otx *bolt.Tx) error { 122 | log.WithFields(log.Fields{ 123 | "snapshot key": skinfo.Key, 124 | "snapshot name": skinfo.Name, 125 | }).Debug("meta.db snapshot key") 126 | skbkt := getOverlaySnapshotBucket(otx, skinfo.Name) 127 | if skbkt == nil { 128 | log.WithFields(log.Fields{ 129 | "snapshot key": skinfo.Key, 130 | }).Info("empty metata.db snapshot key bucket") 131 | return nil 132 | } 133 | readOverlaySnapshotKey(&skinfo, skbkt) 134 | 135 | return nil 136 | }) 137 | } 138 | 139 | skinfos = append(skinfos, skinfo) 140 | return nil 141 | }) 142 | }) 143 | 144 | return nil 145 | }); err != nil { 146 | return nil, err 147 | } 148 | 149 | return skinfos, nil 150 | } 151 | 152 | // NativePath returns the upperdir for a container. 153 | func (s *snapshotStore) NativePath(ctx context.Context, container containers.Container) (string, error) { 154 | if s.sdb == nil { 155 | return "", fmt.Errorf("snapshot database handler (metadata.db) is nil") 156 | } 157 | 158 | snapshotkeys, err := s.SnapshotKeys(ctx, container) 159 | if err != nil { 160 | return "", fmt.Errorf("could not get snapshot keys for container ", container.ID) 161 | } 162 | 163 | var ( 164 | upperdir string 165 | snapshotroot string 166 | ) 167 | 168 | snapshotroot = snapshotRootDir(s.root, container.Snapshotter) 169 | // Read snapshot metadata (metadata.db) snapshotkey bucket 170 | // and extract value of key "id". 171 | // 172 | // The value of "id" specifies snapshot path in overlayfs 173 | // i.e. /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots//fs 174 | if err := s.sdb.View(func(tx *bolt.Tx) error { 175 | upperdirID, err := getSnapshotID(tx, snapshotkeys[0]) 176 | if err != nil { 177 | return err 178 | } 179 | upperdir = filepath.Join(snapshotroot, "snapshots", fmt.Sprintf("%d", upperdirID)) 180 | return nil 181 | }); err != nil { 182 | return "", err 183 | } 184 | return upperdir, nil 185 | } 186 | 187 | // OverlayPath returns the overlay paths lowerdir, upperdir, and workdir for a container. 188 | func (s *snapshotStore) OverlayPath(ctx context.Context, container containers.Container) (string, string, string, error) { 189 | if s.sdb == nil { 190 | return "", "", "", fmt.Errorf("snapshot database handler (metadata.db) is nil") 191 | } 192 | 193 | snapshotkeys, err := s.SnapshotKeys(ctx, container) 194 | if err != nil { 195 | return "", "", "", fmt.Errorf("could not get snapshot keys for container ", container.ID) 196 | } 197 | 198 | var ( 199 | lowerdir string 200 | upperdir string 201 | workdir string 202 | snapshotroot string 203 | ) 204 | 205 | snapshotroot = snapshotRootDir(s.root, container.Snapshotter) 206 | // Read snapshot metadata (metadata.db) snapshotkey bucket 207 | // and extract value of key "id". 208 | // 209 | // The value of "id" specifies snapshot path in overlayfs 210 | // i.e. /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots//fs 211 | if err := s.sdb.View(func(tx *bolt.Tx) error { 212 | upperdirID, err := getSnapshotID(tx, snapshotkeys[0]) 213 | if err != nil { 214 | return err 215 | } 216 | upperdir = filepath.Join(snapshotroot, "snapshots", fmt.Sprintf("%d", upperdirID), "fs") 217 | workdir = filepath.Join(snapshotroot, "snapshots", fmt.Sprintf("%d", upperdirID), "work") 218 | 219 | if s.layercache != "" { 220 | symlink, err := os.Readlink(upperdir) 221 | if err == nil { 222 | log.WithFields(log.Fields{ 223 | "id": upperdirID, 224 | "symlink": symlink, 225 | }).Debug("upperdir") 226 | _, layer := filepath.Split(symlink) 227 | upperdir = filepath.Join(snapshotroot, s.layercache, layer) 228 | } 229 | } 230 | 231 | // compute lowerdir 232 | for _, ssk := range snapshotkeys[1:] { 233 | id, err := getSnapshotID(tx, ssk) 234 | if err != nil { 235 | return err 236 | } 237 | ldir := filepath.Join(snapshotroot, "snapshots", fmt.Sprintf("%d", id), "fs") 238 | if s.layercache != "" { 239 | symlink, err := os.Readlink(ldir) 240 | if err == nil { 241 | log.WithFields(log.Fields{ 242 | "id": id, 243 | "symlink": symlink, 244 | }).Debug("layer") 245 | _, layer := filepath.Split(symlink) 246 | ldir = filepath.Join(snapshotroot, "layers", layer) 247 | } 248 | } 249 | 250 | if lowerdir == "" { 251 | lowerdir = ldir 252 | continue 253 | } 254 | lowerdir = fmt.Sprintf("%s:%s", lowerdir, ldir) 255 | } 256 | return nil 257 | }); err != nil { 258 | return "", "", "", err 259 | } 260 | 261 | // default return 262 | return lowerdir, upperdir, workdir, nil 263 | } 264 | 265 | // SnapshotKeys returns the snapshot keys for a container. 266 | func (s *snapshotStore) SnapshotKeys(ctx context.Context, container containers.Container) ([]string, error) { 267 | namespace, err := namespaces.NamespaceRequired(ctx) 268 | if err != nil { 269 | return nil, fmt.Errorf("failed to get namespace from context %v", err) 270 | } 271 | var snapshotkeys []string 272 | 273 | if err := s.db.View(func(tx *bolt.Tx) error { 274 | ssk := container.SnapshotKey 275 | 276 | for { 277 | bkt := getSnapshotKeyBucket(tx, namespace, container.Snapshotter, ssk) 278 | log.WithFields(log.Fields{ 279 | "namespace": namespace, 280 | "snapshotter": container.Snapshotter, 281 | "snapshotkey": ssk, 282 | }).Debug("snapshot key bucket") 283 | if bkt == nil { 284 | return fmt.Errorf("empty meta.db snapshotkey bucket") 285 | } 286 | 287 | name := string(bkt.Get(bucketKeyName)) 288 | parent := string(bkt.Get(bucketKeyParent)) 289 | 290 | snapshotkeys = append(snapshotkeys, name) 291 | 292 | if parent == "" { 293 | break 294 | } 295 | ssk = parent 296 | } 297 | 298 | return nil 299 | }); err != nil { 300 | return nil, err 301 | } 302 | return snapshotkeys, nil 303 | } 304 | 305 | // readMetaSnapshotKey parses the snapshot key key-value pairs in meta.db 306 | func readMetaSnapshotKey(skinfo *explorers.SnapshotKeyInfo, bkt *bolt.Bucket) error { 307 | boltutil.ReadTimestamps(bkt, &skinfo.CreatedAt, &skinfo.UpdatedAt) 308 | 309 | skinfo.Name = string(bkt.Get(bucketKeyName)) 310 | skinfo.Parent = string(bkt.Get(bucketKeyParent)) 311 | skinfo.Labels, _ = boltutil.ReadLabels(bkt) 312 | 313 | return nil 314 | } 315 | 316 | // readOverlaySnapshotKey parses the snapshot key key-value pairs in metadata.db 317 | func readOverlaySnapshotKey(skinfo *explorers.SnapshotKeyInfo, bkt *bolt.Bucket) error { 318 | boltutil.ReadTimestamps(bkt, &skinfo.CreatedAt, &skinfo.UpdatedAt) 319 | 320 | parent := string(bkt.Get(bucketKeyParent)) 321 | if skinfo.Parent == "" { 322 | skinfo.Parent = parent 323 | } else if skinfo.Parent != parent { 324 | log.WithFields(log.Fields{ 325 | "old parent": skinfo.Parent, 326 | "new parent": parent, 327 | }).Info("overwriting old parent with new parent") 328 | } 329 | 330 | skinfo.ID, _ = binary.Uvarint(bkt.Get(bucketKeyID)) 331 | skinfo.OverlayPath = fmt.Sprintf("snapshots/%d/fs", skinfo.ID) 332 | 333 | kind, _ := binary.Uvarint(bkt.Get(bucketKeyKind)) 334 | skinfo.Kind = snapshots.Kind(uint8(kind)) 335 | 336 | skinfo.Size, _ = binary.Uvarint(bkt.Get(bucketKeySize)) 337 | 338 | // Handle if skinfo already has labels from meta.db 339 | labels, _ := boltutil.ReadLabels(bkt) 340 | for k, v := range labels { 341 | if val, found := skinfo.Labels[k]; found { 342 | if v != val { 343 | log.WithFields(log.Fields{ 344 | "existing value": val, 345 | "new value": v, 346 | }).Warn("over writing old lable with new label") 347 | } 348 | } else { 349 | skinfo.Labels[k] = v 350 | } 351 | } 352 | return nil 353 | } 354 | 355 | // getSnapshotID returns snapshot key ID. 356 | // 357 | // This returns value of metadata.db/v1/snapshots//id 358 | func getSnapshotID(tx *bolt.Tx, snapshotkey string) (uint64, error) { 359 | bkt := getOverlaySnapshotBucket(tx, snapshotkey) 360 | if bkt == nil { 361 | return 0, fmt.Errorf("empty snapshotkey bucket %s", snapshotkey) 362 | } 363 | 364 | id, _ := binary.Uvarint(bkt.Get(bucketKeyID)) 365 | return id, nil 366 | } 367 | 368 | // snapshotRootDir returns snapshot root directory. 369 | // 370 | // In containerd, the default snapshot root directory is 371 | // /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs 372 | func snapshotRootDir(root string, snapshotter string) string { 373 | dirs, _ := filepath.Glob(filepath.Join(root, "*")) 374 | snapshotRoot := "" 375 | for _, dir := range dirs { 376 | if strings.Contains(strings.ToLower(dir), strings.ToLower(snapshotter)) { 377 | filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { 378 | if strings.Contains(path, "metadata.db") { 379 | snapshotRoot, _ = filepath.Split(path) 380 | log.WithFields(log.Fields{ 381 | "path": path, 382 | "snapshotRoot": snapshotRoot, 383 | }).Debug("snapshot root") 384 | return fs.SkipAll 385 | } 386 | return nil 387 | }) 388 | return snapshotRoot 389 | } 390 | } 391 | return "" 392 | } 393 | -------------------------------------------------------------------------------- /explorers/content.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package explorers 18 | 19 | import "github.com/containerd/containerd/content" 20 | 21 | // Content provides information about containers' content 22 | type Content struct { 23 | Namespace string 24 | content.Info 25 | } 26 | -------------------------------------------------------------------------------- /explorers/docker/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package docker 18 | 19 | import "time" 20 | 21 | // State holds attribute about docker container state 22 | type State struct { 23 | Running bool 24 | Paused bool 25 | Resarting bool 26 | OOMKilled bool 27 | RemovalInProgress bool 28 | Dead bool 29 | Pid int64 30 | ExitCode int64 31 | Error string 32 | StartedAt time.Time 33 | FinishedAt time.Time 34 | Health interface{} 35 | } 36 | 37 | // Config holds docker runtime config 38 | type Config struct { 39 | ExposedPorts map[string]interface{} 40 | Hostname string 41 | Domainname string 42 | User string 43 | AttachStdin bool 44 | AttachStdout bool 45 | AttachStderr bool 46 | Tty bool 47 | OpenStdin bool 48 | StdinOnce bool 49 | Env []string 50 | Cmd []string 51 | Image string 52 | Volumes interface{} 53 | WorkingDir interface{} 54 | EntryPoint interface{} 55 | OnBuild interface{} 56 | Labels map[string]string 57 | } 58 | 59 | // Bridge represents docker networks bridge structure 60 | type Bridge struct { 61 | IPAMConfig interface{} 62 | Links interface{} 63 | Aliases interface{} 64 | NetworkID string 65 | EndpointID string 66 | Gateway string 67 | IPAddresses string 68 | IPPrefixLen int 69 | IPv6Gateway string 70 | GlobalIPv6Address string 71 | GlobalIPPrefixLen int 72 | MacAddresses string 73 | IPAMOperational bool 74 | } 75 | 76 | // NetworkSettings represents docker network settings 77 | type NetworkSettings struct { 78 | Bridge string 79 | SandboxID string 80 | HairpinMode bool 81 | LinkLocalIPv6Address string 82 | LinkLocalIPv6PrefixLen int 83 | Networks map[string]interface{} 84 | Service map[string]interface{} 85 | Ports map[string]interface{} 86 | SandboxKey string 87 | SecondaryIPAddresses interface{} 88 | SecondaryIPv6Addresses interface{} 89 | IsAnonymousEndpoint bool 90 | HasSwarmEndpoint bool 91 | } 92 | 93 | // ConfigFile represents docker config.v2.json structure 94 | type ConfigFile struct { 95 | StreamConfig map[string]interface{} 96 | State State 97 | ID string 98 | Created time.Time 99 | Managed bool 100 | Path string 101 | Args []string 102 | ContainerConfig map[string]interface{} 103 | Config Config 104 | Image string 105 | NetworkSettings NetworkSettings 106 | LogPath string 107 | Name string 108 | Driver string 109 | MountLabel string 110 | ProcessLabel string 111 | RestartCount int64 112 | HasBeenRestartedBefore bool 113 | HasBeenManuallyStopped bool 114 | MountPoints map[string]interface{} 115 | SecretReferences interface{} 116 | AppArmorProfile string 117 | HostnamePath string 118 | HostsPath string 119 | ShmPath string 120 | ResolvConfPath string 121 | SeccompProfile string 122 | NoNewPrivileges bool 123 | } 124 | -------------------------------------------------------------------------------- /explorers/docker/export.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package docker 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "os" 23 | "os/exec" 24 | "path/filepath" 25 | "strings" 26 | 27 | "github.com/containerd/containerd/namespaces" 28 | "github.com/google/container-explorer/explorers" 29 | "github.com/google/container-explorer/utils" 30 | log "github.com/sirupsen/logrus" 31 | ) 32 | 33 | // ExportContainer exports a container either as a raw image or an archive. 34 | func (e *explorer) ExportContainer(ctx context.Context, containerID string, outputDir string, exportOption map[string]bool) error { 35 | // Check if the specified containerID exists. 36 | containerExists := false 37 | 38 | containerNamespaces, err := e.ListNamespaces(ctx) 39 | if err != nil { 40 | return fmt.Errorf("listing namespaces: %w", err) 41 | } 42 | 43 | for _, containerNamespace := range containerNamespaces { 44 | ctx = namespaces.WithNamespace(ctx, containerNamespace) 45 | 46 | containers, err := e.ListContainers(ctx) 47 | if err != nil { 48 | log.WithFields(log.Fields{ 49 | "namespace": containerNamespace, 50 | "error": err, 51 | }).Warnf("error listing containers in namespace") 52 | 53 | continue 54 | } 55 | 56 | var targetContainer explorers.Container 57 | 58 | for _, container := range containers { 59 | if container.ID == containerID { 60 | targetContainer = container // Found the container 61 | containerExists = true 62 | break 63 | } 64 | } 65 | 66 | if !containerExists { 67 | log.WithFields(log.Fields{ 68 | "containerID": containerID, 69 | "namespace": containerNamespace, 70 | }).Debug("no container in namespace") 71 | 72 | continue 73 | } 74 | 75 | // Continue the following if a matching containerID is found. 76 | log.WithFields(log.Fields{ 77 | "containerID": targetContainer.ID, 78 | "name": targetContainer.Runtime.Name, 79 | "namespace": targetContainer.Namespace, 80 | "containerType": targetContainer.ContainerType, 81 | }).Info("container found") 82 | 83 | // Ensure outputDir exists 84 | if err := os.MkdirAll(outputDir, 0755); err != nil { 85 | return fmt.Errorf("failed to create output directory %s: %w", outputDir, err) 86 | } 87 | 88 | // Mount the container 89 | var mountpoint string 90 | for { 91 | mountpoint = utils.GetMountPoint() 92 | exists, _ := utils.PathExists(mountpoint) 93 | if !exists { 94 | // Create the mountpoint directory 95 | if err := os.MkdirAll(mountpoint, 0755); err != nil { 96 | return fmt.Errorf("failed to create mountpoint directory %s: %w", mountpoint, err) 97 | } 98 | break 99 | } 100 | } 101 | log.Infof("Attempting to mount container %s to %s", targetContainer.ID, mountpoint) 102 | 103 | if err := e.MountContainer(ctx, targetContainer.ID, mountpoint); err != nil { 104 | // If mountpoint was created, attempt to clean it up. 105 | _ = os.Remove(mountpoint) // Best effort removal 106 | return fmt.Errorf("failed to mount container %s: %w", targetContainer.ID, err) 107 | } 108 | log.Infof("Successfully mounted container %s to %s", targetContainer.ID, mountpoint) 109 | 110 | // Defer unmount and cleanup of the mountpoint 111 | defer func() { 112 | log.Infof("Cleaning up mountpoint %s for container %s", mountpoint, targetContainer.ID) 113 | unmountCmd := exec.Command("umount", mountpoint) 114 | unmountCmdOutput, unmountErr := unmountCmd.CombinedOutput() // Run and get output/error 115 | if unmountErr != nil { 116 | log.Warnf("Failed to unmount %s: %v. Output: %s", mountpoint, unmountErr, string(unmountCmdOutput)) 117 | } else { 118 | log.Infof("Successfully unmounted %s. Output: %s", mountpoint, string(unmountCmdOutput)) 119 | } 120 | 121 | if rmErr := os.Remove(mountpoint); rmErr != nil { 122 | log.Warnf("Failed to remove temporary mountpoint directory %s: %v", mountpoint, rmErr) 123 | } else { 124 | log.Infof("Successfully removed mountpoint directory %s", mountpoint) 125 | } 126 | }() 127 | 128 | if exportOption["image"] { 129 | log.Infof("Exporting container %s as a raw image to %s", targetContainer.ID, outputDir) 130 | if err := exportContainerImage(ctx, targetContainer.ID, mountpoint, outputDir); err != nil { 131 | return fmt.Errorf("failed to export container %s as raw image: %w", targetContainer.ID, err) 132 | } 133 | log.Infof("Successfully exported container %s as a raw image.", targetContainer.ID) 134 | } 135 | 136 | if exportOption["archive"] { 137 | log.Infof("Exporting container %s as an archive to %s", targetContainer.ID, outputDir) 138 | if err := exportContainerArchive(ctx, targetContainer.ID, mountpoint, outputDir); err != nil { 139 | return fmt.Errorf("failed to export container %s as archive: %w", targetContainer.ID, err) 140 | } 141 | log.Infof("Successfully exported container %s as an archive.", targetContainer.ID) 142 | } 143 | } 144 | 145 | if !containerExists { 146 | log.Infof("Container %s not found in Docker containers", containerID) 147 | } 148 | 149 | return nil 150 | } 151 | 152 | // ExportAllContainers exports all Docker containers to specified output directory. 153 | func (e *explorer) ExportAllContainers(ctx context.Context, outputDir string, exportOption map[string]bool, filter map[string]string, exportSupportContainers bool) error { 154 | containerNamespaces, err := e.ListNamespaces(ctx) 155 | if err != nil { 156 | return fmt.Errorf("listing namespaces: %w", err) 157 | } 158 | 159 | for _, containerNamespace := range containerNamespaces { 160 | ctx = namespaces.WithNamespace(ctx, containerNamespace) 161 | 162 | containers, err := e.ListContainers(ctx) 163 | if err != nil { 164 | log.WithFields(log.Fields{ 165 | "namespace": containerNamespace, 166 | "error": err, 167 | }).Warnf("error listing containers in namespace") 168 | 169 | continue 170 | } 171 | log.WithFields(log.Fields{ 172 | "namespace": containerNamespace, 173 | "container_count": len(containers), 174 | }).Debug("Docker containers in namespace") 175 | 176 | for _, container := range containers { 177 | log.WithFields(log.Fields{ 178 | "containerID": container.ID, 179 | "name": container.Runtime.Name, 180 | "namespace": container.Namespace, 181 | "containerType": container.ContainerType, 182 | }).Debug("processing Docker container for export") 183 | 184 | if !exportSupportContainers && container.SupportContainer{ 185 | log.WithFields(log.Fields{ 186 | "containerID": container.ID, 187 | "name": container.Runtime.Name, 188 | "namespace": container.Namespace, 189 | "containerType": container.ContainerType, 190 | }).Debug("skipping Kubernetes support containers") 191 | continue 192 | } 193 | 194 | if utils.IncludeContainer(container, filter) { 195 | log.WithFields(log.Fields{ 196 | "containerID": container.ID, 197 | "name": container.Runtime.Name, 198 | "namespace": container.Namespace, 199 | "containerType": container.ContainerType, 200 | }).Debug("ignoring Docker container for export") 201 | 202 | err := e.ExportContainer(ctx, container.ID, outputDir, exportOption) 203 | if err != nil { 204 | log.WithFields(log.Fields{ 205 | "containerID": container.ID, 206 | "name": container.Runtime.Name, 207 | "namespace": container.Namespace, 208 | "containerType": container.ContainerType, 209 | "error": err, 210 | }).Error("error exporting Docker container") 211 | } 212 | } 213 | } 214 | } 215 | 216 | // Default 217 | return nil 218 | } 219 | 220 | // exportContainerImage creates a raw disk image file of a calculated size based on 221 | // the content of the mountpoint, formats it to ext4, and saves it to outputDir. 222 | func exportContainerImage(ctx context.Context, containerID string, mountpoint string, outputDir string) error { 223 | // 1. Calculate the required size for the image. 224 | contentSize, err := utils.CalculateDirectorySize(mountpoint) 225 | if err != nil { 226 | return fmt.Errorf("failed to calculate content size for %s: %w", mountpoint, err) 227 | } 228 | log.Infof("Calculated content size for %s: %d bytes", mountpoint, contentSize) 229 | 230 | // Add overhead for filesystem structures (e.g., 20MB base + 5% of content size for inodes, metadata) 231 | overhead := int64(20*1024*1024) + (contentSize / 20) 232 | imageSize := contentSize + overhead 233 | log.Infof("Target image size for %s: %d bytes (content: %d, overhead: %d)", containerID, imageSize, contentSize, overhead) 234 | 235 | imageFileName := fmt.Sprintf("%s.raw", containerID) 236 | imageFilePath := filepath.Join(outputDir, imageFileName) 237 | 238 | log.WithFields(log.Fields{ 239 | "containerID": containerID, 240 | "imageFilePath": imageFilePath, 241 | "imageSize": imageSize, 242 | }).Info("Preparing to create and format disk image") 243 | 244 | // 2. Create the image file 245 | imgFile, err := os.Create(imageFilePath) 246 | if err != nil { 247 | return fmt.Errorf("failed to create image file %s: %w", imageFilePath, err) 248 | } 249 | 250 | // 3. Set the image file size 251 | if err := imgFile.Truncate(imageSize); err != nil { 252 | imgFile.Close() // Attempt to close before returning 253 | return fmt.Errorf("failed to truncate image file %s to size %d: %w", imageFilePath, imageSize, err) 254 | } 255 | 256 | // 4. Sync and Close the file before formatting 257 | if err := imgFile.Sync(); err != nil { 258 | imgFile.Close() // Attempt to close before returning 259 | log.Warnf("failed to sync image file %s after truncation: %v", imageFilePath, err) 260 | } 261 | if err := imgFile.Close(); err != nil { 262 | return fmt.Errorf("failed to close image file %s before formatting: %w", imageFilePath, err) 263 | } 264 | log.Infof("Successfully created and sized image file: %s", imageFilePath) 265 | 266 | // 5. Format the image file as ext4 267 | log.WithFields(log.Fields{ 268 | "imageFilePath": imageFilePath, 269 | }).Info("Formatting image as ext4...") 270 | 271 | mkfsCmd := exec.CommandContext(ctx, "mkfs.ext4", "-F", "-q", imageFilePath) 272 | mkfsOutput, err := mkfsCmd.CombinedOutput() 273 | if err != nil { 274 | log.WithFields(log.Fields{ 275 | "command": mkfsCmd.String(), 276 | "output": string(mkfsOutput), 277 | "error": err, 278 | }).Error("mkfs.ext4 command failed") 279 | return fmt.Errorf("mkfs.ext4 failed for %s: %w. Output: %s", imageFilePath, err, string(mkfsOutput)) 280 | } 281 | 282 | log.WithFields(log.Fields{ 283 | "imageFilePath": imageFilePath, 284 | "output": string(mkfsOutput), 285 | }).Info("Successfully formatted image as ext4") 286 | 287 | // 6. Mount the formatted image, copy data, then unmount. 288 | log.Infof("Preparing to copy data from %s to image %s", mountpoint, imageFilePath) 289 | 290 | imageMountDir, err := os.MkdirTemp(outputDir, fmt.Sprintf("%s-img-mount-*.d", containerID)) 291 | if err != nil { 292 | return fmt.Errorf("failed to create temporary mount directory for image %s: %w", imageFilePath, err) 293 | } 294 | log.Infof("Created temporary image mount directory: %s", imageMountDir) 295 | 296 | var loopDevice string 297 | var imageSuccessfullyMounted bool = false 298 | 299 | // Defer cleanup actions in LIFO order (unmount image, detach loop, remove temp dir) 300 | defer func() { 301 | if imageSuccessfullyMounted { 302 | log.Infof("Unmounting image from %s", imageMountDir) 303 | umountCmd := exec.Command("umount", imageMountDir) // Use non-contextual command for cleanup 304 | // Best effort unmount 305 | if umountErr := umountCmd.Run(); umountErr != nil { 306 | umountOutput, _ := umountCmd.CombinedOutput() // Get output for logging 307 | log.Warnf("Failed to unmount image filesystem from %s: %v. Output: %s", imageMountDir, umountErr, string(umountOutput)) 308 | } else { 309 | log.Infof("Successfully unmounted image filesystem from %s", imageMountDir) 310 | } 311 | } 312 | 313 | if loopDevice != "" { 314 | log.Infof("Detaching loop device %s for image %s", loopDevice, imageFilePath) 315 | losetupDetachCmd := exec.Command("losetup", "-d", loopDevice) // Use non-contextual command for cleanup 316 | // Best effort detach 317 | if detachErr := losetupDetachCmd.Run(); detachErr != nil { 318 | detachOutput, _ := losetupDetachCmd.CombinedOutput() // Get output for logging 319 | log.Warnf("Failed to detach loop device %s: %v. Output: %s", loopDevice, detachErr, string(detachOutput)) 320 | } else { 321 | log.Infof("Successfully detached loop device %s", loopDevice) 322 | } 323 | } 324 | 325 | log.Infof("Removing temporary image mount directory %s", imageMountDir) 326 | if err := os.RemoveAll(imageMountDir); err != nil { 327 | log.Warnf("Failed to remove temporary image mount directory %s: %v", imageMountDir, err) 328 | } 329 | }() 330 | 331 | // 6.1. Setup loop device 332 | log.Infof("Setting up loop device for %s", imageFilePath) 333 | losetupCmd := exec.CommandContext(ctx, "losetup", "-f", "--show", imageFilePath) 334 | loopDeviceBytes, err := losetupCmd.Output() // Use Output to capture stdout, which is the loop device path 335 | if err != nil { 336 | // If Output() fails, CombinedOutput() can give more info if stderr was involved 337 | losetupCombinedOutput, _ := exec.CommandContext(ctx, "losetup", "-f", "--show", imageFilePath).CombinedOutput() 338 | log.Errorf("losetup -f --show %s failed: %v. Output: %s", imageFilePath, err, string(losetupCombinedOutput)) 339 | return fmt.Errorf("losetup -f --show %s failed: %w. Output: %s", imageFilePath, err, string(losetupCombinedOutput)) 340 | } 341 | loopDevice = strings.TrimSpace(string(loopDeviceBytes)) 342 | if loopDevice == "" { 343 | log.Errorf("losetup -f --show %s returned an empty loop device path.", imageFilePath) 344 | return fmt.Errorf("losetup -f --show %s returned an empty loop device path", imageFilePath) 345 | } 346 | log.Infof("Image %s associated with loop device %s", imageFilePath, loopDevice) 347 | 348 | // 6.2. Mount the loop device 349 | log.Infof("Mounting loop device %s to %s", loopDevice, imageMountDir) 350 | mountImageCmd := exec.CommandContext(ctx, "mount", loopDevice, imageMountDir) 351 | mountImageOutput, err := mountImageCmd.CombinedOutput() 352 | if err != nil { 353 | log.Errorf("Failed to mount %s to %s: %v. Output: %s", loopDevice, imageMountDir, err, string(mountImageOutput)) 354 | return fmt.Errorf("failed to mount loop device %s to %s: %w. Output: %s", loopDevice, imageMountDir, err, string(mountImageOutput)) 355 | } 356 | imageSuccessfullyMounted = true // Set flag for deferred cleanup 357 | log.Infof("Successfully mounted %s to %s. Output: %s", loopDevice, imageMountDir, string(mountImageOutput)) 358 | 359 | // 6.3. Copy content from container's mountpoint to the image's mountpoint 360 | // Source path: mountpoint + "/." to copy contents of the directory, not the directory itself. 361 | sourcePathFiles, _ := filepath.Glob(filepath.Join(mountpoint, "*")) 362 | 363 | for _, sourcePathForCopy := range sourcePathFiles { 364 | log.Infof("Copying contents from %s to %s using 'cp -a'", sourcePathForCopy, imageMountDir) 365 | 366 | copyCmd := exec.Command("cp", "-a", sourcePathForCopy, imageMountDir) 367 | copyOutput, err := copyCmd.CombinedOutput() 368 | if err != nil { 369 | log.Errorf("Failed to copy data from %s to %s: %v. Output: %s", sourcePathForCopy, imageMountDir, err, string(copyOutput)) 370 | return fmt.Errorf("failed to copy data from %s to %s: %w. Output: %s", sourcePathForCopy, imageMountDir, err, string(copyOutput)) 371 | } 372 | log.Infof("Successfully copied data from %s to %s. Output: %s", sourcePathForCopy, imageMountDir, string(copyOutput)) 373 | } 374 | 375 | // 6.4. Sync filesystem buffers to ensure all data is written to the image 376 | log.Info("Syncing filesystem buffers for the image.") 377 | syncCmd := exec.CommandContext(ctx, "sync") 378 | if syncErr := syncCmd.Run(); syncErr != nil { 379 | // This is usually not fatal but good to log. 380 | syncOutput, _ := syncCmd.CombinedOutput() // Get output for logging 381 | log.Warnf("sync command failed after copying to image: %v. Output: %s", syncErr, string(syncOutput)) 382 | } else { 383 | log.Info("Filesystem buffers synced.") 384 | } 385 | 386 | log.Infof("Image %s successfully created, formatted, and populated.", imageFilePath) 387 | 388 | return nil 389 | } 390 | 391 | // exportContainerArchive creates a .tar.gz archive of the content of the mountpoint. 392 | func exportContainerArchive(ctx context.Context, containerID string, mountpoint string, outputDir string) error { 393 | archiveFileName := fmt.Sprintf("%s.tar.gz", containerID) 394 | archiveFilePath := filepath.Join(outputDir, archiveFileName) 395 | 396 | log.WithFields(log.Fields{ 397 | "containerID": containerID, 398 | "mountpoint": mountpoint, 399 | "archiveFilePath": archiveFilePath, 400 | }).Info("Preparing to create container archive") 401 | 402 | // Command: tar -czf -C . 403 | // -c: create 404 | // -z: gzip 405 | // -f: file 406 | // -C : change to directory before processing files 407 | // .: process all files in the current directory (which is due to -C) 408 | tarCmd := exec.CommandContext(ctx, "tar", "-czf", archiveFilePath, "-C", mountpoint, ".") 409 | 410 | tarOutput, err := tarCmd.CombinedOutput() 411 | if err != nil { 412 | log.WithFields(log.Fields{ 413 | "command": tarCmd.String(), 414 | "output": string(tarOutput), 415 | "error": err, 416 | }).Error("tar command failed") 417 | return fmt.Errorf("failed to create archive %s: %w. Output: %s", archiveFilePath, err, string(tarOutput)) 418 | } 419 | 420 | log.WithFields(log.Fields{ 421 | "archiveFilePath": archiveFilePath, 422 | "output": string(tarOutput), 423 | }).Info("Successfully created container archive") 424 | 425 | return nil 426 | } 427 | -------------------------------------------------------------------------------- /explorers/explorers.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package explorers 18 | 19 | import ( 20 | "context" 21 | ) 22 | 23 | // ContainerExplorer defines the methods required to explore a container. 24 | type ContainerExplorer interface { 25 | // Close releases the internal resources 26 | Close() error 27 | 28 | // ContainerDrift identifies container filesystem changes 29 | ContainerDrift(ctx context.Context, filter string, skipsupportcontainers bool, containerID string) ([]Drift, error) 30 | 31 | // ExportAllContainers exports all Docker and containerd containers. 32 | ExportAllContainers(ctx context.Context, outputDir string, exportOption map[string]bool, filter map[string]string, exportSupportContainers bool) error 33 | 34 | // ExportContainer exports a container as an image or archive. 35 | ExportContainer(ctx context.Context, containerID string, outputDir string, exportOption map[string]bool) error 36 | 37 | // InfoContainer returns container internal information 38 | InfoContainer(ctx context.Context, containerid string, spec bool) (interface{}, error) 39 | 40 | // ListContainers returns all the containers in all the namespaces. 41 | // 42 | // ListContainers returns the ContainerExplorer's Containers structure 43 | // that holds additional information about the containers. 44 | ListContainers(ctx context.Context) ([]Container, error) 45 | 46 | // ListContent returns information about content 47 | ListContent(ctx context.Context) ([]Content, error) 48 | 49 | // ListImages returns content information 50 | ListImages(ctx context.Context) ([]Image, error) 51 | 52 | // ListNamespaces returns all the namespaces in the metadata file i.e. 53 | // meta.db 54 | ListNamespaces(ctx context.Context) ([]string, error) 55 | 56 | // ListSnapshots returns the snapshot information 57 | ListSnapshots(ctx context.Context) ([]SnapshotKeyInfo, error) 58 | 59 | // ListTasks returns the container task status 60 | ListTasks(ctx context.Context) ([]Task, error) 61 | 62 | // MountAllContainer mounts all containers to the specfied path 63 | MountAllContainers(ctx context.Context, mountpoint string, filter string, skipsupportcontainers bool) error 64 | 65 | // MountContainer mounts a container to the specified path 66 | MountContainer(ctx context.Context, containerid string, mountpoint string) error 67 | 68 | // SnapshotRoot returns the directory containing snapshots and snapshot 69 | // database i.e. metadata.db 70 | // 71 | // SnapshotRoot is required for the containers managed using containerd. 72 | SnapshotRoot(snapshotter string) string 73 | } 74 | -------------------------------------------------------------------------------- /explorers/fileinfo.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package explorers 18 | 19 | import ( 20 | "crypto/sha256" 21 | "encoding/hex" 22 | "encoding/json" 23 | "io" 24 | "os" 25 | "strings" 26 | "syscall" 27 | "time" 28 | ) 29 | 30 | type FileInfo struct { 31 | FileName string `json:"file_name"` 32 | FullPath string `json:"full_path"` 33 | FileSize int64 `json:"file_size"` 34 | FileModified time.Time `json:"file_modified"` 35 | FileAccessed time.Time `json:"file_accessed"` 36 | FileChanged time.Time `json:"file_changed"` 37 | FileBirth time.Time `json:"file_birth"` 38 | FileUid string `json:"file_uid,omitempty"` 39 | FileOwner string `json:"file_owner,omitempty"` 40 | FileGid string `json:"file_gid,omitempty"` 41 | FileType string `json:"file_type,omitempty"` 42 | FileSHA256 string `json:"file_sha256,omitempty"` 43 | } 44 | 45 | func (f *FileInfo) AsJSON() ([]byte, error) { 46 | jsonData, err := json.Marshal(f) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | return jsonData, nil 52 | } 53 | 54 | // FileSHA256Sum calculates SHA256 hash of the specified 55 | // file. 56 | func FileSHA256Sum(path string) (string, error) { 57 | var err error 58 | 59 | file, err := os.Open(path) 60 | if err != nil { 61 | return "", err 62 | } 63 | defer file.Close() 64 | 65 | hash := sha256.New() 66 | if _, err = io.Copy(hash, file); err != nil { 67 | return "", err 68 | } 69 | 70 | hashBytes := hash.Sum(nil) 71 | hashString := hex.EncodeToString(hashBytes) 72 | 73 | return hashString, nil 74 | } 75 | 76 | // GetFileInfo returns file information in drift detection. 77 | func GetFileInfo(info os.FileInfo, path string, diffDir string) (*FileInfo, error) { 78 | diffFileInfo := FileInfo{ 79 | FileName: info.Name(), 80 | FullPath: strings.Replace(path, diffDir, "", 1), 81 | FileSize: info.Size(), 82 | FileModified: info.ModTime().UTC(), 83 | } 84 | 85 | if stat, ok := info.Sys().(*syscall.Stat_t); ok { 86 | diffFileInfo.FileAccessed = time.Unix(stat.Atim.Sec, stat.Atim.Nsec).UTC() 87 | diffFileInfo.FileChanged = time.Unix(stat.Mtim.Sec, stat.Ctim.Nsec).UTC() 88 | diffFileInfo.FileBirth = time.Unix(stat.Ctim.Sec, stat.Ctim.Nsec).UTC() 89 | } 90 | 91 | if hash, err := FileSHA256Sum(path); err == nil { 92 | diffFileInfo.FileSHA256 = hash 93 | } 94 | 95 | return &diffFileInfo, nil 96 | } 97 | -------------------------------------------------------------------------------- /explorers/image.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package explorers 18 | 19 | import ( 20 | "github.com/containerd/containerd/images" 21 | ) 22 | 23 | // Image provides information about a container image. 24 | type Image struct { 25 | Namespace string 26 | SupportContainerImage bool 27 | images.Image 28 | } 29 | -------------------------------------------------------------------------------- /explorers/runtime.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package explorers 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "path/filepath" 23 | "strconv" 24 | "strings" 25 | 26 | log "github.com/sirupsen/logrus" 27 | ) 28 | 29 | // GetTaskStatus returns task status 30 | func GetTaskStatus(cgrouppath string) (string, error) { 31 | populated, frozen, err := ReadCgroupEvents(cgrouppath) 32 | if err != nil { 33 | return "UNKNOWN", fmt.Errorf("reading group.events: %w", err) 34 | } 35 | 36 | if populated == 0 && frozen == 0 { 37 | return "STOPPED", nil 38 | } else if populated == 1 && frozen == 0 { 39 | return "RUNNING", nil 40 | } else if populated == 1 && frozen == 1 { 41 | return "PAUSED", nil 42 | } 43 | 44 | return "UNKNOWN", fmt.Errorf("unknown status with values populated: %d, frozen: %d", populated, frozen) 45 | } 46 | 47 | // GetTaskPID returns process ID of the containers 48 | func GetTaskPID(path string) int { 49 | pidfile := filepath.Join(path, "cgroup.procs") 50 | if !PathExists(pidfile, true) { 51 | return -1 52 | } 53 | 54 | data, err := os.ReadFile(pidfile) 55 | if err != nil { 56 | log.WithField("path", pidfile).Error("reading cgroup.procs: ", err) 57 | return -1 58 | } 59 | 60 | pid, err := strconv.Atoi(strings.Split(string(data), "\n")[0]) 61 | if err != nil { 62 | log.WithField("path", pidfile).Info("converting to int: ", err) 63 | return -1 64 | } 65 | return pid 66 | } 67 | 68 | // ReadCgroupEvents returns populated and frozen status 69 | func ReadCgroupEvents(path string) (int, int, error) { 70 | data, err := os.ReadFile(filepath.Join(path, "cgroup.events")) 71 | if err != nil { 72 | return -1, -1, err 73 | } 74 | 75 | populated := -1 76 | frozen := -1 77 | 78 | for _, line := range strings.Split(string(data), "\n") { 79 | if strings.Contains(line, "populated ") { 80 | val := strings.Replace(line, "populated ", "", -1) 81 | val = strings.TrimSpace(val) 82 | 83 | populated, err = strconv.Atoi(val) 84 | if err != nil { 85 | populated = -1 86 | } 87 | } 88 | 89 | if strings.Contains(line, "frozen ") { 90 | val := strings.Replace(line, "frozen ", "", -1) 91 | val = strings.TrimSpace(val) 92 | 93 | frozen, err = strconv.Atoi(val) 94 | if err != nil { 95 | frozen = -1 96 | } 97 | } 98 | } 99 | return populated, frozen, nil 100 | } 101 | 102 | // PathExists returns true if the path exists 103 | func PathExists(path string, isfile bool) bool { 104 | finfo, err := os.Stat(path) 105 | if err != nil { 106 | return false 107 | } 108 | 109 | if isfile { 110 | return !finfo.IsDir() 111 | } 112 | return finfo.IsDir() 113 | } 114 | -------------------------------------------------------------------------------- /explorers/snapshot.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package explorers 18 | 19 | import ( 20 | "time" 21 | 22 | "github.com/containerd/containerd/snapshots" 23 | ) 24 | 25 | // SnapshotKeyInfo provides information about snapshots. 26 | // 27 | // SnapshotKeyInfo contains information found in containerd 28 | // metadata (meta.db) and snapshot database (metadata.db). 29 | type SnapshotKeyInfo struct { 30 | Namespace string // namespace only used in meta.db 31 | Snapshotter string // only used in meta.db 32 | Key string // snapshot key 33 | ID uint64 // File system ID. Only used in metadata.db 34 | Name string // snapshot name. Only used in meta.db 35 | Parent string // snapshot parent 36 | Kind snapshots.Kind // snapshot kind 37 | Inodes []int64 // Inode numbers. Only in metadata.db 38 | Size uint64 // Only in metadata.db 39 | OverlayPath string // Custom field added by container explorer 40 | Labels map[string]string // mapped labels 41 | Children []string // array of . Only in meta.db 42 | CreatedAt time.Time // created timestamp 43 | UpdatedAt time.Time // updated timestamp 44 | } 45 | -------------------------------------------------------------------------------- /explorers/state.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package explorers 18 | 19 | import "time" 20 | 21 | // State holds runc state information. 22 | // 23 | // runc state file is located at `/run/containerd/runc///state.json`. 24 | // 25 | // The State structure only maps the required attributes form state.json. 26 | type State struct { 27 | ID string `json:"state,omitempty"` 28 | InitProcessPid int `json:"init_process_pid"` 29 | InitProcessstart int `json:"init_process_start"` 30 | Created time.Time `json:"created"` 31 | Config map[string]interface{} `json:"config"` 32 | Rootless bool `json:"rootless"` 33 | CgroupPaths map[string]string `json:"cgroup_paths"` 34 | NamespacePaths map[string]string `json:"namespace_paths"` 35 | ExternalDescriptors []string `json:"external_descriptors"` 36 | IntelRdtPath string `json:"intel_rdt_path"` 37 | } 38 | -------------------------------------------------------------------------------- /explorers/support_containers.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package explorers 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "os" 23 | "strings" 24 | 25 | log "github.com/sirupsen/logrus" 26 | "gopkg.in/yaml.v3" 27 | ) 28 | 29 | // SupportContainer contains information about support container. 30 | // 31 | // A support container can be identified by: 32 | // - Container or pod hostname 33 | // - Image name 34 | // - Container labels or annotations 35 | type SupportContainer struct { 36 | ContainerNames []string `json:"names" yaml:"names"` 37 | ImageNames []string `json:"images" yaml:"images"` 38 | Labels []string `json:"labels" yaml:"labels"` 39 | } 40 | 41 | // NewSupportContainer returns the support container instance. 42 | func NewSupportContainer(path string) (*SupportContainer, error) { 43 | sc, err := LoadSupportContainerFromFile(path) 44 | if err != nil { 45 | return nil, err 46 | } 47 | // default return 48 | return &sc, nil 49 | } 50 | 51 | // LoadSupportContainerFromFile loads the support container information from 52 | // a yaml file on disk. 53 | func LoadSupportContainerFromFile(path string) (SupportContainer, error) { 54 | var sc SupportContainer 55 | 56 | data, err := os.ReadFile(path) 57 | if err != nil { 58 | return SupportContainer{}, fmt.Errorf("reading file %s: %v", path, err) 59 | } 60 | 61 | if err := yaml.Unmarshal(data, &sc); err != nil { 62 | return SupportContainer{}, fmt.Errorf("unmarshalling %s: %v", path, err) 63 | } 64 | 65 | // default return 66 | return sc, nil 67 | } 68 | 69 | // SupportContainerImage returns true if the supplied image is a known support 70 | // container image. 71 | func (sc *SupportContainer) SupportContainerImage(image string) bool { 72 | if sc == nil { 73 | log.WithField("imagebase", image).Debug("support container data not initialized") 74 | return false 75 | } 76 | 77 | for _, scimage := range sc.ImageNames { 78 | /* 79 | if strings.ToLower(scimage) == strings.ToLower(image) { 80 | return true 81 | } 82 | */ 83 | if strings.Contains(strings.ToLower(image), strings.ToLower(scimage)) { 84 | log.WithField("imagebase", image).Debug("support container image found") 85 | return true 86 | } 87 | } 88 | // default 89 | log.WithField("imagebase", image).Debug("support container image not found") 90 | return false 91 | } 92 | 93 | // SupportContainerName returns true if the supplied name is a known support 94 | // container name. 95 | func (sc *SupportContainer) SupportContainerName(name string) bool { 96 | if sc == nil { 97 | log.WithField("name", name).Debug("support container data not initialized") 98 | return false 99 | } 100 | 101 | for _, scname := range sc.ContainerNames { 102 | /* 103 | if strings.ToLower(scname) == strings.ToLower(name) { 104 | return true 105 | } 106 | */ 107 | if strings.Contains(strings.ToLower(name), strings.ToLower(scname)) { 108 | return true 109 | } 110 | } 111 | 112 | // default 113 | return false 114 | } 115 | 116 | // SupportContainerLabel returns true if the supplied name is a known support 117 | // container label 118 | func (sc *SupportContainer) SupportContainerLabel(label string) bool { 119 | if sc == nil { 120 | log.WithField("label", label).Debug("support container data not initiazed") 121 | return false 122 | } 123 | 124 | for _, sclabel := range sc.Labels { 125 | //if strings.ToLower(sclabel) == strings.ToLower(label) 126 | if strings.EqualFold(sclabel, label) { 127 | return true 128 | } 129 | } 130 | // default 131 | return false 132 | } 133 | 134 | // IsSupportContainer returns if the support container 135 | func (sc *SupportContainer) IsSupportContainer(ctr Container) bool { 136 | if sc.SupportContainerImage(ctr.ImageBase) { 137 | return true 138 | } 139 | 140 | if sc.SupportContainerName(ctr.Hostname) { 141 | return true 142 | } 143 | 144 | for k, v := range ctr.Labels { 145 | labelstring := fmt.Sprintf("%s=%s", k, v) 146 | if sc.SupportContainerLabel(labelstring) { 147 | return true 148 | } 149 | } 150 | 151 | // default 152 | return false 153 | } 154 | 155 | // JSON returns the data in json 156 | func (sc *SupportContainer) JSON() string { 157 | data, _ := json.MarshalIndent(sc, "", " ") 158 | return string(data) 159 | } 160 | -------------------------------------------------------------------------------- /explorers/task.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package explorers 18 | 19 | type Task struct { 20 | Namespace string 21 | Name string 22 | PID int 23 | ContainerType string 24 | Status string 25 | } 26 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/google/container-explorer 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.3 6 | 7 | require ( 8 | github.com/containerd/containerd v1.7.11 9 | github.com/opencontainers/go-digest v1.0.0 10 | github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b 11 | github.com/opencontainers/runtime-spec v1.1.0-rc.1 12 | github.com/sirupsen/logrus v1.9.3 13 | github.com/urfave/cli v1.22.12 14 | go.etcd.io/bbolt v1.3.7 15 | gopkg.in/yaml.v3 v3.0.1 16 | ) 17 | 18 | require ( 19 | github.com/Microsoft/go-winio v0.6.1 // indirect 20 | github.com/Microsoft/hcsshim v0.11.4 // indirect 21 | github.com/containerd/cgroups v1.1.0 // indirect 22 | github.com/containerd/continuity v0.4.2 // indirect 23 | github.com/containerd/log v0.1.0 // indirect 24 | github.com/containerd/ttrpc v1.2.2 // indirect 25 | github.com/containerd/typeurl/v2 v2.1.1 // indirect 26 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 27 | github.com/gogo/protobuf v1.3.2 // indirect 28 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 29 | github.com/golang/protobuf v1.5.3 // indirect 30 | github.com/google/go-cmp v0.6.0 // indirect 31 | github.com/klauspost/compress v1.16.0 // indirect 32 | github.com/moby/sys/mountinfo v0.6.2 // indirect 33 | github.com/pkg/errors v0.9.1 // indirect 34 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 35 | go.opencensus.io v0.24.0 // indirect 36 | golang.org/x/mod v0.17.0 // indirect 37 | golang.org/x/net v0.36.0 // indirect 38 | golang.org/x/sync v0.11.0 // indirect 39 | golang.org/x/sys v0.30.0 // indirect 40 | golang.org/x/text v0.22.0 // indirect 41 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect 42 | google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 // indirect 43 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect 44 | google.golang.org/grpc v1.58.3 // indirect 45 | google.golang.org/protobuf v1.33.0 // indirect 46 | ) 47 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 4 | github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= 5 | github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= 6 | github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8= 7 | github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w= 8 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 9 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 10 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 11 | github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= 12 | github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= 13 | github.com/containerd/containerd v1.7.11 h1:lfGKw3eU35sjV0aG2eYZTiwFEY1pCzxdzicHP3SZILw= 14 | github.com/containerd/containerd v1.7.11/go.mod h1:5UluHxHTX2rdvYuZ5OJTC5m/KJNs0Zs9wVoJm9zf5ZE= 15 | github.com/containerd/continuity v0.4.2 h1:v3y/4Yz5jwnvqPKJJ+7Wf93fyWoCB3F5EclWG023MDM= 16 | github.com/containerd/continuity v0.4.2/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= 17 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 18 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 19 | github.com/containerd/ttrpc v1.2.2 h1:9vqZr0pxwOF5koz6N0N3kJ0zDHokrcPxIR/ZR2YFtOs= 20 | github.com/containerd/ttrpc v1.2.2/go.mod h1:sIT6l32Ph/H9cvnJsfXM5drIVzTr5A2flTf1G5tYZak= 21 | github.com/containerd/typeurl/v2 v2.1.1 h1:3Q4Pt7i8nYwy2KmQWIw2+1hTvwTE/6w9FqcttATPO/4= 22 | github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0= 23 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 24 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 25 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 27 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 28 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 29 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 30 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 31 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 32 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 33 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 34 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 35 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 36 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 37 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 38 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 39 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 40 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 41 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 42 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 43 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 44 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 45 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 46 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 47 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 48 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 49 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 50 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 51 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 52 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 53 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 54 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 55 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 56 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 57 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 58 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 59 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 60 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 61 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 62 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 63 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 64 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 65 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 66 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 67 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 68 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 69 | github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= 70 | github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 71 | github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= 72 | github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= 73 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 74 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 75 | github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b h1:YWuSjZCQAPM8UUBLkYUk1e+rZcvWHJmFb6i6rM44Xs8= 76 | github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= 77 | github.com/opencontainers/runtime-spec v1.1.0-rc.1 h1:wHa9jroFfKGQqFHj0I1fMRKLl0pfj+ynAqBxo3v6u9w= 78 | github.com/opencontainers/runtime-spec v1.1.0-rc.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= 79 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 80 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 81 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 82 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 83 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 84 | github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 85 | github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= 86 | github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= 87 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 88 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 89 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 90 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 91 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 92 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 93 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 94 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 95 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 96 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 97 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 98 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 99 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 100 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 101 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 102 | github.com/urfave/cli v1.22.12 h1:igJgVw1JdKH+trcLWLeLwZjU9fEfPesQ+9/e4MQ44S8= 103 | github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= 104 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 105 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 106 | go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= 107 | go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= 108 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= 109 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 110 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 111 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 112 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 113 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 114 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 115 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 116 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 117 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 118 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 119 | golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= 120 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 121 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 122 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 123 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 124 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 125 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 126 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 127 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 128 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 129 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 130 | golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= 131 | golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= 132 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 133 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 134 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 135 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 136 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 137 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 138 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 139 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 140 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 141 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 142 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 143 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 144 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 145 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 146 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 147 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 148 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 149 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 150 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 151 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 152 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 153 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 154 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 155 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 156 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 157 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 158 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 159 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 160 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 161 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 162 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 163 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 164 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= 165 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 166 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 167 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 168 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 169 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 170 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 171 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 172 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 173 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 174 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 175 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 176 | google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 h1:Z0hjGZePRE0ZBWotvtrwxFNrNE9CUAGtplaDK5NNI/g= 177 | google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98/go.mod h1:S7mY02OqCJTD0E1OiQy1F72PWFB4bZJ87cAtLPYgDR0= 178 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U= 179 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= 180 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 181 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 182 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 183 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 184 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 185 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 186 | google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= 187 | google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= 188 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 189 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 190 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 191 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 192 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 193 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 194 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 195 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 196 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 197 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 198 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 199 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 200 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 201 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 202 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 203 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 204 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 205 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 206 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 207 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 208 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 209 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 210 | -------------------------------------------------------------------------------- /script/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # ContainerExplorer installation script 4 | # 5 | set -e 6 | SCRIPTNAME=$(basename "$0") 7 | 8 | CE_VER=0.3.1 9 | CE_PKG=container-explorer.tar.gz 10 | 11 | CE_DIR=/opt/container-explorer 12 | CE_BIN=${CE_DIR}/bin/ce 13 | CE_SUPPORT=${CE_DIR}/etc/supportcontainer.yaml 14 | 15 | TMP_DIR=$(mktemp -d -t ce-XXXXXXXXX) 16 | CE_TMP_DIR=${TMP_DIR}/container-explorer 17 | 18 | download_release() { 19 | echo "[+] Downloading container-explorer ${CE_PKG}" 20 | wget -P "${TMP_DIR}" https://github.com/google/container-explorer/releases/download/"${CE_VER}"/"${CE_PKG}" > /dev/null 2>&1 21 | tar -zxf "${TMP_DIR}"/container-explorer.tar.gz -C "${TMP_DIR}" > /dev/null 2>&1 22 | } 23 | 24 | 25 | install_release() { 26 | echo "[+] Installing contianer-explorer" 27 | if [ ! -d "${CE_DIR}" ]; then 28 | mkdir -p "${CE_DIR}"/bin 29 | mkdir -p "${CE_DIR}"/etc 30 | fi 31 | 32 | if [ -f "${CE_BIN}" ]; then 33 | rm -f "${CE_BIN}" 34 | fi 35 | 36 | if [ -f "${CE_SUPPORT}" ]; then 37 | rm -f "${CE_SUPPORT}" 38 | fi 39 | 40 | # Copy new binary and support file 41 | cp -f "${CE_TMP_DIR}"/ce "${CE_BIN}" 42 | cp -f "${CE_TMP_DIR}"/supportcontainer.yaml "${CE_SUPPORT}" 43 | } 44 | 45 | uninstall_release() { 46 | echo "[+] Uninstalling container-explorer" 47 | if [ -d "${CE_DIR}" ]; then 48 | rm -rf "${CE_DIR}" 49 | fi 50 | } 51 | 52 | 53 | cleanup() { 54 | echo "[+] Cleaning up..." 55 | rm -rf "${TMP_DIR}" > /dev/null 2>&1 56 | } 57 | 58 | check_root() { 59 | if [ "$(id -u)" -ne 0 ]; then 60 | echo "This script must be run as root. Use sudo ${SCRIPTNAME}" 61 | exit 1 62 | fi 63 | } 64 | 65 | case "$1" in 66 | install|upgrade) 67 | check_root 68 | download_release 69 | install_release 70 | cleanup 71 | echo "contianer-explorer installed at ${CE_DIR}" 72 | echo "For details use ${CE_BIN} -h" 73 | ;; 74 | remove|uninstall) 75 | check_root 76 | uninstall_release 77 | ;; 78 | *) 79 | echo "USAGE: ${SCRIPTNAME} {install|upgrade|remove}" >&2 80 | exit 2 81 | ;; 82 | esac 83 | 84 | exit 0 85 | -------------------------------------------------------------------------------- /supportcontainer.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | labels: 3 | - io.kubernetes.pod.namespace=kube-system 4 | -------------------------------------------------------------------------------- /utils/container.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package utils 18 | 19 | import "github.com/google/container-explorer/explorers" 20 | 21 | func IgnoreContainer(container explorers.Container, filter map[string]string) bool { 22 | ignore := false 23 | 24 | for k, v := range filter { 25 | containerLabel, ok := container.Labels[k] 26 | if !ok { 27 | // Container label does not exist. Check next label. 28 | continue 29 | } 30 | if containerLabel == v { 31 | ignore = true 32 | break 33 | } 34 | } 35 | 36 | return ignore 37 | } 38 | 39 | func IncludeContainer(container explorers.Container, filter map[string]string) bool { 40 | if filter == nil { 41 | return true 42 | } 43 | 44 | include := false 45 | 46 | for k, v := range filter { 47 | containerLabel, ok := container.Labels[k] 48 | if !ok { 49 | // Container label does not exist. Check next label. 50 | continue 51 | } 52 | if containerLabel == v { 53 | include = true 54 | break 55 | } 56 | } 57 | 58 | return include 59 | } -------------------------------------------------------------------------------- /utils/file.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package utils 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "io/fs" 23 | "math/rand" 24 | "os" 25 | "path/filepath" 26 | "time" 27 | ) 28 | 29 | // PathExists returns true of specified file or directory exists. 30 | // If symlink is provided, it returns error. 31 | func PathExists(path string) (bool, error) { 32 | _, err := os.Stat(path) 33 | if err == nil { 34 | return true, nil 35 | } 36 | 37 | if errors.Is(err, fs.ErrNotExist) { 38 | return false, nil 39 | } 40 | 41 | return false, err 42 | } 43 | 44 | const ( 45 | charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 46 | ) 47 | 48 | var seededRand *rand.Rand = rand.New( 49 | rand.NewSource(time.Now().UnixNano())) 50 | 51 | // GenerateRandomString creates a random string of a fixed length (6 characters). 52 | func GenerateRandomString(stringLength int) string { 53 | b := make([]byte, stringLength) 54 | for i := range b { 55 | b[i] = charset[seededRand.Intn(len(charset))] 56 | } 57 | return string(b) 58 | } 59 | 60 | func GetMountPoint() string { 61 | mountSuffix := GenerateRandomString(6) 62 | mountPoint := filepath.Join("/", "mnt", mountSuffix) 63 | return mountPoint 64 | } 65 | 66 | // CalculateDirectorySize calculates the total size in bytes of all regular files 67 | // within the given rootPath. It follows symbolic links for files to count the 68 | // target's size. It does not follow symbolic links for directories during traversal. 69 | // Broken symbolic links are skipped. 70 | func CalculateDirectorySize(rootPath string) (int64, error) { 71 | var totalSize int64 72 | 73 | // Ensure the rootPath is a directory and exists. 74 | // os.Stat follows symlinks, so if rootPath is a symlink to a directory, 75 | // rootInfo will be for the target directory. 76 | rootInfo, err := os.Stat(rootPath) 77 | if err != nil { 78 | return 0, fmt.Errorf("failed to stat root path %s: %w", rootPath, err) 79 | } 80 | if !rootInfo.IsDir() { 81 | return 0, fmt.Errorf("%s is not a directory", rootPath) 82 | } 83 | 84 | // filepath.WalkDir does not follow symbolic links to directories when recursing. 85 | // If rootPath itself is a symlink to a directory, WalkDir will follow it once. 86 | walkErr := filepath.WalkDir(rootPath, func(path string, d fs.DirEntry, err error) error { 87 | if err != nil { 88 | // This error is from WalkDir itself (e.g., permission denied reading a directory). 89 | // We'll return it to stop the walk. 90 | // You could choose to log this error and return nil to try to continue, 91 | // or return filepath.SkipDir if d is a directory. 92 | return fmt.Errorf("error accessing path %s: %w", path, err) 93 | } 94 | 95 | // We only care about files for sizing. Directories themselves don't add to the size. 96 | if d.IsDir() { 97 | return nil // Continue walking 98 | } 99 | 100 | // For non-directory entries (files, symlinks to files, etc.): 101 | // We use os.Stat(path) because it follows symlinks. 102 | // d.Info() would give info about the symlink itself, not its target. 103 | fileInfo, statErr := os.Stat(path) 104 | if statErr != nil { 105 | // If it's a broken symlink (os.IsNotExist error and entry is a symlink), skip it. 106 | if os.IsNotExist(statErr) && (d.Type()&fs.ModeSymlink != 0) { 107 | fmt.Fprintf(os.Stderr, "Warning: skipping broken symlink %s\n", path) 108 | return nil // Continue walking 109 | } 110 | // For other stat errors, stop the walk. 111 | return fmt.Errorf("failed to stat %s: %w", path, statErr) 112 | } 113 | 114 | // Add size if it's a regular file (or a symlink pointing to a regular file). 115 | if fileInfo.Mode().IsRegular() { 116 | totalSize += fileInfo.Size() 117 | } 118 | 119 | return nil // Continue walking 120 | }) 121 | 122 | if walkErr != nil { 123 | return 0, walkErr // Return the error that stopped the walk. 124 | } 125 | 126 | return totalSize, nil 127 | } --------------------------------------------------------------------------------