├── .github ├── dependabot.yml └── workflows │ └── ci.yaml ├── .gitignore ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── demo.gif ├── src ├── config.rs ├── lib.rs ├── main.rs └── runtime │ ├── container.rs │ ├── docker.rs │ ├── mod.rs │ └── podman.rs └── tests └── cli.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Lint 19 | run: cargo fmt && git diff --exit-code 20 | 21 | - uses: actions/checkout@v3 22 | - name: Clippy 23 | run: cargo clippy --fix && git diff --exit-code 24 | 25 | build: 26 | needs: lint 27 | runs-on: ubuntu-latest 28 | 29 | steps: 30 | - uses: actions/checkout@v3 31 | - name: Build 32 | run: cargo build --verbose 33 | 34 | e2e-docker: 35 | needs: build 36 | runs-on: ubuntu-latest 37 | 38 | steps: 39 | - uses: actions/checkout@v3 40 | - name: Run e2e tests with docker tests 41 | run: cargo test --verbose 42 | 43 | e2e-podman: 44 | needs: build 45 | runs-on: ubuntu-latest 46 | 47 | steps: 48 | - uses: actions/checkout@v3 49 | - name: Run podman tests 50 | run: | 51 | sudo systemctl stop docker 52 | sudo systemctl stop docker.socket 53 | echo "podman info" 54 | podman info 55 | systemctl --user enable --now podman.socket 56 | cargo test --verbose 57 | 58 | 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | .idea 4 | 5 | .DS_Store 6 | 7 | release.sh 8 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # CODEOWNERS 2 | * @exdx @tylerslaton @timflannagan 3 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Scope 40 | 41 | This Code of Conduct applies within all community spaces, and also applies when 42 | an individual is officially representing the community in public spaces. 43 | Examples of representing our community include using an official e-mail address, 44 | posting via an official social media account, or acting as an appointed 45 | representative at an online or offline event. 46 | 47 | ## Enforcement 48 | 49 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 50 | reported to the community leaders responsible for enforcement at 51 | denton24646@gmail.com. 52 | All complaints will be reviewed and investigated promptly and fairly. 53 | 54 | All community leaders are obligated to respect the privacy and security of the 55 | reporter of any incident. 56 | 57 | ## Attribution 58 | 59 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 60 | version 2.0, available at 61 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 62 | 63 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 64 | enforcement ladder](https://github.com/mozilla/diversity). 65 | 66 | [homepage]: https://www.contributor-covenant.org 67 | 68 | For answers to common questions about this code of conduct, see the FAQ at 69 | https://www.contributor-covenant.org/faq. Translations are available at 70 | https://www.contributor-covenant.org/translations. 71 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | I'm really glad you're reading this, thank you for your interest in contributing! 🙂 4 | 5 | The best way to communicate in this project is via GitHub issues. Issues are triaged frequently. 6 | 7 | ## Submitting changes 8 | 9 | The simplest way to submit a change is via a PR. For larger features, opening an issue first and discussing the feature would be preferred. 10 | 11 | Always write a clear log message for your commits. One-line messages are fine for small changes, but bigger changes should look like this: 12 | 13 | $ git commit -m "A brief summary of the commit 14 | > 15 | > A paragraph describing what changed and its impact." 16 | 17 | ## Coding conventions 18 | 19 | Running clippy or `cargo fmt` before pushing code is encouraged, since there is a lint GitHub Action that will fail if code is not formatted correctly. 20 | 21 | Beyond that, it's encouraged to write concise code: if something can be expressed in one line versus five, the one liner is preferred. 22 | 23 | ## Testing 24 | 25 | Testing is pretty straightforward. We have a series of e2e tests that run against both docker and podman runtimes. Running `cargo test` locally would enable the 26 | test suite to run against either of the runtimes available locally. 27 | 28 | Thank you for contributing! 29 | 30 | 31 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "adler" 7 | version = "1.0.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 10 | 11 | [[package]] 12 | name = "aho-corasick" 13 | version = "0.7.18" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 16 | dependencies = [ 17 | "memchr", 18 | ] 19 | 20 | [[package]] 21 | name = "ansi_term" 22 | version = "0.12.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 25 | dependencies = [ 26 | "winapi", 27 | ] 28 | 29 | [[package]] 30 | name = "anstyle" 31 | version = "1.0.0" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" 34 | 35 | [[package]] 36 | name = "anyhow" 37 | version = "1.0.71" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" 40 | 41 | [[package]] 42 | name = "assert_cmd" 43 | version = "2.0.5" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "d5c2ca00549910ec251e3bd15f87aeeb206c9456b9a77b43ff6c97c54042a472" 46 | dependencies = [ 47 | "bstr", 48 | "doc-comment", 49 | "predicates 2.1.5", 50 | "predicates-core", 51 | "predicates-tree", 52 | "wait-timeout", 53 | ] 54 | 55 | [[package]] 56 | name = "async-trait" 57 | version = "0.1.66" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "b84f9ebcc6c1f5b8cb160f6990096a5c127f423fcb6e1ccc46c370cbdfb75dfc" 60 | dependencies = [ 61 | "proc-macro2", 62 | "quote", 63 | "syn", 64 | ] 65 | 66 | [[package]] 67 | name = "atty" 68 | version = "0.2.14" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 71 | dependencies = [ 72 | "hermit-abi", 73 | "libc", 74 | "winapi", 75 | ] 76 | 77 | [[package]] 78 | name = "autocfg" 79 | version = "1.1.0" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 82 | 83 | [[package]] 84 | name = "base64" 85 | version = "0.13.0" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 88 | 89 | [[package]] 90 | name = "bitflags" 91 | version = "1.3.2" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 94 | 95 | [[package]] 96 | name = "bstr" 97 | version = "0.2.17" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" 100 | dependencies = [ 101 | "lazy_static", 102 | "memchr", 103 | "regex-automata", 104 | ] 105 | 106 | [[package]] 107 | name = "byteorder" 108 | version = "1.4.3" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 111 | 112 | [[package]] 113 | name = "bytes" 114 | version = "0.5.6" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" 117 | 118 | [[package]] 119 | name = "bytes" 120 | version = "1.1.0" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" 123 | 124 | [[package]] 125 | name = "cfg-if" 126 | version = "1.0.0" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 129 | 130 | [[package]] 131 | name = "chrono" 132 | version = "0.4.19" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" 135 | dependencies = [ 136 | "libc", 137 | "num-integer", 138 | "num-traits", 139 | "serde", 140 | "time", 141 | "winapi", 142 | ] 143 | 144 | [[package]] 145 | name = "clap" 146 | version = "2.34.0" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" 149 | dependencies = [ 150 | "ansi_term", 151 | "atty", 152 | "bitflags", 153 | "strsim", 154 | "textwrap", 155 | "unicode-width", 156 | "vec_map", 157 | ] 158 | 159 | [[package]] 160 | name = "containers-api" 161 | version = "0.2.0" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "2d2ca95f626f7be904f6ee24d0a525e7564c5c99544727229577f707dca42468" 164 | dependencies = [ 165 | "chrono", 166 | "flate2", 167 | "futures-util", 168 | "futures_codec", 169 | "http", 170 | "hyper", 171 | "hyperlocal", 172 | "log", 173 | "mime", 174 | "paste", 175 | "pin-project 1.0.11", 176 | "serde", 177 | "serde_json", 178 | "tar", 179 | "thiserror", 180 | "tokio", 181 | "url", 182 | ] 183 | 184 | [[package]] 185 | name = "containers-api-conn" 186 | version = "0.1.3" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "e3143a01be4d1a4675ac1841ea101dad3919bedfe23ba571ab64ab8e8cf3eaa1" 189 | dependencies = [ 190 | "futures-util", 191 | "futures_codec", 192 | "http", 193 | "hyper", 194 | "hyperlocal", 195 | "log", 196 | "mime", 197 | "pin-project 1.0.11", 198 | "serde", 199 | "serde_json", 200 | "thiserror", 201 | "tokio", 202 | "url", 203 | ] 204 | 205 | [[package]] 206 | name = "crc32fast" 207 | version = "1.3.2" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" 210 | dependencies = [ 211 | "cfg-if", 212 | ] 213 | 214 | [[package]] 215 | name = "dcp" 216 | version = "0.4.1" 217 | dependencies = [ 218 | "anyhow", 219 | "assert_cmd", 220 | "async-trait", 221 | "clap", 222 | "docker-api", 223 | "futures-util", 224 | "log", 225 | "podman-api", 226 | "predicates 3.0.3", 227 | "pretty_env_logger", 228 | "rand", 229 | "tar", 230 | "tokio", 231 | "xdg", 232 | ] 233 | 234 | [[package]] 235 | name = "difflib" 236 | version = "0.4.0" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" 239 | 240 | [[package]] 241 | name = "dirs" 242 | version = "4.0.0" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" 245 | dependencies = [ 246 | "dirs-sys", 247 | ] 248 | 249 | [[package]] 250 | name = "dirs-sys" 251 | version = "0.3.7" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" 254 | dependencies = [ 255 | "libc", 256 | "redox_users", 257 | "winapi", 258 | ] 259 | 260 | [[package]] 261 | name = "doc-comment" 262 | version = "0.3.3" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 265 | 266 | [[package]] 267 | name = "docker-api" 268 | version = "0.9.2" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "4ac0a69b1dc462b3a3b278d3f235d560282cb66eab2f0aae7fef0458b61d98fe" 271 | dependencies = [ 272 | "base64", 273 | "byteorder", 274 | "bytes 1.1.0", 275 | "chrono", 276 | "containers-api-conn", 277 | "flate2", 278 | "futures-util", 279 | "futures_codec", 280 | "http", 281 | "hyper", 282 | "hyperlocal", 283 | "log", 284 | "mime", 285 | "paste", 286 | "pin-project 1.0.11", 287 | "serde", 288 | "serde_json", 289 | "tar", 290 | "thiserror", 291 | "tokio", 292 | "url", 293 | ] 294 | 295 | [[package]] 296 | name = "either" 297 | version = "1.7.0" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be" 300 | 301 | [[package]] 302 | name = "env_logger" 303 | version = "0.7.1" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" 306 | dependencies = [ 307 | "atty", 308 | "humantime", 309 | "log", 310 | "regex", 311 | "termcolor", 312 | ] 313 | 314 | [[package]] 315 | name = "filetime" 316 | version = "0.2.17" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "e94a7bbaa59354bc20dd75b67f23e2797b4490e9d6928203fb105c79e448c86c" 319 | dependencies = [ 320 | "cfg-if", 321 | "libc", 322 | "redox_syscall", 323 | "windows-sys 0.36.1", 324 | ] 325 | 326 | [[package]] 327 | name = "flate2" 328 | version = "1.0.24" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" 331 | dependencies = [ 332 | "crc32fast", 333 | "miniz_oxide", 334 | ] 335 | 336 | [[package]] 337 | name = "float-cmp" 338 | version = "0.9.0" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" 341 | dependencies = [ 342 | "num-traits", 343 | ] 344 | 345 | [[package]] 346 | name = "fnv" 347 | version = "1.0.7" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 350 | 351 | [[package]] 352 | name = "form_urlencoded" 353 | version = "1.0.1" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" 356 | dependencies = [ 357 | "matches", 358 | "percent-encoding", 359 | ] 360 | 361 | [[package]] 362 | name = "futures" 363 | version = "0.3.21" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" 366 | dependencies = [ 367 | "futures-channel", 368 | "futures-core", 369 | "futures-executor", 370 | "futures-io", 371 | "futures-sink", 372 | "futures-task", 373 | "futures-util", 374 | ] 375 | 376 | [[package]] 377 | name = "futures-channel" 378 | version = "0.3.27" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "164713a5a0dcc3e7b4b1ed7d3b433cabc18025386f9339346e8daf15963cf7ac" 381 | dependencies = [ 382 | "futures-core", 383 | "futures-sink", 384 | ] 385 | 386 | [[package]] 387 | name = "futures-core" 388 | version = "0.3.27" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd" 391 | 392 | [[package]] 393 | name = "futures-executor" 394 | version = "0.3.21" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" 397 | dependencies = [ 398 | "futures-core", 399 | "futures-task", 400 | "futures-util", 401 | ] 402 | 403 | [[package]] 404 | name = "futures-io" 405 | version = "0.3.27" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "89d422fa3cbe3b40dca574ab087abb5bc98258ea57eea3fd6f1fa7162c778b91" 408 | 409 | [[package]] 410 | name = "futures-macro" 411 | version = "0.3.27" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "3eb14ed937631bd8b8b8977f2c198443447a8355b6e3ca599f38c975e5a963b6" 414 | dependencies = [ 415 | "proc-macro2", 416 | "quote", 417 | "syn", 418 | ] 419 | 420 | [[package]] 421 | name = "futures-sink" 422 | version = "0.3.27" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "ec93083a4aecafb2a80a885c9de1f0ccae9dbd32c2bb54b0c3a65690e0b8d2f2" 425 | 426 | [[package]] 427 | name = "futures-task" 428 | version = "0.3.27" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "fd65540d33b37b16542a0438c12e6aeead10d4ac5d05bd3f805b8f35ab592879" 431 | 432 | [[package]] 433 | name = "futures-util" 434 | version = "0.3.27" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab" 437 | dependencies = [ 438 | "futures-channel", 439 | "futures-core", 440 | "futures-io", 441 | "futures-macro", 442 | "futures-sink", 443 | "futures-task", 444 | "memchr", 445 | "pin-project-lite", 446 | "pin-utils", 447 | "slab", 448 | ] 449 | 450 | [[package]] 451 | name = "futures_codec" 452 | version = "0.4.1" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "ce54d63f8b0c75023ed920d46fd71d0cbbb830b0ee012726b5b4f506fb6dea5b" 455 | dependencies = [ 456 | "bytes 0.5.6", 457 | "futures", 458 | "memchr", 459 | "pin-project 0.4.30", 460 | ] 461 | 462 | [[package]] 463 | name = "getrandom" 464 | version = "0.2.7" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" 467 | dependencies = [ 468 | "cfg-if", 469 | "libc", 470 | "wasi 0.11.0+wasi-snapshot-preview1", 471 | ] 472 | 473 | [[package]] 474 | name = "hermit-abi" 475 | version = "0.1.19" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 478 | dependencies = [ 479 | "libc", 480 | ] 481 | 482 | [[package]] 483 | name = "hex" 484 | version = "0.4.3" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 487 | 488 | [[package]] 489 | name = "http" 490 | version = "0.2.8" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" 493 | dependencies = [ 494 | "bytes 1.1.0", 495 | "fnv", 496 | "itoa", 497 | ] 498 | 499 | [[package]] 500 | name = "http-body" 501 | version = "0.4.5" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" 504 | dependencies = [ 505 | "bytes 1.1.0", 506 | "http", 507 | "pin-project-lite", 508 | ] 509 | 510 | [[package]] 511 | name = "httparse" 512 | version = "1.7.1" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" 515 | 516 | [[package]] 517 | name = "httpdate" 518 | version = "1.0.2" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" 521 | 522 | [[package]] 523 | name = "humantime" 524 | version = "1.3.0" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" 527 | dependencies = [ 528 | "quick-error", 529 | ] 530 | 531 | [[package]] 532 | name = "hyper" 533 | version = "0.14.20" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" 536 | dependencies = [ 537 | "bytes 1.1.0", 538 | "futures-channel", 539 | "futures-core", 540 | "futures-util", 541 | "http", 542 | "http-body", 543 | "httparse", 544 | "httpdate", 545 | "itoa", 546 | "pin-project-lite", 547 | "socket2", 548 | "tokio", 549 | "tower-service", 550 | "tracing", 551 | "want", 552 | ] 553 | 554 | [[package]] 555 | name = "hyperlocal" 556 | version = "0.8.0" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "0fafdf7b2b2de7c9784f76e02c0935e65a8117ec3b768644379983ab333ac98c" 559 | dependencies = [ 560 | "futures-util", 561 | "hex", 562 | "hyper", 563 | "pin-project 1.0.11", 564 | "tokio", 565 | ] 566 | 567 | [[package]] 568 | name = "idna" 569 | version = "0.2.3" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" 572 | dependencies = [ 573 | "matches", 574 | "unicode-bidi", 575 | "unicode-normalization", 576 | ] 577 | 578 | [[package]] 579 | name = "itertools" 580 | version = "0.10.3" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" 583 | dependencies = [ 584 | "either", 585 | ] 586 | 587 | [[package]] 588 | name = "itoa" 589 | version = "1.0.2" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" 592 | 593 | [[package]] 594 | name = "lazy_static" 595 | version = "1.4.0" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 598 | 599 | [[package]] 600 | name = "libc" 601 | version = "0.2.126" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" 604 | 605 | [[package]] 606 | name = "lock_api" 607 | version = "0.4.7" 608 | source = "registry+https://github.com/rust-lang/crates.io-index" 609 | checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" 610 | dependencies = [ 611 | "autocfg", 612 | "scopeguard", 613 | ] 614 | 615 | [[package]] 616 | name = "log" 617 | version = "0.4.17" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 620 | dependencies = [ 621 | "cfg-if", 622 | ] 623 | 624 | [[package]] 625 | name = "matches" 626 | version = "0.1.9" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" 629 | 630 | [[package]] 631 | name = "memchr" 632 | version = "2.5.0" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 635 | 636 | [[package]] 637 | name = "mime" 638 | version = "0.3.16" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" 641 | 642 | [[package]] 643 | name = "miniz_oxide" 644 | version = "0.5.3" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" 647 | dependencies = [ 648 | "adler", 649 | ] 650 | 651 | [[package]] 652 | name = "mio" 653 | version = "0.8.4" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" 656 | dependencies = [ 657 | "libc", 658 | "log", 659 | "wasi 0.11.0+wasi-snapshot-preview1", 660 | "windows-sys 0.36.1", 661 | ] 662 | 663 | [[package]] 664 | name = "normalize-line-endings" 665 | version = "0.3.0" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" 668 | 669 | [[package]] 670 | name = "num-integer" 671 | version = "0.1.45" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 674 | dependencies = [ 675 | "autocfg", 676 | "num-traits", 677 | ] 678 | 679 | [[package]] 680 | name = "num-traits" 681 | version = "0.2.15" 682 | source = "registry+https://github.com/rust-lang/crates.io-index" 683 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 684 | dependencies = [ 685 | "autocfg", 686 | ] 687 | 688 | [[package]] 689 | name = "num_cpus" 690 | version = "1.13.1" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" 693 | dependencies = [ 694 | "hermit-abi", 695 | "libc", 696 | ] 697 | 698 | [[package]] 699 | name = "once_cell" 700 | version = "1.13.0" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" 703 | 704 | [[package]] 705 | name = "parking_lot" 706 | version = "0.12.1" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 709 | dependencies = [ 710 | "lock_api", 711 | "parking_lot_core", 712 | ] 713 | 714 | [[package]] 715 | name = "parking_lot_core" 716 | version = "0.9.3" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" 719 | dependencies = [ 720 | "cfg-if", 721 | "libc", 722 | "redox_syscall", 723 | "smallvec", 724 | "windows-sys 0.36.1", 725 | ] 726 | 727 | [[package]] 728 | name = "paste" 729 | version = "1.0.7" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" 732 | 733 | [[package]] 734 | name = "percent-encoding" 735 | version = "2.1.0" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 738 | 739 | [[package]] 740 | name = "pin-project" 741 | version = "0.4.30" 742 | source = "registry+https://github.com/rust-lang/crates.io-index" 743 | checksum = "3ef0f924a5ee7ea9cbcea77529dba45f8a9ba9f622419fe3386ca581a3ae9d5a" 744 | dependencies = [ 745 | "pin-project-internal 0.4.30", 746 | ] 747 | 748 | [[package]] 749 | name = "pin-project" 750 | version = "1.0.11" 751 | source = "registry+https://github.com/rust-lang/crates.io-index" 752 | checksum = "78203e83c48cffbe01e4a2d35d566ca4de445d79a85372fc64e378bfc812a260" 753 | dependencies = [ 754 | "pin-project-internal 1.0.11", 755 | ] 756 | 757 | [[package]] 758 | name = "pin-project-internal" 759 | version = "0.4.30" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "851c8d0ce9bebe43790dedfc86614c23494ac9f423dd618d3a61fc693eafe61e" 762 | dependencies = [ 763 | "proc-macro2", 764 | "quote", 765 | "syn", 766 | ] 767 | 768 | [[package]] 769 | name = "pin-project-internal" 770 | version = "1.0.11" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "710faf75e1b33345361201d36d04e98ac1ed8909151a017ed384700836104c74" 773 | dependencies = [ 774 | "proc-macro2", 775 | "quote", 776 | "syn", 777 | ] 778 | 779 | [[package]] 780 | name = "pin-project-lite" 781 | version = "0.2.9" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 784 | 785 | [[package]] 786 | name = "pin-utils" 787 | version = "0.1.0" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 790 | 791 | [[package]] 792 | name = "podman-api" 793 | version = "0.4.0" 794 | source = "registry+https://github.com/rust-lang/crates.io-index" 795 | checksum = "852a469c27bc8e6a403f1bd73288f241b3e813fe837b749ab87d03ef8f7ca147" 796 | dependencies = [ 797 | "base64", 798 | "byteorder", 799 | "bytes 1.1.0", 800 | "chrono", 801 | "containers-api", 802 | "flate2", 803 | "futures-util", 804 | "futures_codec", 805 | "log", 806 | "paste", 807 | "podman-api-stubs", 808 | "serde", 809 | "serde_json", 810 | "tar", 811 | "thiserror", 812 | "tokio", 813 | "url", 814 | ] 815 | 816 | [[package]] 817 | name = "podman-api-stubs" 818 | version = "0.5.0" 819 | source = "registry+https://github.com/rust-lang/crates.io-index" 820 | checksum = "fd2c5fdd235733e79a8570997ef664788a9f877899d0e7ae6e17acbd40b11413" 821 | dependencies = [ 822 | "chrono", 823 | "serde", 824 | "serde_json", 825 | ] 826 | 827 | [[package]] 828 | name = "ppv-lite86" 829 | version = "0.2.16" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" 832 | 833 | [[package]] 834 | name = "predicates" 835 | version = "2.1.5" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" 838 | dependencies = [ 839 | "difflib", 840 | "itertools", 841 | "predicates-core", 842 | ] 843 | 844 | [[package]] 845 | name = "predicates" 846 | version = "3.0.3" 847 | source = "registry+https://github.com/rust-lang/crates.io-index" 848 | checksum = "09963355b9f467184c04017ced4a2ba2d75cbcb4e7462690d388233253d4b1a9" 849 | dependencies = [ 850 | "anstyle", 851 | "difflib", 852 | "float-cmp", 853 | "itertools", 854 | "normalize-line-endings", 855 | "predicates-core", 856 | "regex", 857 | ] 858 | 859 | [[package]] 860 | name = "predicates-core" 861 | version = "1.0.3" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "da1c2388b1513e1b605fcec39a95e0a9e8ef088f71443ef37099fa9ae6673fcb" 864 | 865 | [[package]] 866 | name = "predicates-tree" 867 | version = "1.0.5" 868 | source = "registry+https://github.com/rust-lang/crates.io-index" 869 | checksum = "4d86de6de25020a36c6d3643a86d9a6a9f552107c0559c60ea03551b5e16c032" 870 | dependencies = [ 871 | "predicates-core", 872 | "termtree", 873 | ] 874 | 875 | [[package]] 876 | name = "pretty_env_logger" 877 | version = "0.4.0" 878 | source = "registry+https://github.com/rust-lang/crates.io-index" 879 | checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d" 880 | dependencies = [ 881 | "env_logger", 882 | "log", 883 | ] 884 | 885 | [[package]] 886 | name = "proc-macro2" 887 | version = "1.0.40" 888 | source = "registry+https://github.com/rust-lang/crates.io-index" 889 | checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" 890 | dependencies = [ 891 | "unicode-ident", 892 | ] 893 | 894 | [[package]] 895 | name = "quick-error" 896 | version = "1.2.3" 897 | source = "registry+https://github.com/rust-lang/crates.io-index" 898 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 899 | 900 | [[package]] 901 | name = "quote" 902 | version = "1.0.20" 903 | source = "registry+https://github.com/rust-lang/crates.io-index" 904 | checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" 905 | dependencies = [ 906 | "proc-macro2", 907 | ] 908 | 909 | [[package]] 910 | name = "rand" 911 | version = "0.8.5" 912 | source = "registry+https://github.com/rust-lang/crates.io-index" 913 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 914 | dependencies = [ 915 | "libc", 916 | "rand_chacha", 917 | "rand_core", 918 | ] 919 | 920 | [[package]] 921 | name = "rand_chacha" 922 | version = "0.3.1" 923 | source = "registry+https://github.com/rust-lang/crates.io-index" 924 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 925 | dependencies = [ 926 | "ppv-lite86", 927 | "rand_core", 928 | ] 929 | 930 | [[package]] 931 | name = "rand_core" 932 | version = "0.6.3" 933 | source = "registry+https://github.com/rust-lang/crates.io-index" 934 | checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" 935 | dependencies = [ 936 | "getrandom", 937 | ] 938 | 939 | [[package]] 940 | name = "redox_syscall" 941 | version = "0.2.13" 942 | source = "registry+https://github.com/rust-lang/crates.io-index" 943 | checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" 944 | dependencies = [ 945 | "bitflags", 946 | ] 947 | 948 | [[package]] 949 | name = "redox_users" 950 | version = "0.4.3" 951 | source = "registry+https://github.com/rust-lang/crates.io-index" 952 | checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" 953 | dependencies = [ 954 | "getrandom", 955 | "redox_syscall", 956 | "thiserror", 957 | ] 958 | 959 | [[package]] 960 | name = "regex" 961 | version = "1.6.0" 962 | source = "registry+https://github.com/rust-lang/crates.io-index" 963 | checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" 964 | dependencies = [ 965 | "aho-corasick", 966 | "memchr", 967 | "regex-syntax", 968 | ] 969 | 970 | [[package]] 971 | name = "regex-automata" 972 | version = "0.1.10" 973 | source = "registry+https://github.com/rust-lang/crates.io-index" 974 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 975 | 976 | [[package]] 977 | name = "regex-syntax" 978 | version = "0.6.27" 979 | source = "registry+https://github.com/rust-lang/crates.io-index" 980 | checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" 981 | 982 | [[package]] 983 | name = "ryu" 984 | version = "1.0.10" 985 | source = "registry+https://github.com/rust-lang/crates.io-index" 986 | checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" 987 | 988 | [[package]] 989 | name = "scopeguard" 990 | version = "1.1.0" 991 | source = "registry+https://github.com/rust-lang/crates.io-index" 992 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 993 | 994 | [[package]] 995 | name = "serde" 996 | version = "1.0.139" 997 | source = "registry+https://github.com/rust-lang/crates.io-index" 998 | checksum = "0171ebb889e45aa68b44aee0859b3eede84c6f5f5c228e6f140c0b2a0a46cad6" 999 | dependencies = [ 1000 | "serde_derive", 1001 | ] 1002 | 1003 | [[package]] 1004 | name = "serde_derive" 1005 | version = "1.0.139" 1006 | source = "registry+https://github.com/rust-lang/crates.io-index" 1007 | checksum = "dc1d3230c1de7932af58ad8ffbe1d784bd55efd5a9d84ac24f69c72d83543dfb" 1008 | dependencies = [ 1009 | "proc-macro2", 1010 | "quote", 1011 | "syn", 1012 | ] 1013 | 1014 | [[package]] 1015 | name = "serde_json" 1016 | version = "1.0.82" 1017 | source = "registry+https://github.com/rust-lang/crates.io-index" 1018 | checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" 1019 | dependencies = [ 1020 | "itoa", 1021 | "ryu", 1022 | "serde", 1023 | ] 1024 | 1025 | [[package]] 1026 | name = "signal-hook-registry" 1027 | version = "1.4.0" 1028 | source = "registry+https://github.com/rust-lang/crates.io-index" 1029 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" 1030 | dependencies = [ 1031 | "libc", 1032 | ] 1033 | 1034 | [[package]] 1035 | name = "slab" 1036 | version = "0.4.6" 1037 | source = "registry+https://github.com/rust-lang/crates.io-index" 1038 | checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" 1039 | 1040 | [[package]] 1041 | name = "smallvec" 1042 | version = "1.9.0" 1043 | source = "registry+https://github.com/rust-lang/crates.io-index" 1044 | checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" 1045 | 1046 | [[package]] 1047 | name = "socket2" 1048 | version = "0.4.4" 1049 | source = "registry+https://github.com/rust-lang/crates.io-index" 1050 | checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" 1051 | dependencies = [ 1052 | "libc", 1053 | "winapi", 1054 | ] 1055 | 1056 | [[package]] 1057 | name = "strsim" 1058 | version = "0.8.0" 1059 | source = "registry+https://github.com/rust-lang/crates.io-index" 1060 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 1061 | 1062 | [[package]] 1063 | name = "syn" 1064 | version = "1.0.98" 1065 | source = "registry+https://github.com/rust-lang/crates.io-index" 1066 | checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" 1067 | dependencies = [ 1068 | "proc-macro2", 1069 | "quote", 1070 | "unicode-ident", 1071 | ] 1072 | 1073 | [[package]] 1074 | name = "tar" 1075 | version = "0.4.38" 1076 | source = "registry+https://github.com/rust-lang/crates.io-index" 1077 | checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6" 1078 | dependencies = [ 1079 | "filetime", 1080 | "libc", 1081 | "xattr", 1082 | ] 1083 | 1084 | [[package]] 1085 | name = "termcolor" 1086 | version = "1.1.3" 1087 | source = "registry+https://github.com/rust-lang/crates.io-index" 1088 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 1089 | dependencies = [ 1090 | "winapi-util", 1091 | ] 1092 | 1093 | [[package]] 1094 | name = "termtree" 1095 | version = "0.2.4" 1096 | source = "registry+https://github.com/rust-lang/crates.io-index" 1097 | checksum = "507e9898683b6c43a9aa55b64259b721b52ba226e0f3779137e50ad114a4c90b" 1098 | 1099 | [[package]] 1100 | name = "textwrap" 1101 | version = "0.11.0" 1102 | source = "registry+https://github.com/rust-lang/crates.io-index" 1103 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 1104 | dependencies = [ 1105 | "unicode-width", 1106 | ] 1107 | 1108 | [[package]] 1109 | name = "thiserror" 1110 | version = "1.0.31" 1111 | source = "registry+https://github.com/rust-lang/crates.io-index" 1112 | checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" 1113 | dependencies = [ 1114 | "thiserror-impl", 1115 | ] 1116 | 1117 | [[package]] 1118 | name = "thiserror-impl" 1119 | version = "1.0.31" 1120 | source = "registry+https://github.com/rust-lang/crates.io-index" 1121 | checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" 1122 | dependencies = [ 1123 | "proc-macro2", 1124 | "quote", 1125 | "syn", 1126 | ] 1127 | 1128 | [[package]] 1129 | name = "time" 1130 | version = "0.1.44" 1131 | source = "registry+https://github.com/rust-lang/crates.io-index" 1132 | checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" 1133 | dependencies = [ 1134 | "libc", 1135 | "wasi 0.10.0+wasi-snapshot-preview1", 1136 | "winapi", 1137 | ] 1138 | 1139 | [[package]] 1140 | name = "tinyvec" 1141 | version = "1.6.0" 1142 | source = "registry+https://github.com/rust-lang/crates.io-index" 1143 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 1144 | dependencies = [ 1145 | "tinyvec_macros", 1146 | ] 1147 | 1148 | [[package]] 1149 | name = "tinyvec_macros" 1150 | version = "0.1.0" 1151 | source = "registry+https://github.com/rust-lang/crates.io-index" 1152 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 1153 | 1154 | [[package]] 1155 | name = "tokio" 1156 | version = "1.26.0" 1157 | source = "registry+https://github.com/rust-lang/crates.io-index" 1158 | checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64" 1159 | dependencies = [ 1160 | "autocfg", 1161 | "bytes 1.1.0", 1162 | "libc", 1163 | "memchr", 1164 | "mio", 1165 | "num_cpus", 1166 | "parking_lot", 1167 | "pin-project-lite", 1168 | "signal-hook-registry", 1169 | "socket2", 1170 | "tokio-macros", 1171 | "windows-sys 0.45.0", 1172 | ] 1173 | 1174 | [[package]] 1175 | name = "tokio-macros" 1176 | version = "1.8.0" 1177 | source = "registry+https://github.com/rust-lang/crates.io-index" 1178 | checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" 1179 | dependencies = [ 1180 | "proc-macro2", 1181 | "quote", 1182 | "syn", 1183 | ] 1184 | 1185 | [[package]] 1186 | name = "tower-service" 1187 | version = "0.3.2" 1188 | source = "registry+https://github.com/rust-lang/crates.io-index" 1189 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" 1190 | 1191 | [[package]] 1192 | name = "tracing" 1193 | version = "0.1.35" 1194 | source = "registry+https://github.com/rust-lang/crates.io-index" 1195 | checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160" 1196 | dependencies = [ 1197 | "cfg-if", 1198 | "pin-project-lite", 1199 | "tracing-core", 1200 | ] 1201 | 1202 | [[package]] 1203 | name = "tracing-core" 1204 | version = "0.1.28" 1205 | source = "registry+https://github.com/rust-lang/crates.io-index" 1206 | checksum = "7b7358be39f2f274f322d2aaed611acc57f382e8eb1e5b48cb9ae30933495ce7" 1207 | dependencies = [ 1208 | "once_cell", 1209 | ] 1210 | 1211 | [[package]] 1212 | name = "try-lock" 1213 | version = "0.2.3" 1214 | source = "registry+https://github.com/rust-lang/crates.io-index" 1215 | checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" 1216 | 1217 | [[package]] 1218 | name = "unicode-bidi" 1219 | version = "0.3.8" 1220 | source = "registry+https://github.com/rust-lang/crates.io-index" 1221 | checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" 1222 | 1223 | [[package]] 1224 | name = "unicode-ident" 1225 | version = "1.0.1" 1226 | source = "registry+https://github.com/rust-lang/crates.io-index" 1227 | checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" 1228 | 1229 | [[package]] 1230 | name = "unicode-normalization" 1231 | version = "0.1.21" 1232 | source = "registry+https://github.com/rust-lang/crates.io-index" 1233 | checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" 1234 | dependencies = [ 1235 | "tinyvec", 1236 | ] 1237 | 1238 | [[package]] 1239 | name = "unicode-width" 1240 | version = "0.1.9" 1241 | source = "registry+https://github.com/rust-lang/crates.io-index" 1242 | checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" 1243 | 1244 | [[package]] 1245 | name = "url" 1246 | version = "2.2.2" 1247 | source = "registry+https://github.com/rust-lang/crates.io-index" 1248 | checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" 1249 | dependencies = [ 1250 | "form_urlencoded", 1251 | "idna", 1252 | "matches", 1253 | "percent-encoding", 1254 | ] 1255 | 1256 | [[package]] 1257 | name = "vec_map" 1258 | version = "0.8.2" 1259 | source = "registry+https://github.com/rust-lang/crates.io-index" 1260 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 1261 | 1262 | [[package]] 1263 | name = "wait-timeout" 1264 | version = "0.2.0" 1265 | source = "registry+https://github.com/rust-lang/crates.io-index" 1266 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 1267 | dependencies = [ 1268 | "libc", 1269 | ] 1270 | 1271 | [[package]] 1272 | name = "want" 1273 | version = "0.3.0" 1274 | source = "registry+https://github.com/rust-lang/crates.io-index" 1275 | checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" 1276 | dependencies = [ 1277 | "log", 1278 | "try-lock", 1279 | ] 1280 | 1281 | [[package]] 1282 | name = "wasi" 1283 | version = "0.10.0+wasi-snapshot-preview1" 1284 | source = "registry+https://github.com/rust-lang/crates.io-index" 1285 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 1286 | 1287 | [[package]] 1288 | name = "wasi" 1289 | version = "0.11.0+wasi-snapshot-preview1" 1290 | source = "registry+https://github.com/rust-lang/crates.io-index" 1291 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1292 | 1293 | [[package]] 1294 | name = "winapi" 1295 | version = "0.3.9" 1296 | source = "registry+https://github.com/rust-lang/crates.io-index" 1297 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1298 | dependencies = [ 1299 | "winapi-i686-pc-windows-gnu", 1300 | "winapi-x86_64-pc-windows-gnu", 1301 | ] 1302 | 1303 | [[package]] 1304 | name = "winapi-i686-pc-windows-gnu" 1305 | version = "0.4.0" 1306 | source = "registry+https://github.com/rust-lang/crates.io-index" 1307 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1308 | 1309 | [[package]] 1310 | name = "winapi-util" 1311 | version = "0.1.5" 1312 | source = "registry+https://github.com/rust-lang/crates.io-index" 1313 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 1314 | dependencies = [ 1315 | "winapi", 1316 | ] 1317 | 1318 | [[package]] 1319 | name = "winapi-x86_64-pc-windows-gnu" 1320 | version = "0.4.0" 1321 | source = "registry+https://github.com/rust-lang/crates.io-index" 1322 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1323 | 1324 | [[package]] 1325 | name = "windows-sys" 1326 | version = "0.36.1" 1327 | source = "registry+https://github.com/rust-lang/crates.io-index" 1328 | checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" 1329 | dependencies = [ 1330 | "windows_aarch64_msvc 0.36.1", 1331 | "windows_i686_gnu 0.36.1", 1332 | "windows_i686_msvc 0.36.1", 1333 | "windows_x86_64_gnu 0.36.1", 1334 | "windows_x86_64_msvc 0.36.1", 1335 | ] 1336 | 1337 | [[package]] 1338 | name = "windows-sys" 1339 | version = "0.45.0" 1340 | source = "registry+https://github.com/rust-lang/crates.io-index" 1341 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 1342 | dependencies = [ 1343 | "windows-targets", 1344 | ] 1345 | 1346 | [[package]] 1347 | name = "windows-targets" 1348 | version = "0.42.1" 1349 | source = "registry+https://github.com/rust-lang/crates.io-index" 1350 | checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" 1351 | dependencies = [ 1352 | "windows_aarch64_gnullvm", 1353 | "windows_aarch64_msvc 0.42.1", 1354 | "windows_i686_gnu 0.42.1", 1355 | "windows_i686_msvc 0.42.1", 1356 | "windows_x86_64_gnu 0.42.1", 1357 | "windows_x86_64_gnullvm", 1358 | "windows_x86_64_msvc 0.42.1", 1359 | ] 1360 | 1361 | [[package]] 1362 | name = "windows_aarch64_gnullvm" 1363 | version = "0.42.1" 1364 | source = "registry+https://github.com/rust-lang/crates.io-index" 1365 | checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" 1366 | 1367 | [[package]] 1368 | name = "windows_aarch64_msvc" 1369 | version = "0.36.1" 1370 | source = "registry+https://github.com/rust-lang/crates.io-index" 1371 | checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" 1372 | 1373 | [[package]] 1374 | name = "windows_aarch64_msvc" 1375 | version = "0.42.1" 1376 | source = "registry+https://github.com/rust-lang/crates.io-index" 1377 | checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" 1378 | 1379 | [[package]] 1380 | name = "windows_i686_gnu" 1381 | version = "0.36.1" 1382 | source = "registry+https://github.com/rust-lang/crates.io-index" 1383 | checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" 1384 | 1385 | [[package]] 1386 | name = "windows_i686_gnu" 1387 | version = "0.42.1" 1388 | source = "registry+https://github.com/rust-lang/crates.io-index" 1389 | checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" 1390 | 1391 | [[package]] 1392 | name = "windows_i686_msvc" 1393 | version = "0.36.1" 1394 | source = "registry+https://github.com/rust-lang/crates.io-index" 1395 | checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" 1396 | 1397 | [[package]] 1398 | name = "windows_i686_msvc" 1399 | version = "0.42.1" 1400 | source = "registry+https://github.com/rust-lang/crates.io-index" 1401 | checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" 1402 | 1403 | [[package]] 1404 | name = "windows_x86_64_gnu" 1405 | version = "0.36.1" 1406 | source = "registry+https://github.com/rust-lang/crates.io-index" 1407 | checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" 1408 | 1409 | [[package]] 1410 | name = "windows_x86_64_gnu" 1411 | version = "0.42.1" 1412 | source = "registry+https://github.com/rust-lang/crates.io-index" 1413 | checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" 1414 | 1415 | [[package]] 1416 | name = "windows_x86_64_gnullvm" 1417 | version = "0.42.1" 1418 | source = "registry+https://github.com/rust-lang/crates.io-index" 1419 | checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" 1420 | 1421 | [[package]] 1422 | name = "windows_x86_64_msvc" 1423 | version = "0.36.1" 1424 | source = "registry+https://github.com/rust-lang/crates.io-index" 1425 | checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" 1426 | 1427 | [[package]] 1428 | name = "windows_x86_64_msvc" 1429 | version = "0.42.1" 1430 | source = "registry+https://github.com/rust-lang/crates.io-index" 1431 | checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" 1432 | 1433 | [[package]] 1434 | name = "xattr" 1435 | version = "0.2.3" 1436 | source = "registry+https://github.com/rust-lang/crates.io-index" 1437 | checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" 1438 | dependencies = [ 1439 | "libc", 1440 | ] 1441 | 1442 | [[package]] 1443 | name = "xdg" 1444 | version = "2.4.1" 1445 | source = "registry+https://github.com/rust-lang/crates.io-index" 1446 | checksum = "0c4583db5cbd4c4c0303df2d15af80f0539db703fa1c68802d4cbbd2dd0f88f6" 1447 | dependencies = [ 1448 | "dirs", 1449 | ] 1450 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dcp" 3 | version = "0.4.1" 4 | edition = "2021" 5 | license = "MIT" 6 | description = "A utility tool to copy container filesystems easily" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | clap = "2.33" 12 | docker-api = "0.9.1" 13 | tokio = { version = "1.26", features = ["full"] } 14 | futures-util = "0.3" 15 | tar = "0.4" 16 | log = "0.4" 17 | pretty_env_logger = "0.4" 18 | anyhow = "1.0" 19 | podman-api = "0.4" 20 | xdg = "^2.1" 21 | async-trait = "0.1.66" 22 | 23 | [dev-dependencies] 24 | predicates = "3.0.3" 25 | assert_cmd = "2.0.5" 26 | rand = "0.8.5" 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 exdx 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dcp: docker cp made easy 2 | 3 | [![GitHub Actions](https://github.com/exdx/dcp/workflows/ci/badge.svg)](https://github.com/exdx/dcp/actions) 4 | [![Latest version](https://img.shields.io/crates/v/dcp.svg)](https://crates.io/crates/dcp) 5 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) 6 | 7 | ## Summary 8 | 9 | Containers are great tools that can encapsulate an application and its dependencies, 10 | allowing apps to run anywhere in a streamlined way. Some container images contain 11 | commands to start a long-lived binary, whereas others may simply contain data 12 | that needs to be available in the environment (for example, a Kubernetes cluster). 13 | For example, [operator-framework bundles](https://olm.operatorframework.io/docs/tasks/creating-operator-bundle/) and [crossplane packages](https://crossplane.io/docs/v1.9/concepts/packages.html) both use 14 | container images to store Kubernetes manifests. These manifests are unpacked and applied to the cluster. 15 | 16 | One of the downsides of using container images to store data is that they are 17 | opaque. There's no way to quickly tell what's inside the image, although 18 | the hash digest is useful in seeing whether the image has changed from a previous 19 | version. The options are to use `docker cp` or something similar using podman 20 | or containerd. 21 | 22 | Using `docker cp` by itself can be cumbersome. Say you have a remote image 23 | somewhere in a registry. You have to pull the image, create a container from that 24 | image, and only then run `docker cp ` using an unintuitive syntax for selecting 25 | what should be copied to the local filesystem. 26 | 27 | dcp is a simple binary that simplifies this workflow. A user can simply 28 | say `dcp ` and dcp can extract the contents of that image onto the 29 | local filesystem. From there, users are free to view and edit the files locally. Any OCI-based image is supported. 30 | 31 | ![Demo](demo.gif) 32 | 33 | ## Installing 34 | 35 | ### Installing from crates.io 36 | 37 | If you're a Rust programmer and have Rust installed locally, you can install dcp 38 | by simply entering `cargo install dcp`, which will fetch the latest version from 39 | crates.io. 40 | dcp relies on the stable Rust toolchain. 41 | 42 | ### Download compiled binary 43 | 44 | The [release section](https://github.com/exdx/dcp/releases) has a number 45 | of precompiled versions of dcp for different platforms. Linux, macOS, and Windows (experimental) 46 | binaries are pre-built. For MacOS, both arm and x86 targets are provided, and 47 | for Linux only x86 is provided. If your system is not supported, building dcp from 48 | the source is straightforward. 49 | 50 | ### Build from source 51 | 52 | To build from source, ensure that you have the rust toolchain installed locally. 53 | This project does not rely on nightly and uses the 1.62-stable toolchain. 54 | Clone the repository and run `cargo build --release` to build a release version 55 | of the binary. From there, you can move the binary to a folder on your $PATH to access 56 | it easily. 57 | 58 | ## Implementation 59 | 60 | Because there wasn't a suitable `containerd` client implementation in Rust at the time 61 | of writing, dcp relies on APIs provided by external docker and podman crates. This limits dcp to working on systems where docker or podman is the container runtime. 62 | 63 | By default, dcp will look for an active docker socket to connect to at the standard path. If the docker socket is unavailable, dcp will fallback to the current user's podman socket based on the $XDG_RUNTIME_DIR environment variable. 64 | 65 | If the docker socket is on a remote host, or in a custom location, use the `-s` flag with the path to the custom socket. 66 | 67 | ## Flags and Examples 68 | 69 | By default, dcp will copy content to the current working directory. For example, lets try issuing the following command: 70 | 71 | ``` 72 | $ dcp tyslaton/sample-catalog:v0.0.4 -c configs 73 | ``` 74 | 75 | This command will copy the `configs` directory (specified via the `-c` flag) from the image to the current directory. 76 | 77 | For further configuration, lets try: 78 | 79 | ``` 80 | $ dcp tyslaton/sample-catalog:v0.0.4 -d output -c configs 81 | ``` 82 | 83 | This command pulls down the requested image, only extracting 84 | the `configs` directory and copying it to the `output` directory 85 | locally (specified via the `-d` flag). If `output` does not exist locally, 86 | it will be created as part of the process. 87 | 88 | Another example, for copying only the manifests directory: 89 | 90 | ``` 91 | $ dcp quay.io/tflannag/bundles:resolveset-v0.0.2 -c manifests 92 | ``` 93 | 94 | Lastly, we can copy from a private image by providing a username 95 | and password (specified via the `-u` and `-p` flags). 96 | 97 | ``` 98 | $ dcp quay.io/tyslaton/sample-catalog-private:latest -u -p 99 | ``` 100 | 101 | > :warning: This serves as a convenient way to copy contents from a private image 102 | but is insecure as your registry credentials are saved in 103 | your shell history. If you would like to be completely secure then 104 | login via ` login` and pull the image first. dcp 105 | will then be able to find the image locally and process it. 106 | 107 | ## FAQ 108 | 109 | **Q**: I hit an unexpected error unpacking the root filesystem of an image: `trying to unpack outside of destination path`. How can I avoid this? 110 | 111 | **A**: dcp relies on the underlying `tar` Rust library to unpack the image filesystem represented as a tar file. The [unpack](https://docs.rs/tar/latest/tar/struct.Archive.html#method.unpack) method is sensitive in that it will not write files outside of the path specified by the destination. So things like symlinks will cause errors when unpacking. Whenever possible, use the `-c` flag to specify a directory to unpack, instead of the container filesystem root, to avoid this error. 112 | 113 | ------------------ 114 | **Q**: I would like to use dcp to pull content from an image but I don't know where in the image the content is stored. Is there an `ls` command or similar functionality in dcp? 115 | 116 | **A**: Checkout the excellent [dive tool](https://github.com/wagoodman/dive) to easily explore a container filesystem by layer. After finding the path of the files to copy, you can then use dcp to extract just those specific files. 117 | 118 | ------------------ 119 | **Q**: Is dcp supported on Windows? 120 | 121 | **A**: Yes, dcp is supported on Windows. Windows support is experimental, as there is no CI coverage, but it will likely work in your windows environment. The only non-default change you need to make is to expose the docker daemon so that dcp can connect to it. This can be done through one of two ways: 122 | 123 | 1. Adding the following to your `%userprofile%\.docker\daemon.json` file. 124 | ```json 125 | { 126 | "hosts": ["tcp://0.0.0.0:2375"] 127 | } 128 | ``` 129 | 130 | 2. Going through the Docker Desktop UI and enabling the setting for `Expose daemon on tcp://localhost:2375 without TLS` under `General`. 131 | 132 | 133 | ------------------ 134 | **Q**: I would like to inspect image labels to figure out where in the filesystem I should copy from. Does dcp have an `inspect` command to list image labels? 135 | 136 | **A**: Listing an image's labels can be done easily using the underlying container runtime. For example, run `docker image inspect | grep Labels` to see labels attached to an image. From there, dcp can be used to copy files from the container filesystem. 137 | 138 | ## Testing 139 | 140 | If you would like to run the test suite, you just need to run the standard cargo command. This will run all relevant 141 | unit, integration and documentation tests. 142 | 143 | ``` 144 | $ cargo test 145 | ``` 146 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exdx/dcp/06a60d46ea088d634dee0e8c6de1234d9efdfd3b/demo.gif -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use clap::{App, Arg}; 3 | 4 | use crate::runtime; 5 | 6 | pub const VERSION: &str = "0.4.1"; 7 | 8 | #[derive(Debug)] 9 | pub struct Config { 10 | // List of images 11 | pub image: String, 12 | // Where the download files should be saved on the filesystem. Default "." 13 | pub download_path: String, 14 | // Where the content (files) are in the container filesystem. Default "/" 15 | pub content_path: String, 16 | // Option to write to stdout instead of the local filesystem. 17 | pub write_to_stdout: bool, 18 | // What level of logs to output 19 | pub log_level: String, 20 | // Username for singing into a private registry 21 | pub username: String, 22 | // Password for signing into a private registry 23 | pub password: String, 24 | // Force a pull even if the image is present locally 25 | pub force_pull: bool, 26 | // Specify a custom socket to utilize for the runtime 27 | pub socket: String, 28 | } 29 | 30 | pub fn get_args() -> Result { 31 | let matches = App::new("dcp") 32 | .version(VERSION) 33 | .author("exdx") 34 | .about("docker cp made easy") 35 | .arg( 36 | Arg::with_name("image") 37 | .value_name("IMAGE") 38 | .help("Container image to extract content from") 39 | .required(true), 40 | ) 41 | .arg( 42 | Arg::with_name("download-path") 43 | .value_name("DOWNLOAD-PATH") 44 | .help("Where the image contents should be saved on the filesystem") 45 | .default_value(".") 46 | .short("d") 47 | .long("download-path"), 48 | ) 49 | .arg( 50 | Arg::with_name("content-path") 51 | .value_name("CONTENT-PATH") 52 | .help("Where in the container filesystem the content to extract is") 53 | .short("c") 54 | .default_value("/") 55 | .long("content-path"), 56 | ) 57 | .arg( 58 | Arg::with_name("write-to-stdout") 59 | .value_name("WRITE-TO-STDOUT") 60 | .help("Whether to write to stdout instead of the filesystem") 61 | .takes_value(false) 62 | .short("w") 63 | .long("write-to-stdout"), 64 | ) 65 | .arg( 66 | Arg::with_name("username") 67 | .value_name("USERNAME") 68 | .help("Username used for singing into a private registry.") 69 | .short("u") 70 | .long("username") 71 | .default_value(""), 72 | 73 | ) 74 | .arg( 75 | Arg::with_name("password") 76 | .value_name("PASSWORD") 77 | .help("Password used for signing into a private registry. * WARNING *: Writing credentials to your terminal is risky. Be sure you are okay with them showing up in your history") 78 | .short("p") 79 | .long("password") 80 | .default_value(""), 81 | 82 | ) 83 | .arg( 84 | Arg::with_name("log-level") 85 | .value_name("LOG-LEVEL") 86 | .help("What level of logs to output. Accepts: [info, debug, trace, error, warn]") 87 | .short("l") 88 | .long("log-level") 89 | .default_value("debug"), 90 | ) 91 | .arg( 92 | Arg::with_name("force-pull") 93 | .value_name("FORCE-PULL") 94 | .help("Force a pull even if the image is present locally") 95 | .takes_value(false) 96 | .long("force-pull") 97 | .short("f") 98 | ) 99 | .arg( 100 | Arg::with_name("socket") 101 | .value_name("SOCKET") 102 | .help("Specify a custom socket to utilize for the runtime") 103 | .long("socket") 104 | .short("s") 105 | .default_value(runtime::DEFAULT_SOCKET) 106 | ) 107 | .get_matches(); 108 | 109 | let image = matches.value_of("image").unwrap().to_string(); 110 | let download_path = matches.value_of("download-path").unwrap().to_string(); 111 | let content_path = matches.value_of("content-path").unwrap().to_string(); 112 | let write_to_stdout = matches.is_present("write-to-stdout"); 113 | let force_pull = matches.is_present("force-pull"); 114 | let log_level = matches.value_of("log-level").unwrap().to_string(); 115 | let socket = matches.value_of("socket").unwrap().to_string(); 116 | // TODO (tyslaton): Need to come up with a way for this to be extracted from the docker config to be more secure locally. 117 | let username = matches.value_of("username").unwrap().to_string(); 118 | let password = matches.value_of("password").unwrap().to_string(); 119 | 120 | if write_to_stdout { 121 | return Err(anyhow!("❌ writing to stdout is not currently implemented")); 122 | }; 123 | 124 | Ok(Config { 125 | image, 126 | download_path, 127 | content_path, 128 | write_to_stdout, 129 | log_level, 130 | username, 131 | password, 132 | force_pull, 133 | socket, 134 | }) 135 | } 136 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | 3 | pub mod config; 4 | mod runtime; 5 | 6 | extern crate pretty_env_logger; 7 | #[macro_use] 8 | extern crate log; 9 | 10 | /// Run runs a sequence of events with the provided image 11 | /// Run supports copying container filesystems running on both the docker and podman runtimes 12 | /// 1. Pull down the image 13 | /// 2. Create a container, receiving the container id as a response 14 | /// 3. Copy the container content to the specified directory 15 | /// 4. Delete the container 16 | pub async fn run(cfg: config::Config) -> Result<()> { 17 | pretty_env_logger::formatted_builder() 18 | .parse_filters(&cfg.log_level.clone()) 19 | .init(); 20 | 21 | // Build the runtime 22 | let rt = if let Some(runtime) = runtime::set(&cfg.socket).await { 23 | runtime 24 | } else { 25 | return Err(anyhow!("❌ no valid container runtime")); 26 | }; 27 | 28 | // Build the image struct 29 | let container = match runtime::container::new(cfg.image, rt) { 30 | Ok(i) => i, 31 | Err(e) => { 32 | return Err(anyhow!("❌ error building the image: {}", e)); 33 | } 34 | }; 35 | 36 | // Pull the image 37 | match container 38 | .pull(cfg.username, cfg.password, cfg.force_pull) 39 | .await 40 | { 41 | Ok(_) => {} 42 | Err(e) => { 43 | return Err(anyhow!("❌ error building the image: {}", e)); 44 | } 45 | } 46 | 47 | // Copy files from the image 48 | match container 49 | .copy_files(cfg.content_path, cfg.download_path, cfg.write_to_stdout) 50 | .await 51 | { 52 | Ok(_) => {} 53 | Err(e) => { 54 | return Err(anyhow!("❌ error copying the image's files: {}", e)); 55 | } 56 | } 57 | 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | extern crate pretty_env_logger; 4 | #[macro_use] 5 | extern crate log; 6 | 7 | #[tokio::main] 8 | async fn main() -> Result<()> { 9 | match dcp::config::get_args() { 10 | Err(e) => { 11 | error!("❌ error reading arguments {}", e); 12 | std::process::exit(1) 13 | } 14 | Ok(config) => dcp::run(config).await?, 15 | } 16 | 17 | Ok(()) 18 | } 19 | -------------------------------------------------------------------------------- /src/runtime/container.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use async_trait::async_trait; 3 | 4 | use super::docker::Image as DockerImage; 5 | use super::podman::Image as PodmanImage; 6 | use super::Runtime; 7 | 8 | /// Container is a trait that defines the functionality of a container 9 | /// to be used by dcp. It contains various methods that are required for 10 | /// the process of copying files out of a container but leaves the physical 11 | /// actions of accomplishing this to the implementation. 12 | /// 13 | /// # Functions 14 | /// 15 | /// * `pull` - Pulls the container's image. Accepts authentication and can ignore local images if `force` is set. 16 | /// * `start` - Starts the container and returns the started container's ID if successful. 17 | /// * `stop` - Stops the container. 18 | /// * `copy_files` - Copies the files from the specified locations to the specified destination locally. 19 | /// * `present_locally` - Checks to see if the image is already pulled locally. 20 | #[async_trait] 21 | pub trait Container { 22 | async fn pull(&self, username: String, password: String, force: bool) -> Result<()>; 23 | async fn start(&self) -> Result; 24 | async fn stop(&self, id: String) -> Result<()>; 25 | async fn copy_files( 26 | &self, 27 | content_path: String, 28 | download_path: String, 29 | write_to_stdout: bool, 30 | ) -> Result<()>; 31 | async fn present_locally(&self) -> bool; 32 | } 33 | 34 | /// Returns a container with the provided image and runtime 35 | /// 36 | /// # Arguments 37 | /// 38 | /// * `image` - String representation of an image 39 | /// * `runtime` - Runtime object from representing what this container will run on 40 | pub fn new(image: String, runtime: Runtime) -> Result> { 41 | let repo: String; 42 | let tag: String; 43 | match split(image.clone()) { 44 | Some(image) => { 45 | repo = image.0; 46 | tag = image.1; 47 | } 48 | None => { 49 | return Err(anyhow!( 50 | "could not split provided image into repository and tag" 51 | )) 52 | } 53 | } 54 | 55 | if let Some(docker) = runtime.docker { 56 | return Ok(Box::new(DockerImage { 57 | image, 58 | repo, 59 | tag, 60 | runtime: docker, 61 | })); 62 | } 63 | 64 | if let Some(podman) = runtime.podman { 65 | return Ok(Box::new(PodmanImage { 66 | image, 67 | repo, 68 | tag, 69 | runtime: podman, 70 | })); 71 | } 72 | 73 | Err(anyhow!("failed to determine proper runtime for image")) 74 | } 75 | 76 | fn split(image: String) -> Option<(String, String)> { 77 | let image_split: Vec<&str> = if image.contains('@') { 78 | image.split('@').collect() 79 | } else { 80 | image.split(':').collect() 81 | }; 82 | 83 | if image_split.is_empty() { 84 | return None; 85 | } 86 | 87 | let repo: String; 88 | if let Some(i) = image_split.first() { 89 | repo = i.to_string(); 90 | } else { 91 | return None; 92 | } 93 | 94 | let tag: String; 95 | if let Some(i) = image_split.get(1) { 96 | tag = i.to_string(); 97 | } else { 98 | // Fall back to latest tag if none is provided 99 | tag = String::from("latest"); 100 | } 101 | 102 | Some((repo, tag)) 103 | } 104 | #[cfg(test)] 105 | mod tests { 106 | use super::split; 107 | 108 | #[test] 109 | fn test_split() { 110 | let digest_image = "quay.io/tflannag/bundles@sha256:145ccb5e7e73d4ae914160c066e49f35bc2be2bb86e4ab0002a802aa436599bf"; 111 | let image = digest_image.to_string(); 112 | 113 | let (out_repo, out_tag) = split(image).unwrap(); 114 | assert_eq!(out_repo, "quay.io/tflannag/bundles".to_string()); 115 | assert_eq!( 116 | out_tag, 117 | "sha256:145ccb5e7e73d4ae914160c066e49f35bc2be2bb86e4ab0002a802aa436599bf".to_string() 118 | ); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/runtime/docker.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use async_trait::async_trait; 3 | use docker_api::api::{ContainerCreateOpts, PullOpts, RegistryAuth, RmContainerOpts}; 4 | use futures_util::{StreamExt, TryStreamExt}; 5 | use std::path::PathBuf; 6 | use tar::Archive; 7 | 8 | use super::container::Container; 9 | 10 | pub struct Image { 11 | pub image: String, 12 | pub repo: String, 13 | pub tag: String, 14 | pub runtime: docker_api::Docker, 15 | } 16 | 17 | #[async_trait] 18 | impl Container for Image { 19 | // pull ensures that the image is present locally and, if it is isn't 20 | // will do the work necessary to pull it. 21 | async fn pull(&self, username: String, password: String, force: bool) -> Result<()> { 22 | if self.present_locally().await { 23 | if !force { 24 | debug!("✅ Skipping the pull process as the image was found locally"); 25 | return Ok(()); 26 | } 27 | debug!("🔧 Force was set, ignoring images present locally") 28 | } 29 | 30 | let auth = RegistryAuth::builder() 31 | .username(username) 32 | .password(password) 33 | .build(); 34 | 35 | let pull_opts = PullOpts::builder() 36 | .image(&self.repo) 37 | .tag(&self.tag) 38 | .auth(auth) 39 | .build(); 40 | 41 | let images = self.runtime.images(); 42 | let mut stream = images.pull(&pull_opts); 43 | while let Some(pull_result) = stream.next().await { 44 | match pull_result { 45 | Ok(output) => { 46 | debug!("🔧 {:?}", output); 47 | } 48 | Err(e) => { 49 | return Err(anyhow!("{}", e)); 50 | } 51 | } 52 | } 53 | 54 | debug!("✅ Successfully pulled the image"); 55 | Ok(()) 56 | } 57 | 58 | // copy_files uses the image_structs values to copy files from the 59 | // image's file systems appropriately. 60 | async fn copy_files( 61 | &self, 62 | content_path: String, 63 | download_path: String, 64 | write_to_stdout: bool, 65 | ) -> Result<()> { 66 | // Create the container 67 | let container_id = match self.start().await { 68 | Ok(id) => id, 69 | Err(e) => { 70 | return Err(anyhow!("failed to start the image: {}", e)); 71 | } 72 | }; 73 | 74 | let mut content_path_buffer = PathBuf::new(); 75 | content_path_buffer.push(&content_path); 76 | 77 | let mut download_path_buffer = PathBuf::new(); 78 | download_path_buffer.push(&download_path); 79 | 80 | // Get the files from the container 81 | let bytes = self 82 | .runtime 83 | .containers() 84 | .get(&*container_id) 85 | .copy_from(&content_path_buffer) 86 | .try_concat() 87 | .await?; 88 | 89 | // Fail out if the buffer data processed is empty 90 | if bytes.is_empty() { 91 | return Err(anyhow!("failed to retrieve the files from the container")); 92 | } 93 | 94 | // Unpack the archive 95 | let mut archive = Archive::new(&bytes[..]); 96 | if write_to_stdout { 97 | unimplemented!() 98 | } else { 99 | archive.unpack(&download_path_buffer)?; 100 | } 101 | 102 | info!( 103 | "✅ Copied content to {} successfully", 104 | download_path_buffer.display() 105 | ); 106 | 107 | // Stop the container 108 | match self.stop(container_id).await { 109 | Ok(_) => {} 110 | Err(e) => { 111 | return Err(anyhow!("failed to stop the image: {}", e)); 112 | } 113 | } 114 | 115 | Ok(()) 116 | } 117 | 118 | // start takes the the image struct's values to build a container 119 | // by interacting the container runtime's socket. 120 | async fn start(&self) -> Result { 121 | // note(tflannag): Use a "dummy" command "FROM SCRATCH" container images. 122 | let cmd = vec![""]; 123 | let create_opts = ContainerCreateOpts::builder(&self.image).cmd(&cmd).build(); 124 | let container = self.runtime.containers().create(&create_opts).await?; 125 | let id = container.id().to_string(); 126 | 127 | debug!("📦 Created container with id: {:?}", id); 128 | Ok(id) 129 | } 130 | 131 | // stop takes the given container ID and interacts with the container 132 | // runtime socket to stop the container. 133 | async fn stop(&self, id: String) -> Result<()> { 134 | let delete_opts = RmContainerOpts::builder().force(true).build(); 135 | if let Err(e) = self 136 | .runtime 137 | .containers() 138 | .get(&*id) 139 | .remove(&delete_opts) 140 | .await 141 | { 142 | return Err(anyhow!("{}", e)); 143 | } 144 | 145 | debug!("📦 Cleaned up container {:?} successfully", id); 146 | Ok(()) 147 | } 148 | 149 | // present_locally determines if this container's image is pulled locally 150 | async fn present_locally(&self) -> bool { 151 | debug!("📦 Searching for image {} locally", self.image); 152 | match self.runtime.images().list(&Default::default()).await { 153 | Ok(images) => { 154 | for image in images { 155 | if let Some(repo_tag) = image.repo_tags { 156 | for tag in repo_tag { 157 | if tag == self.image { 158 | debug!("📦 Found image {} locally", self.image); 159 | return true; 160 | } 161 | } 162 | } 163 | } 164 | } 165 | Err(e) => error!("error occurred while searching for image locally: {}", e), 166 | } 167 | 168 | return false; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/runtime/mod.rs: -------------------------------------------------------------------------------- 1 | mod docker; 2 | mod podman; 3 | 4 | pub mod container; 5 | 6 | use docker_api::Docker; 7 | 8 | // Imports not used by windows environments 9 | #[cfg(not(target_os = "windows"))] 10 | use anyhow::anyhow; 11 | #[cfg(not(target_os = "windows"))] 12 | use anyhow::Result; 13 | #[cfg(not(target_os = "windows"))] 14 | use podman_api::Podman; 15 | 16 | // Imports that cannot be used in windows environments 17 | #[cfg(not(target_os = "windows"))] 18 | use xdg::BaseDirectories; 19 | 20 | // Set logical socket default per environment 21 | #[cfg(not(target_os = "windows"))] 22 | pub const DEFAULT_SOCKET: &str = "unix:///var/run/docker.sock"; 23 | #[cfg(target_os = "windows")] 24 | pub const DEFAULT_SOCKET: &str = "tcp://localhost:2375"; 25 | 26 | pub struct Runtime { 27 | pub docker: Option, 28 | pub podman: Option, 29 | } 30 | 31 | pub async fn set(socket: &str) -> Option { 32 | match Docker::new(socket) { 33 | Ok(docker) => { 34 | // Use version() as a proxy for socket connection status 35 | match docker.version().await { 36 | Ok(_) => Some(Runtime { 37 | docker: Some(docker), 38 | podman: None, 39 | }), 40 | #[cfg(not(target_os = "windows"))] 41 | Err(_) => { 42 | // Fallback to podman config 43 | debug!("🔧 docker socket not found: falling back to podman configuration"); 44 | let podman_socket = get_podman_socket(socket).ok()?; 45 | match Podman::new(podman_socket) { 46 | // Use version() as a proxy for socket connection status 47 | Ok(podman) => match podman.version().await { 48 | Ok(_) => Some(Runtime { 49 | docker: None, 50 | podman: Some(podman), 51 | }), 52 | Err(err) => { 53 | error!("❌ neither docker or podman sockets were found running at {} on this host: {}", socket, err); 54 | None 55 | } 56 | }, 57 | Err(err) => { 58 | error!("❌ unable to create a podman client on the host: {}", err); 59 | None 60 | } 61 | } 62 | } 63 | #[cfg(target_os = "windows")] 64 | Err(err) => { 65 | error!( 66 | "❌ docker socket was not found running at {} on this host: {}", 67 | socket, err 68 | ); 69 | return None; 70 | } 71 | } 72 | } 73 | Err(err) => { 74 | error!("❌ unable to create a docker client on the host: {}", err); 75 | None 76 | } 77 | } 78 | } 79 | 80 | #[cfg(not(target_os = "windows"))] 81 | fn get_podman_socket(socket: &str) -> Result { 82 | let mut podman_socket = String::from(socket); 83 | 84 | // If a custom socket has not been set, find the logical default 85 | if socket == DEFAULT_SOCKET { 86 | let base_dirs = BaseDirectories::new()?; 87 | if !base_dirs.has_runtime_directory() { 88 | return Err(anyhow!("could not find xdg runtime directory")); 89 | } 90 | let runtime_dir = base_dirs.get_runtime_directory()?; 91 | podman_socket = format!( 92 | "{}{}{}", 93 | "unix://", 94 | runtime_dir.as_path().to_str().unwrap(), 95 | "/podman/podman.sock" 96 | ); 97 | } 98 | 99 | debug!("🔧 podman socket at {}", podman_socket); 100 | 101 | Ok(podman_socket) 102 | } 103 | -------------------------------------------------------------------------------- /src/runtime/podman.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use async_trait::async_trait; 3 | use futures_util::{StreamExt, TryStreamExt}; 4 | use podman_api::opts::{ContainerCreateOpts, PullOpts, RegistryAuth}; 5 | use std::path::PathBuf; 6 | use tar::Archive; 7 | 8 | use super::container::Container; 9 | 10 | pub struct Image { 11 | pub image: String, 12 | pub repo: String, 13 | pub tag: String, 14 | pub runtime: podman_api::Podman, 15 | } 16 | 17 | #[async_trait] 18 | impl Container for Image { 19 | // pull ensures that the image is present locally and, if it is isn't 20 | // will do the work necessary to pull it. 21 | async fn pull(&self, username: String, password: String, force: bool) -> Result<()> { 22 | if self.present_locally().await { 23 | if !force { 24 | debug!("✅ Skipping the pull process as the image was found locally"); 25 | return Ok(()); 26 | } 27 | debug!("🔧 Force was set, ignoring images present locally") 28 | } 29 | 30 | let auth = RegistryAuth::builder() 31 | .username(username) 32 | .password(password) 33 | .build(); 34 | let pull_opts = PullOpts::builder() 35 | .reference(self.image.clone().trim()) 36 | .auth(auth) 37 | .build(); 38 | 39 | let images = self.runtime.images(); 40 | let mut stream = images.pull(&pull_opts); 41 | while let Some(pull_result) = stream.next().await { 42 | match pull_result { 43 | Ok(output) => { 44 | debug!("🔧 {:?}", output); 45 | } 46 | Err(e) => { 47 | return Err(anyhow!("{}", e)); 48 | } 49 | } 50 | } 51 | 52 | debug!("✅ Successfully pulled the image"); 53 | 54 | Ok(()) 55 | } 56 | 57 | // copy_files uses the image_structs values to copy files from the 58 | // image's file systems appropriately. 59 | async fn copy_files( 60 | &self, 61 | content_path: String, 62 | download_path: String, 63 | write_to_stdout: bool, 64 | ) -> Result<()> { 65 | // Create the container 66 | let container_id = match self.start().await { 67 | Ok(id) => id, 68 | Err(e) => { 69 | return Err(anyhow!("failed to start the image: {}", e)); 70 | } 71 | }; 72 | 73 | let mut content_path_buffer = PathBuf::new(); 74 | content_path_buffer.push(&content_path); 75 | 76 | let mut download_path_buffer = PathBuf::new(); 77 | download_path_buffer.push(&download_path); 78 | 79 | // Get the files from the container 80 | let bytes = self 81 | .runtime 82 | .containers() 83 | .get(&*container_id) 84 | .copy_from(&content_path_buffer) 85 | .try_concat() 86 | .await?; 87 | 88 | // Unpack the archive 89 | let mut archive = Archive::new(&bytes[..]); 90 | if write_to_stdout { 91 | unimplemented!() 92 | } else { 93 | archive.unpack(&download_path_buffer)?; 94 | } 95 | 96 | info!( 97 | "✅ Copied content to {} successfully", 98 | download_path_buffer.display() 99 | ); 100 | 101 | // Stop the container 102 | match self.stop(container_id).await { 103 | Ok(_) => {} 104 | Err(e) => { 105 | return Err(anyhow!("failed to stop the image: {}", e)); 106 | } 107 | } 108 | 109 | Ok(()) 110 | } 111 | 112 | // start takes the the image struct's values to build a container 113 | // by interacting the container runtime's socket. 114 | async fn start(&self) -> Result { 115 | // note(tflannag): Use a "dummy" command "FROM SCRATCH" container images. 116 | let cmd = vec![""]; 117 | let create_opts = ContainerCreateOpts::builder() 118 | .image(self.image.trim()) 119 | .command(&cmd) 120 | .build(); 121 | let container = self.runtime.containers().create(&create_opts).await?; 122 | let id = container.id; 123 | 124 | debug!("📦 Created container with id: {:?}", id); 125 | Ok(id) 126 | } 127 | 128 | // stop takes the given container ID and interacts with the container 129 | // runtime socket to stop the container. 130 | async fn stop(&self, id: String) -> Result<()> { 131 | match self.runtime.containers().prune(&Default::default()).await { 132 | Ok(_) => {} 133 | Err(e) => { 134 | return Err(anyhow!("failed to stop the image: {}", e)); 135 | } 136 | } 137 | debug!("📦 Cleaned up container {:?} successfully", id); 138 | Ok(()) 139 | } 140 | 141 | // present_locally determines if this container's image is pulled locally 142 | async fn present_locally(&self) -> bool { 143 | debug!("📦 Searching for image {} locally", self.image); 144 | match self.runtime.images().list(&Default::default()).await { 145 | Ok(images) => { 146 | for image in images { 147 | if let Some(repo_tag) = image.repo_tags { 148 | for tag in repo_tag { 149 | if tag == self.image { 150 | debug!("📦 Found image {} locally", self.image); 151 | return true; 152 | } 153 | } 154 | } 155 | } 156 | } 157 | Err(e) => error!("error occurred while searching for image locally: {}", e), 158 | } 159 | false 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /tests/cli.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::Command; 2 | use dcp::config::VERSION; 3 | use predicates::prelude::*; 4 | use rand::{thread_rng, Rng}; 5 | use std::error::Error; 6 | use std::fs::remove_dir_all; 7 | 8 | const PRG: &str = "dcp"; 9 | const TEST_CONTENT_DIR: &str = "./target/tmp/test_runs"; 10 | const DEFAULT_IMAGE: &str = "quay.io/tyslaton/sample-catalog:v0.0.4"; 11 | const IMAGE_NO_TAG: &str = "quay.io/tyslaton/sample-catalog"; 12 | const SCRATCH_BASE_IMAGE: &str = "quay.io/tflannag/bundles:resolveset-v0.0.2"; 13 | 14 | // generate_temp_path takes the constant TEST_CONTENT_DIR and 15 | // returns a new string with an appended 5 digit string 16 | fn generate_temp_path() -> String { 17 | let random_string = thread_rng().gen_range(10000..99999); 18 | format!("{}/{}", TEST_CONTENT_DIR, random_string) 19 | } 20 | 21 | // clean_up_test_dir removes the testing directory specified completely. 22 | // 23 | // WARNING: This function is deleting directories recursively, if you are 24 | // using it, be absolutely sure you know what you're doing. 25 | fn clean_up_test_dir(path: &str) { 26 | if path.starts_with(TEST_CONTENT_DIR) { 27 | match remove_dir_all(path) { 28 | Ok(_) => {} 29 | Err(e) => { 30 | eprintln!("failed to delete testing dir {}:{}", path, e); 31 | } 32 | } 33 | } 34 | } 35 | 36 | type TestResult = Result<(), Box>; 37 | 38 | // -------------------------------------------------- 39 | #[test] 40 | fn prints_version() -> TestResult { 41 | let expected_version_output: String = format!("dcp {}", VERSION); 42 | 43 | // version is defined and suceeds with the desired output 44 | Command::cargo_bin(PRG)? 45 | .args(["-V"]) 46 | .assert() 47 | .success() 48 | .stdout(predicate::str::contains(expected_version_output)); 49 | 50 | Ok(()) 51 | } 52 | 53 | // -------------------------------------------------- 54 | #[test] 55 | fn accepts_download_path() -> TestResult { 56 | let path = &generate_temp_path(); 57 | 58 | // content_path is defined and succeeds 59 | Command::cargo_bin(PRG)? 60 | .args(["--download-path", path]) 61 | .args([DEFAULT_IMAGE]) 62 | .assert() 63 | .success(); 64 | 65 | // verify that content was written to the desired download_path 66 | assert!(std::path::Path::new(path).exists()); 67 | 68 | clean_up_test_dir(path); 69 | 70 | Ok(()) 71 | } 72 | 73 | // -------------------------------------------------- 74 | #[test] 75 | fn accepts_content_path() -> TestResult { 76 | let path = &generate_temp_path(); 77 | let content_path = "configs"; 78 | 79 | // content_path is defined and succeeds 80 | Command::cargo_bin(PRG)? 81 | .args(["--download-path", path]) 82 | .args(["--content-path", content_path, DEFAULT_IMAGE]) 83 | .assert() 84 | .success(); 85 | 86 | // verify that content_path grabbed the desired content 87 | let specific_content = &format!("{}/{}", path, content_path); 88 | assert!(std::path::Path::new(specific_content).exists()); 89 | 90 | clean_up_test_dir(path); 91 | 92 | Ok(()) 93 | } 94 | 95 | // -------------------------------------------------- 96 | #[test] 97 | fn fails_invalid_content_path() -> TestResult { 98 | let path = &generate_temp_path(); 99 | let content_path = "manifests"; 100 | 101 | // --content-path has been specified but fails as 102 | // there's no "manifests" directory in the 103 | // DEFAULT_IMAGE container image. 104 | Command::cargo_bin(PRG)? 105 | .args(["--download-path", path]) 106 | .args(["--content-path", content_path, DEFAULT_IMAGE]) 107 | .assert() 108 | .failure(); 109 | 110 | clean_up_test_dir(path); 111 | 112 | Ok(()) 113 | } 114 | 115 | // -------------------------------------------------- 116 | #[test] 117 | fn accepts_image() -> TestResult { 118 | let path = &generate_temp_path(); 119 | 120 | // image is defined and succeeds 121 | Command::cargo_bin(PRG)? 122 | .args(["--download-path", path]) 123 | .args([DEFAULT_IMAGE]) 124 | .assert() 125 | .success(); 126 | 127 | // image is not defined and fails 128 | Command::cargo_bin(PRG)? 129 | .args(["--download-path", path]) 130 | .assert() 131 | .failure(); 132 | 133 | clean_up_test_dir(path); 134 | 135 | Ok(()) 136 | } 137 | 138 | // -------------------------------------------------- 139 | #[test] 140 | fn defaults_tag_to_latest() -> TestResult { 141 | let path = &generate_temp_path(); 142 | 143 | // image is defined and succeeds 144 | Command::cargo_bin(PRG)? 145 | .args(["--download-path", path]) 146 | .args([IMAGE_NO_TAG]) 147 | .assert() 148 | .success(); 149 | 150 | clean_up_test_dir(path); 151 | 152 | Ok(()) 153 | } 154 | 155 | // -------------------------------------------------- 156 | #[test] 157 | fn fails_on_just_tag() -> TestResult { 158 | let path = &generate_temp_path(); 159 | 160 | // image is defined and succeeds 161 | Command::cargo_bin(PRG)? 162 | .args(["--download-path", path]) 163 | .args([":v0.0.4"]) 164 | .assert() 165 | .failure(); 166 | 167 | clean_up_test_dir(path); 168 | 169 | Ok(()) 170 | } 171 | 172 | // -------------------------------------------------- 173 | #[test] 174 | fn accepts_scratch_base_images() -> TestResult { 175 | let path = &generate_temp_path(); 176 | let content_path: &str = "manifests"; 177 | 178 | Command::cargo_bin(PRG)? 179 | .args(["--download-path", path]) 180 | .args(["--content-path", content_path]) 181 | .args([SCRATCH_BASE_IMAGE]) 182 | .assert() 183 | .success(); 184 | 185 | clean_up_test_dir(path); 186 | 187 | Ok(()) 188 | } 189 | --------------------------------------------------------------------------------