diff --git a/.github/workflows/run0.yaml b/.github/workflows/run0.yaml index d61991a..bafb240 100644 --- a/.github/workflows/run0.yaml +++ b/.github/workflows/run0.yaml @@ -26,7 +26,10 @@ jobs: toolchain: ${{ matrix.rust }} override: true components: rustfmt, clippy - + - name: Cargo setup + run: | + echo "[target.x86_64-unknown-linux-gnu]" >> ~/.cargo/config + echo "runner = 'sudo -E'" >> ~/.cargo/config - name: Build and install with default features run: cargo build @@ -34,6 +37,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: test + args: --workspace - name: Run cargo fmt check uses: actions-rs/cargo@v1 @@ -45,4 +49,4 @@ jobs: uses: actions-rs/cargo@v1 with: command: clippy - args: --all-targets --all-features -- -D warnings \ No newline at end of file + args: --all-targets --all-features -- -D warnings diff --git a/.gitignore b/.gitignore index 93f8b77..7f6f714 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,14 @@ # Generated by Cargo # will have compiled files and executables -/target/ +target # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html Cargo.lock - +!crates/**/Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk .idea *.tar.gz +.vscode diff --git a/Cargo.lock b/Cargo.lock index 6866ebc..f6544eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,38 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" + +[[package]] +name = "async-trait" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atty" version = "0.2.14" @@ -19,12 +51,48 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + [[package]] name = "bitflags" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" + +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + [[package]] name = "cc" version = "1.0.72" @@ -71,11 +139,77 @@ dependencies = [ name = "container" version = "0.1.0" dependencies = [ + "anyhow", + "controlgroup", "lazy_static", "oci-spec", + "proc-mounts", + "serde", + "serde_json", + "tempdir", "unshare", ] +[[package]] +name = "controlgroup" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f949b2ff627553c6757514c885435fbe28ba1c44db5029683ef5d19f684a055f" + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "cpufeatures" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +dependencies = [ + "generic-array", + "subtle", +] + [[package]] name = "darling" version = "0.12.4" @@ -143,252 +277,1142 @@ dependencies = [ ] [[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "getset" -version = "0.1.2" +name = "digest" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e45727250e75cc04ff2846a66397da8ef2b3db8e40e0cef4df67950a07621eb9" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "proc-macro-error", - "proc-macro2", - "quote", - "syn", + "generic-array", ] [[package]] -name = "hashbrown" -version = "0.11.2" +name = "digest" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +dependencies = [ + "block-buffer 0.10.2", + "crypto-common", +] [[package]] -name = "heck" -version = "0.4.0" +name = "encoding_rs" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" +checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +dependencies = [ + "cfg-if", +] [[package]] -name = "hermit-abi" -version = "0.1.19" +name = "env_logger" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" dependencies = [ - "libc", + "atty", + "humantime", + "log", + "regex", + "termcolor", ] [[package]] -name = "ident_case" -version = "1.0.1" +name = "fastrand" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +dependencies = [ + "instant", +] [[package]] -name = "indexmap" -version = "1.8.0" +name = "filetime" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" +checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98" dependencies = [ - "autocfg", - "hashbrown", + "cfg-if", + "libc", + "redox_syscall", + "winapi", ] [[package]] -name = "itoa" -version = "1.0.1" +name = "flate2" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" +checksum = "b39522e96686d38f4bc984b9198e3a0613264abaebaff2c5c918bfa6b6da09af" +dependencies = [ + "cfg-if", + "crc32fast", + "libc", + "miniz_oxide", +] [[package]] -name = "lazy_static" -version = "1.4.0" +name = "fnv" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] -name = "libc" -version = "0.2.112" +name = "foreign-types" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] [[package]] -name = "memchr" -version = "2.4.1" +name = "foreign-types-shared" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] -name = "memoffset" -version = "0.6.5" +name = "form_urlencoded" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" dependencies = [ - "autocfg", + "matches", + "percent-encoding 2.1.0", ] [[package]] -name = "nix" -version = "0.20.2" +name = "fuchsia-cprng" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e06129fb611568ef4e868c14b326274959aa70ff7776e9d55323531c374945" -dependencies = [ - "bitflags", - "cc", - "cfg-if", - "libc", - "memoffset", -] +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" [[package]] -name = "oci-spec" -version = "0.5.3" +name = "futures-channel" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8057bb0f33d7ecdf1f0f7cc74ea5cced7c6c694245e2a8d14700507c3bde32e3" +checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" dependencies = [ - "derive_builder", - "getset", - "serde", - "serde_json", - "thiserror", + "futures-core", ] [[package]] -name = "os_str_bytes" -version = "6.0.0" +name = "futures-core" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" -dependencies = [ - "memchr", -] +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" [[package]] -name = "proc-macro-error" -version = "1.0.4" +name = "futures-macro" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" dependencies = [ - "proc-macro-error-attr", "proc-macro2", "quote", "syn", - "version_check", ] [[package]] -name = "proc-macro-error-attr" -version = "1.0.4" +name = "futures-sink" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" + +[[package]] +name = "futures-task" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" + +[[package]] +name = "futures-util" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" dependencies = [ - "proc-macro2", - "quote", - "version_check", + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", ] [[package]] -name = "proc-macro2" -version = "1.0.36" +name = "generic-array" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" dependencies = [ - "unicode-xid", + "typenum", + "version_check 0.9.4", ] [[package]] -name = "quote" -version = "1.0.14" +name = "getset" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47aa80447ce4daf1717500037052af176af5d38cc3e571d9ec1c7353fc10c87d" +checksum = "e45727250e75cc04ff2846a66397da8ef2b3db8e40e0cef4df67950a07621eb9" dependencies = [ + "proc-macro-error", "proc-macro2", + "quote", + "syn", ] [[package]] -name = "run0" -version = "0.1.0" +name = "h2" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" dependencies = [ - "clap", - "container", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util 0.7.1", + "tracing", ] [[package]] -name = "ryu" -version = "1.0.9" +name = "hashbrown" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" [[package]] -name = "serde" -version = "1.0.133" +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97565067517b60e2d1ea8b268e59ce036de907ac523ad83a0475da04e818989a" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ - "serde_derive", + "libc", ] [[package]] -name = "serde_derive" -version = "1.0.133" +name = "hmac" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed201699328568d8d08208fdd080e3ff594e6c422e438b6705905da01005d537" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" dependencies = [ - "proc-macro2", - "quote", - "syn", + "crypto-mac", + "digest 0.9.0", ] [[package]] -name = "serde_json" -version = "1.0.74" +name = "http" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee2bb9cd061c5865d345bb02ca49fcef1391741b672b54a0bf7b679badec3142" +checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03" dependencies = [ + "bytes", + "fnv", "itoa", - "ryu", - "serde", ] [[package]] -name = "strsim" -version = "0.10.0" +name = "http-body" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] [[package]] -name = "syn" -version = "1.0.85" +name = "httparse" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a684ac3dcd8913827e18cd09a68384ee66c1de24157e3c556c9ab16d85695fb7" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] +checksum = "6330e8a36bd8c859f3fa6d9382911fbb7147ec39807f63b923933a247240b9ba" [[package]] -name = "termcolor" -version = "1.1.2" +name = "httpdate" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" -dependencies = [ - "winapi-util", -] +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] -name = "textwrap" -version = "0.14.2" +name = "humantime" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] -name = "thiserror" -version = "1.0.30" +name = "hyper" +version = "0.14.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +checksum = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "hyperx" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5617e92fc2f2501c3e2bc6ce547cad841adba2bae5b921c7e52510beca6d084c" +dependencies = [ + "base64", + "bytes", + "http", + "httpdate", + "language-tags", + "mime", + "percent-encoding 2.1.0", + "unicase 2.6.0", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ipnet" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e70ee094dc02fd9c13fdad4940090f22dbd6ac7c9e7094a46cf0232a50bc7c" + +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + +[[package]] +name = "js-sys" +version = "0.3.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "jwt" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98328bb4f360e6b2ceb1f95645602c7014000ef0c3809963df8ad3a3a09f8d99" +dependencies = [ + "base64", + "crypto-mac", + "digest 0.9.0", + "hmac", + "serde", + "serde_json", + "sha2 0.9.9", +] + +[[package]] +name = "kaps" +version = "0.1.0" +dependencies = [ + "async-trait", + "clap", + "container", + "env_logger", + "lazy_static", + "log", + "oci-image", + "oci-spec", + "tokio", + "unshare", +] + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" + +[[package]] +name = "lock_api" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "miniz_oxide" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "winapi", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi", +] + +[[package]] +name = "native-tls" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nix" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e06129fb611568ef4e868c14b326274959aa70ff7776e9d55323531c374945" +dependencies = [ + "bitflags", + "cc", + "cfg-if", + "libc", + "memoffset", +] + +[[package]] +name = "nix" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" +dependencies = [ + "bitflags", + "cc", + "cfg-if", + "libc", + "memoffset", +] + +[[package]] +name = "ntapi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" +dependencies = [ + "winapi", +] + +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "oci-distribution" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3c580ad67504493981fff06d790929ece7ce149f344f4d8e411808e5a50f62" +dependencies = [ + "anyhow", + "futures-util", + "hyperx", + "jwt", + "lazy_static", + "regex", + "reqwest", + "serde", + "serde_json", + "sha2 0.9.9", + "tokio", + "tracing", + "unicase 1.4.2", + "url 1.7.2", + "www-authenticate", +] + +[[package]] +name = "oci-image" +version = "0.1.0" +dependencies = [ + "container", + "flate2", + "futures-util", + "log", + "nix 0.23.1", + "oci-distribution", + "oci-spec", + "serde", + "serde_json", + "sha2 0.10.2", + "tar", + "tokio", +] + +[[package]] +name = "oci-spec" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b409d52fff741f330914aa6b8ab73e9113607bb13fbc09f95cdb04d16c8dd5d" +dependencies = [ + "derive_builder", + "getset", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "once_cell" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openssl" +version = "0.10.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-sys", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "os_str_bytes" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +dependencies = [ + "memchr", +] + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + +[[package]] +name = "partition-identity" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa925f9becb532d758b0014b472c576869910929cf4c3f8054b386f19ab9e21" +dependencies = [ + "thiserror", +] + +[[package]] +name = "percent-encoding" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "pin-project-lite" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check 0.9.4", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check 0.9.4", +] + +[[package]] +name = "proc-macro2" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "proc-mounts" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d652f8435d0ab70bf4f3590a6a851d59604831a458086541b95238cc51ffcf2" +dependencies = [ + "partition-identity", +] + +[[package]] +name = "quote" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47aa80447ce4daf1717500037052af176af5d38cc3e571d9ec1c7353fc10c87d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "redox_syscall" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "reqwest" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a1f7aa4f35e5e8b4160449f51afc758f0ce6454315a9fa7d0d113e958c41eb" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "native-tls", + "percent-encoding 2.1.0", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tokio-util 0.6.9", + "url 2.2.2", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "ryu" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" + +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "security-framework" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23a2ac85147a3a11d77ecf1bc7166ec0b92febfa4461c37944e180f319ece467" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee2bb9cd061c5865d345bb02ca49fcef1391741b672b54a0bf7b679badec3142" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.3", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" + +[[package]] +name = "smallvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" + +[[package]] +name = "socket2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a684ac3dcd8913827e18cd09a68384ee66c1de24157e3c556c9ab16d85695fb7" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tar" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tempdir" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +dependencies = [ + "rand", + "remove_dir_all", +] + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" + +[[package]] +name = "thiserror" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" dependencies = [ "thiserror-impl", ] @@ -404,6 +1428,173 @@ dependencies = [ "syn", ] +[[package]] +name = "tinyvec" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c27a64b625de6d309e8c57716ba93021dccf1b3b5c97edd6d3dd2d2135afc0a" +dependencies = [ + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "once_cell", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "tokio-macros", + "winapi", +] + +[[package]] +name = "tokio-macros" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0edfdeb067411dba2044da6d1cb2df793dd35add7888d73c16e3381ded401764" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tower-service" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" + +[[package]] +name = "tracing" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80b9fa4360528139bc96100c160b7ae879f5567f49f1782b0b02035b0358ebf3" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e65ce065b4b5c53e73bb28912318cb8c9e9ad3921f1d669eb0e68b4c8143a2b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dfce9f3241b150f36e8e54bb561a742d5daa1a47b5dd9a5ce369fd4a4db2210" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + +[[package]] +name = "unicase" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4765f83163b74f957c797ad9253caf97f103fb064d3999aea9568d09fc8a33" +dependencies = [ + "version_check 0.1.5", +] + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check 0.9.4", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-xid" version = "0.2.2" @@ -416,15 +1607,136 @@ version = "0.7.0" source = "git+https://github.com/virt-do/unshare?branch=main#7b0a2e8d906bcdb2efa62d3ce9580dc359b731fd" dependencies = [ "libc", - "nix", + "nix 0.20.2", +] + +[[package]] +name = "url" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" +dependencies = [ + "idna 0.1.5", + "matches", + "percent-encoding 1.0.1", ] +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna 0.2.3", + "matches", + "percent-encoding 2.1.0", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" + [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f741de44b75e14c35df886aff5f1eb73aa114fa5d4d00dcd37b5e01259bf3b2" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" + +[[package]] +name = "web-sys" +version = "0.3.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" @@ -455,3 +1767,32 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "www-authenticate" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fd1970505d8d9842104b229ba0c6b6331c0897677d0fc0517ea657e77428d0" +dependencies = [ + "hyperx", + "unicase 1.4.2", + "url 1.7.2", +] + +[[package]] +name = "xattr" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c" +dependencies = [ + "libc", +] diff --git a/Cargo.toml b/Cargo.toml index 5c08e0d..6bc1b9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,10 +5,19 @@ edition = "2021" authors = ["Polytech Montpellier - DevOps"] [dependencies] +oci-image = { path = "oci-image" } clap = { version = "3.0.5", features = ["derive"] } container = { path = "container" } +lazy_static = "1.4.0" +oci-spec = "0.5.3" +unshare = { git = "https://github.com/virt-do/unshare", branch = "main" } +tokio = {version = "1.0", features = ["full"]} +async-trait = "0.1.52" +log = "0.4.14" +env_logger = "0.9.0" [workspace] members = [ - "container" + "container", + "oci-image" ] \ No newline at end of file diff --git a/README.md b/README.md index 0e28a29..65f79ef 100644 --- a/README.md +++ b/README.md @@ -4,4 +4,30 @@ `kaps` is an experimental OCI container runtime written in Rust. The project aims to provide a performant, intuitive and OCI-compliant CLI allowing users to run & manage containers. -**Project is experimental and should not be used in any production systems.** \ No newline at end of file +**Project is experimental and should not be used in any production systems.** + +## Install + +To easily install `kaps` on your computer, simply run : + +```shell +$ cargo install --path . && sudo mv $HOME/.cargo/bin/kaps /usr/local/bin +``` + +## Quickstart + +Here a little quickstart to run an `alpine` container, depending on your architecture, for `amd64` : + +**At the moment, Kaps need to be run with root privileges. Consider adding `sudo` before each command or directly execute the following commands as root.** + +```shell +# --name is used to give an identifier to our image. See reference for more information. +$ kaps pull docker.io/amd64/alpine --name alpine +# Mount the image as an OCI bundle into /tmp/alpine +$ kaps mount alpine /tmp/alpine +# Run your container with the bundle +$ kaps run --bundle /tmp/alpine +``` + +For more documentation about commands, please see : +[Command line reference](docs/cli-reference.md) \ No newline at end of file diff --git a/container/Cargo.toml b/container/Cargo.toml index 1d33220..38fe2cd 100644 --- a/container/Cargo.toml +++ b/container/Cargo.toml @@ -7,4 +7,12 @@ authors = ["Polytech Montpellier - DevOps"] [dependencies] lazy_static = "1.4.0" oci-spec = "0.5.3" -unshare = { git = "https://github.com/virt-do/unshare", branch = "main" } \ No newline at end of file +unshare = { git = "https://github.com/virt-do/unshare", branch = "main" } +serde = { version = "1.0", features = ["derive", "rc"] } +serde_json = "1.0" +controlgroup = "0.3.0" +anyhow = "1.0.57" + +[dev-dependencies] +proc-mounts = "0.3.0" +tempdir = "0.3.7" diff --git a/container/src/command.rs b/container/src/command.rs index 37fff85..f09fbb4 100644 --- a/container/src/command.rs +++ b/container/src/command.rs @@ -42,3 +42,21 @@ impl From<&Option> for Command { command } } + +#[cfg(test)] +mod tests { + use crate::{Command, Error}; + use oci_spec::runtime::Process; + + #[test] + fn test_command_from_process() -> Result<(), Error> { + let mut test_process = Process::default(); + + test_process.set_args(Some(vec!["echo".to_string(), "hello world".to_string()])); + let test_command = Command::from(&Some(test_process)); + assert_eq!(test_command.arg0, "echo"); + assert_eq!(test_command.args[0], "hello world"); + + Ok(()) + } +} diff --git a/container/src/cpu.rs b/container/src/cpu.rs new file mode 100644 index 0000000..ae64222 --- /dev/null +++ b/container/src/cpu.rs @@ -0,0 +1,68 @@ +use anyhow::Result; +use controlgroup::{ + v1::{cpu, Cgroup, CgroupPath, SubsystemKind}, + Pid, +}; +use oci_spec::runtime::LinuxCpu; +use std::path::PathBuf; + +pub struct Cpu { + cpu_cgroup: cpu::Subsystem, +} + +impl Cpu { + pub fn new() -> Self { + // Define and create a new cgroup controlled by the CPU subsystem. + let mut cpu_cgroup = + cpu::Subsystem::new(CgroupPath::new(SubsystemKind::Cpu, PathBuf::from("kaps"))); + cpu_cgroup.create().unwrap(); + Cpu { cpu_cgroup } + } + + pub fn apply(&mut self, cpu: &LinuxCpu) -> Result<()> { + if let Some(cpu_shares) = cpu.shares() { + if cpu_shares != 0 { + let _ = &self.cpu_cgroup.set_shares(cpu_shares); + } + } + + if let Some(cpu_period) = cpu.period() { + if cpu_period != 0 { + let _ = &self.cpu_cgroup.set_cfs_period_us(cpu_period); + } + } + + if let Some(cpu_quota) = cpu.quota() { + if cpu_quota != 0 { + let _ = &self.cpu_cgroup.set_cfs_quota_us(cpu_quota); + } + } + + if let Some(rt_runtime) = cpu.realtime_runtime() { + if rt_runtime != 0 { + let _ = &self.cpu_cgroup.set_rt_runtime_us(rt_runtime); + } + } + + if let Some(rt_period) = cpu.realtime_period() { + if rt_period != 0 { + let _ = &self.cpu_cgroup.set_rt_period_us(rt_period); + } + } + + // Attach the self process to the cgroup. + let pid = Pid::from(std::process::id()); + self.cpu_cgroup.add_task(pid).unwrap(); + + Ok(()) + } + + pub fn delete(&mut self) -> Result<()> { + // Removing self process from the cgroup + let pid = Pid::from(std::process::id()); + self.cpu_cgroup.remove_task(pid)?; + // and deleting the cgroup. + self.cpu_cgroup.delete()?; + Ok(()) + } +} diff --git a/container/src/environment.rs b/container/src/environment.rs index d3e4f25..7279095 100644 --- a/container/src/environment.rs +++ b/container/src/environment.rs @@ -32,3 +32,33 @@ impl From<&Option> for Environment { Environment { vars } } } + +#[cfg(test)] +mod tests { + use crate::{Environment, Error}; + use oci_spec::runtime::Process; + + #[test] + fn test_environment_from_process() -> Result<(), Error> { + let mut test_process = Process::default(); + test_process.set_env(Some(vec![ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".to_string(), + "TERM=xterm".to_string(), + ])); + let test_environment = Environment::from(&Some(test_process)); + + assert_eq!( + test_environment.vars[0], + ( + "PATH".to_string(), + "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".to_string() + ) + ); + assert_eq!( + test_environment.vars[1], + (("TERM".to_string(), "xterm".to_string())) + ); + + Ok(()) + } +} diff --git a/container/src/lib.rs b/container/src/lib.rs index 4e04e01..03dc33b 100644 --- a/container/src/lib.rs +++ b/container/src/lib.rs @@ -1,16 +1,26 @@ +extern crate core; + use std::path::PathBuf; use oci_spec::runtime::Spec; use command::Command; +use cpu::Cpu; use environment::Environment; +use memory::Memory; use mounts::Mounts; use namespaces::Namespaces; +use oci_spec::runtime::LinuxResources; +use state::{ContainerState, Status}; mod command; +mod cpu; mod environment; +mod memory; mod mounts; mod namespaces; +pub mod spec; +mod state; /// Containers related errors #[derive(Debug)] @@ -20,7 +30,23 @@ pub enum Error { ContainerSpawnCommand(unshare::Error), ContainerWaitCommand(std::io::Error), ContainerExit(i32), + /// Fail to create container due to existing container with the same id. + ContainerExists(String), Unmount(std::io::Error), + /// Fail to read container state file. + WriteStateFile(serde_json::error::Error), + /// Fail to save container state file. + ReadStateFile(std::io::Error), + /// Fail to serialize/deserialize file. + SerializeError(serde_json::error::Error), + /// Fail to open container state file. + OpenStateFile(std::io::Error), + /// Fail to create container state file. + CreateStateFile(std::io::Error), + /// Fail to remove container state file. + RemoveStateFile(std::io::Error), + /// Fail to acquire lock for the container status + StatusLockPoisoned(String), } /// A common result type for our container module. @@ -44,11 +70,15 @@ pub struct Container { environment: Environment, /// The command entrypoint command: Command, + /// The container state + state: ContainerState, + /// The container resources, + resources: LinuxResources, } impl Container { /// Build a new container with the bundle provided in parameters. - pub fn new(bundle_path: &str) -> Result { + pub fn new(bundle_path: &str, id: &str) -> Result { let bundle = PathBuf::from(bundle_path); // Load the specification from the file @@ -72,32 +102,72 @@ impl Container { Namespaces::from(linux.namespaces()) }); + // Get the container resources if the linux block is defined into the specification. + let resources = spec + .linux() + .as_ref() + .map_or(LinuxResources::default(), |linux| { + linux + .resources() + .as_ref() + .map_or(LinuxResources::default(), |resources| { + LinuxResources::from(resources.clone()) + }) + }); + + // Set the state of the container + let state = ContainerState::new(id, bundle_path)?; + Ok(Container { environment: Environment::from(spec.process()), command: Command::from(spec.process()), namespaces, rootfs, + state, + resources, ..Default::default() }) } /// Run the container. - pub fn run(&self) -> Result<()> { + pub fn run(&mut self) -> Result<()> { let mounts = self.mounts.clone(); + let mut cpu_cgroup = Cpu::new(); + let mut memory_cgroup = Memory::new(); + + if let Some(resources_cpu) = &self.resources.cpu() { + cpu_cgroup.apply(&resources_cpu.clone()).unwrap(); + } + + if let Some(resources_memory) = &self.resources.memory() { + memory_cgroup.apply(&resources_memory.clone()).unwrap(); + } + let code = unsafe { - unshare::Command::from(&self.command) + let mut child = match unshare::Command::from(&self.command) .chroot_dir(&self.rootfs) .unshare(&*self.namespaces.get()) .pre_exec(move || Mounts::apply(&mounts)) .envs(self.environment.get()) .spawn() - .map_err(Error::ContainerSpawnCommand)? - .wait() - .map_err(Error::ContainerWaitCommand)? - .code() + { + Ok(child) => child, + Err(_) => { + return self.mounts.cleanup(self.rootfs.clone()); + } + }; + + self.state.pid = child.pid(); + self.state.set_status(Status::Running)?; + + child.wait().map_err(Error::ContainerWaitCommand)?.code() }; - let _ = &self.mounts.cleanup(self.rootfs.clone())?; + cpu_cgroup.delete().unwrap(); + memory_cgroup.delete().unwrap(); + + self.mounts.cleanup(self.rootfs.clone())?; + self.state.remove()?; if let Some(code) = code { if code != 0 { @@ -108,3 +178,37 @@ impl Container { Ok(()) } } + +#[cfg(test)] +mod tests { + use crate::{Container, Error}; + use proc_mounts::MountList; + use tempdir::TempDir; + + #[test] + fn test_mount_on_empty_rootfs_should_fail_and_cleanup() -> Result<(), std::io::Error> { + //use an empty rootfs for this test + let dir = TempDir::new_in("../hack/fixtures", "test")?; + let test_folder_path = dir.path().to_str().unwrap(); + std::fs::create_dir(format!("{}/rootfs", &test_folder_path))?; + std::fs::copy( + "../hack/fixtures/config.json", + format!("{}/config.json", &test_folder_path), + )?; + + let host_mounts_before_run_fail = MountList::new().unwrap(); + let mut container = Container::new(test_folder_path, "test_folder").unwrap(); + assert!(container.run().is_err()); + + let host_mounts_after_run_fail = MountList::new().unwrap(); + assert_eq!(host_mounts_before_run_fail, host_mounts_after_run_fail); + + Ok(()) + } + + #[test] + fn test_create_container_with_invalid_oci_runtime_spec_file() -> Result<(), Error> { + assert!(Container::new("invalid_spec_file", "invalid_spec").is_err()); + Ok(()) + } +} diff --git a/container/src/memory.rs b/container/src/memory.rs new file mode 100644 index 0000000..708501e --- /dev/null +++ b/container/src/memory.rs @@ -0,0 +1,74 @@ +use anyhow::Result; +use controlgroup::{ + v1::{memory, Cgroup, CgroupPath, SubsystemKind}, + Pid, +}; +use oci_spec::runtime::LinuxMemory; +use std::path::PathBuf; + +pub struct Memory { + memory_cgroup: memory::Subsystem, +} + +impl Memory { + pub fn new() -> Self { + // Define and create a new cgroup controlled by the Memory subsystem. + let mut memory_cgroup = memory::Subsystem::new(CgroupPath::new( + SubsystemKind::Memory, + PathBuf::from("kaps"), + )); + memory_cgroup.create().unwrap(); + Memory { memory_cgroup } + } + + pub fn apply(&mut self, memory: &LinuxMemory) -> Result<()> { + if let Some(limit) = memory.limit() { + if limit != 0 { + let _ = &self.memory_cgroup.set_limit_in_bytes(limit); + } + } + + if let Some(swappiness) = memory.swappiness() { + if swappiness != 0 { + let _ = &self.memory_cgroup.set_swappiness(swappiness); + } + } + + if let Some(kernel) = memory.kernel() { + if kernel != 0 { + let _ = &self.memory_cgroup.set_kmem_limit_in_bytes(kernel); + } + } + + if let Some(kernel_tcp) = memory.kernel_tcp() { + if kernel_tcp != 0 { + let _ = &self.memory_cgroup.set_kmem_tcp_limit_in_bytes(kernel_tcp); + } + } + + if let Some(reservation) = memory.reservation() { + if reservation != 0 { + let _ = &self.memory_cgroup.set_soft_limit_in_bytes(reservation); + } + } + + if let Some(disable_oom_killer) = memory.disable_oom_killer() { + let _ = &self.memory_cgroup.disable_oom_killer(disable_oom_killer); + } + + // Attach the self process to the cgroup. + let pid = Pid::from(std::process::id()); + self.memory_cgroup.add_task(pid).unwrap(); + + Ok(()) + } + + pub fn delete(&mut self) -> Result<()> { + // Removing self process from the cgroup + let pid = Pid::from(std::process::id()); + self.memory_cgroup.remove_task(pid)?; + // and deleting the cgroup. + self.memory_cgroup.delete()?; + Ok(()) + } +} diff --git a/container/src/mounts.rs b/container/src/mounts.rs index 1c6d034..745cd2b 100644 --- a/container/src/mounts.rs +++ b/container/src/mounts.rs @@ -12,15 +12,15 @@ struct Mount { #[derive(Clone)] pub struct Mounts { - vec: Vec, + mounts: Vec, } impl Mounts { /// Apply some mounts. /// This method should be called before the container process execution in order to prepare /// & mount every mounts defined for it. - pub fn apply(mounts: &Mounts) -> Result<(), std::io::Error> { - for mount in &mounts.vec { + pub fn apply(&self) -> Result<(), std::io::Error> { + for mount in &self.mounts { if let Some(code) = Command::new("mount") .args(["-t", &mount.typ, &mount.source, &mount.destination]) .status()? @@ -37,7 +37,7 @@ impl Mounts { /// Cleanup the mounts of a rootfs. /// This method should be called when a container has ended, to clean up the FS. pub fn cleanup(&self, rootfs: PathBuf) -> Result<(), crate::Error> { - for mount in &self.vec { + for mount in &self.mounts { let mut path = rootfs.clone(); path.push(&mount.source); @@ -64,7 +64,7 @@ impl Default for Mounts { /// Based on the OCI Specification fn default() -> Self { Mounts { - vec: vec![ + mounts: vec![ Mount { typ: String::from("devtmpfs"), source: String::from("dev"), @@ -84,3 +84,53 @@ impl Default for Mounts { } } } + +#[cfg(test)] +mod tests { + use crate::mounts::Mount; + use crate::Mounts; + use proc_mounts::MountList; + use std::path::PathBuf; + + #[test] + fn test_mount_apply_and_cleanup() -> Result<(), std::io::Error> { + let test_folders = vec![ + "/tmp/kaps-test_a", + "/tmp/kaps-test_b", + "/tmp/kaps-test_c", + "/tmp/kaps-test_d", + ]; + for folder in &test_folders { + std::fs::create_dir_all(folder)?; + } + + let host_mounts_before_apply = MountList::new().unwrap(); + let mounts = Mounts { + mounts: vec![ + Mount { + typ: String::from("devtmpfs"), + source: String::from("tmp/kaps-test_a"), + destination: String::from("/tmp/kaps-test_b"), + }, + Mount { + typ: String::from("devtmpfs"), + source: String::from("tmp/kaps-test_c"), + destination: String::from("/tmp/kaps-test_d"), + }, + ], + }; + mounts.apply()?; + + let host_mounts_after_apply = MountList::new().unwrap(); + assert_ne!(host_mounts_before_apply, host_mounts_after_apply); + + mounts.cleanup(PathBuf::from("")).unwrap(); + let host_mounts_after_cleanup = MountList::new().unwrap(); + assert_eq!(host_mounts_before_apply, host_mounts_after_cleanup); + + for folder in &test_folders { + std::fs::remove_dir_all(folder)?; + } + Ok(()) + } +} diff --git a/container/src/spec.rs b/container/src/spec.rs new file mode 100644 index 0000000..dbd0422 --- /dev/null +++ b/container/src/spec.rs @@ -0,0 +1,146 @@ +use std::collections::HashMap; +use std::path::PathBuf; + +use oci_spec::image::ImageConfiguration; +use oci_spec::runtime::{Process, Spec, SpecBuilder}; +use oci_spec::OciSpecError; + +use oci_spec::image::ANNOTATION_CREATED; + +pub const BUNDLE_CONFIG: &str = "config.json"; + +pub type Result = std::result::Result; + +/// Generate a runtime config and return his path +pub fn new_runtime_config(image_config: Option<&ImageConfiguration>) -> Result { + if let Some(image_config) = image_config { + let annotations = build_annotations(image_config); + let process = build_process(image_config); + + Ok(SpecBuilder::default() + .version(String::from("1.0")) + .process(process) + .annotations(annotations) + .build()?) + } else { + Ok(Spec::default()) + } +} + +/// Build process from an image configuration and return it +fn build_process(image_config: &ImageConfiguration) -> Process { + let mut args: Vec = vec![]; + let mut process = Process::default(); + + if let Some(config) = image_config.config() { + if let Some(entrypoint) = config.entrypoint() { + args.extend(entrypoint.clone()); + } + if let Some(cmd) = config.cmd() { + args.extend(cmd.clone()); + } + if let Some(env) = config.env() { + process.set_env(Some(env.to_vec())); + } + if let Some(working_dir) = config.working_dir() { + process.set_cwd(PathBuf::from(working_dir)); + } + if !args.is_empty() { + process.set_args(Some(args)); + } + } + process +} + +/// Build annotations from an image configuration and return it +fn build_annotations(image_config: &ImageConfiguration) -> HashMap { + let mut annotations: HashMap = HashMap::new(); + + if let Some(created) = image_config.created() { + annotations.insert(ANNOTATION_CREATED.to_string(), created.to_string()); + } + + if let Some(config) = image_config.config() { + if let Some(labels) = config.labels() { + annotations.extend(labels.clone()); + } + } + annotations +} + +#[cfg(test)] +mod tests { + use super::*; + use oci_spec::image::{ConfigBuilder, ImageConfigurationBuilder}; + + #[test] + fn test_process_config() -> Result<()> { + let image_config = ImageConfigurationBuilder::default() + .config( + ConfigBuilder::default() + .cmd(vec![String::from("-c"), String::from("ls")]) + .entrypoint(vec![String::from("bash")]) + .env(vec![String::from("PATH=/usr/local/sbin")]) + .working_dir(String::from("/home")) + .build()?, + ) + .build()?; + + let spec = new_runtime_config(Some(&image_config)); + + assert!(spec.is_ok()); + + let spec = spec?; + + assert!(spec.process().is_some()); + if let Some(process) = spec.process() { + assert!(process.args().is_some()); + if let Some(args) = process.args() { + assert_eq!(*args, ["bash", "-c", "ls"]); + } + assert!(process.env().is_some()); + if let Some(env) = process.env() { + assert_eq!(*env, ["PATH=/usr/local/sbin"]); + } + assert!(process.cwd().to_str().is_some()); + assert_eq!(process.cwd().to_str().unwrap(), "/home"); + } + Ok(()) + } + + #[test] + fn test_annotations_config() -> Result<()> { + let image_config = ImageConfigurationBuilder::default() + .author("jhon") + .os("linux") + .architecture("amd64") + .created("01-12") + .config( + ConfigBuilder::default() + .stop_signal("SIGKILL") + .exposed_ports(vec![String::from("21/tcp")]) + .build()?, + ) + .build()?; + + let spec = new_runtime_config(Some(&image_config)); + + assert!(spec.is_ok()); + + let spec = spec?; + + assert!(spec.annotations().is_some()); + if let Some(annotations) = spec.annotations() { + let created = annotations.get_key_value(&ANNOTATION_CREATED.to_string()); + assert!(created.is_some()); + if let Some(created) = created { + assert_eq!( + created, + (&ANNOTATION_CREATED.to_string(), &String::from("01-12")) + ); + } + } + + Ok(()) + } +} diff --git a/container/src/state.rs b/container/src/state.rs new file mode 100644 index 0000000..5d3253a --- /dev/null +++ b/container/src/state.rs @@ -0,0 +1,208 @@ +use crate::{Error, Result}; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::fs::{File, OpenOptions}; +use std::path::PathBuf; +use std::sync::{Arc, RwLock}; + +const KAPS_ROOT_PATH: &str = "/var/run/kaps/containers"; +const OCI_VERSION: &str = "0.2.0"; +const STATE_FILE: &str = "state.json"; + +/// Container runtime status +#[derive(Serialize, Deserialize, Debug, PartialEq, Copy, Clone)] +pub enum Status { + #[serde(rename = "creating")] + Creating, + #[serde(rename = "created")] + Created, + #[serde(rename = "running")] + Running, + #[serde(rename = "stopped")] + Stopped, +} + +impl Default for Status { + fn default() -> Self { + Status::Creating + } +} + +/// Represent the state of the running container. +#[derive(Serialize, Deserialize, Debug)] +pub struct ContainerState { + id: String, + /// OCI version. + oci_version: String, + /// Runtime state of the container. + pub status: Arc>, + /// ID of the container process. + pub pid: i32, + /// Path to the bundle. + bundle: PathBuf, +} + +impl Default for ContainerState { + fn default() -> Self { + ContainerState { + oci_version: OCI_VERSION.to_string(), + id: String::default(), + status: Arc::new(RwLock::new(Status::default())), + pid: 0, + bundle: PathBuf::default(), + } + } +} + +impl ContainerState { + pub fn new(id: &str, bundle_path: &str) -> Result { + ContainerState::_new(id, bundle_path, KAPS_ROOT_PATH) + } + + fn _new(id: &str, bundle_path: &str, container_dir: &str) -> Result { + let bundle = PathBuf::from(bundle_path); + let container_path = PathBuf::from(container_dir).join(id); + + if container_path.as_path().exists() { + return Err(Error::ContainerExists(format!( + "A container with the id '{}' already exists", + id + ))); + } + + // create the container directory + fs::create_dir_all(&container_path).map_err(Error::CreateStateFile)?; + + // create the `state.json` file of the container + File::create(container_path.join(STATE_FILE)).map_err(Error::CreateStateFile)?; + + let container_state = ContainerState { + id: id.to_string(), + bundle, + ..Default::default() + }; + + container_state.save(container_dir)?; + + Ok(container_state) + } + + /// Get the current runtime status of the container. + /// + /// As The container status is a RwLock, + /// calling this function results in acquiring a read lock on the status. + fn _status(&self) -> Result { + let container_status = Arc::clone(&self.status); + let container_status = container_status + .read() + .map_err(|e| Error::StatusLockPoisoned(e.to_string()))?; + + Ok(*container_status) + } + + /// Save the container state. + /// + /// The container state file must already have been created. + fn save(&self, container_dir: &str) -> Result<()> { + let container_path = PathBuf::from(container_dir).join(&self.id); + + let file = OpenOptions::new() + .write(true) + .truncate(true) + .open(container_path.join(STATE_FILE)) + .map_err(Error::OpenStateFile)?; + + serde_json::to_writer_pretty(file, &self).map_err(Error::WriteStateFile) + } + + /// Update runtime status of container. + pub fn set_status(&mut self, status: Status) -> Result<()> { + self._set_status(status, KAPS_ROOT_PATH) + } + + fn _set_status(&mut self, status: Status, container_dir: &str) -> Result<()> { + let container_status = Arc::clone(&self.status); + + let mut container_status = container_status + .write() + .map_err(|e| Error::StatusLockPoisoned(e.to_string()))?; + + *container_status = status; + drop(container_status); + + self.save(container_dir) + } + + /// Remove the container state file. + pub fn remove(&self) -> Result<()> { + self._remove(KAPS_ROOT_PATH) + } + + fn _remove(&self, container_dir: &str) -> Result<()> { + let container_path = PathBuf::from(container_dir).join(&self.id); + + fs::remove_dir_all(container_path).map_err(Error::RemoveStateFile) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Error, Result}; + use std::path::Path; + + const KAPS_TEST_ROOT_PATH: &str = "/tmp/kaps"; + + #[test] + fn should_create_state_file() -> Result<()> { + let container_id = "test1"; + + let state = ContainerState::_new(container_id, "fake/path/to/bundle", KAPS_TEST_ROOT_PATH)?; + + assert!(Path::new(KAPS_TEST_ROOT_PATH).join(container_id).exists()); + + let _ = state._remove(KAPS_TEST_ROOT_PATH)?; + + Ok(()) + } + + #[test] + fn should_remove_state_file() -> Result<()> { + let container_id = "test2"; + + let state = ContainerState::_new(container_id, "fake/path/to/bundle", KAPS_TEST_ROOT_PATH)?; + + let _ = state._remove(KAPS_TEST_ROOT_PATH)?; + + assert!(!Path::new(KAPS_TEST_ROOT_PATH).join(container_id).exists()); + + Ok(()) + } + + #[test] + fn should_update_runtime_status() -> Result<()> { + let container_id = "test3"; + let container_path = PathBuf::from(KAPS_TEST_ROOT_PATH).join(container_id); + + let mut state = + ContainerState::_new(container_id, "fake/path/to/bundle", KAPS_TEST_ROOT_PATH)?; + + state._set_status(Status::Stopped, KAPS_TEST_ROOT_PATH)?; + + let file_state = + fs::read_to_string(container_path.join(STATE_FILE)).map_err(Error::ReadStateFile)?; + + let file_state: ContainerState = + serde_json::from_str(&file_state).map_err(Error::SerializeError)?; + + let file_status = file_state._status()?; + + let status = state._status()?; + + assert_eq!(status, file_status); + + let _ = state._remove(KAPS_TEST_ROOT_PATH)?; + + Ok(()) + } +} diff --git a/docs/cli-reference.md b/docs/cli-reference.md new file mode 100644 index 0000000..865c6af --- /dev/null +++ b/docs/cli-reference.md @@ -0,0 +1,53 @@ +# CLI Reference + +Below you'll find the documentation of `kaps` commands. + +You can also use the `--help` arguments to get the detailed help for each command. + +```bash +$ kaps --help +# or +$ kaps COMMAND --help +``` + +## `run` + +Run a container from an OCI bundle. + +```bash +$ kaps run -b +``` + +Options : + +- `-b, --bundle`: The OCI bundle used by the container. + +## `pull` + +Pull a container image from a registry. + +```bash +$ kaps pull IMAGE +``` + +Options : + +- `--name` : By default, the image id will be generated by creating a unique hash for the image digest. By using `--name`, you can provide a friendly identifier your image. + + +- `--rm` : By default, the command will not pull the image if it was already present. If `--rm` is provided in arguments, the command will remove the previous image before pulling the image. + + +- `-q, --quiet` : If set, the command will be executed silently. + +## `mount` + +Create a ready-to-use OCI compliant bundle from an image. + +```bash +$ kaps mount IMAGE_ID +``` + +Options : + +- `-q, --quiet` : If set, the command will be executed silently. \ No newline at end of file diff --git a/hack/fixtures/config.json b/hack/fixtures/config.json new file mode 100644 index 0000000..d137d24 --- /dev/null +++ b/hack/fixtures/config.json @@ -0,0 +1,203 @@ +{ + "ociVersion": "1.0.2-dev", + "process": { + "terminal": true, + "user": { + "uid": 0, + "gid": 0 + }, + "args": [ + "sh" + ], + "env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "TERM=xterm" + ], + "cwd": "/", + "capabilities": { + "bounding": [ + "CAP_AUDIT_WRITE", + "CAP_KILL", + "CAP_NET_BIND_SERVICE" + ], + "effective": [ + "CAP_AUDIT_WRITE", + "CAP_KILL", + "CAP_NET_BIND_SERVICE" + ], + "inheritable": [ + "CAP_AUDIT_WRITE", + "CAP_KILL", + "CAP_NET_BIND_SERVICE" + ], + "permitted": [ + "CAP_AUDIT_WRITE", + "CAP_KILL", + "CAP_NET_BIND_SERVICE" + ], + "ambient": [ + "CAP_AUDIT_WRITE", + "CAP_KILL", + "CAP_NET_BIND_SERVICE" + ] + }, + "rlimits": [ + { + "type": "RLIMIT_NOFILE", + "hard": 1024, + "soft": 1024 + } + ], + "noNewPrivileges": true + }, + "root": { + "path": "rootfs", + "readonly": true + }, + "hostname": "runc", + "mounts": [ + { + "destination": "/proc", + "type": "proc", + "source": "proc" + }, + { + "destination": "/dev", + "type": "tmpfs", + "source": "tmpfs", + "options": [ + "nosuid", + "strictatime", + "mode=755", + "size=65536k" + ] + }, + { + "destination": "/dev/pts", + "type": "devpts", + "source": "devpts", + "options": [ + "nosuid", + "noexec", + "newinstance", + "ptmxmode=0666", + "mode=0620" + ] + }, + { + "destination": "/dev/shm", + "type": "tmpfs", + "source": "shm", + "options": [ + "nosuid", + "noexec", + "nodev", + "mode=1777", + "size=65536k" + ] + }, + { + "destination": "/dev/mqueue", + "type": "mqueue", + "source": "mqueue", + "options": [ + "nosuid", + "noexec", + "nodev" + ] + }, + { + "destination": "/sys", + "type": "none", + "source": "/sys", + "options": [ + "rbind", + "nosuid", + "noexec", + "nodev", + "ro" + ] + }, + { + "destination": "/sys/fs/cgroup", + "type": "cgroup", + "source": "cgroup", + "options": [ + "nosuid", + "noexec", + "nodev", + "relatime", + "ro" + ] + } + ], + "linux": { + "uidMappings": [ + { + "containerID": 0, + "hostID": 1001, + "size": 1 + } + ], + "gidMappings": [ + { + "containerID": 0, + "hostID": 1001, + "size": 1 + } + ], + "namespaces": [ + { + "type": "pid" + }, + { + "type": "ipc" + }, + { + "type": "uts" + }, + { + "type": "mount" + }, + { + "type": "user" + } + ], + "maskedPaths": [ + "/proc/acpi", + "/proc/asound", + "/proc/kcore", + "/proc/keys", + "/proc/latency_stats", + "/proc/timer_list", + "/proc/timer_stats", + "/proc/sched_debug", + "/sys/firmware", + "/proc/scsi" + ], + "readonlyPaths": [ + "/proc/bus", + "/proc/fs", + "/proc/irq", + "/proc/sys", + "/proc/sysrq-trigger" + ], + "resources": { + "cpu": { + "shares": 1024, + "quota": 1000000, + "period": 500000, + "realtimeRuntime": 950000, + "realtimePeriod": 1000000 + }, + "memory": { + "limit": 536870912, + "reservation": 536870912, + "kernel": -1, + "kernelTCP": -1, + "swappiness": 0, + "disableOOMKiller": false + } + } + } +} \ No newline at end of file diff --git a/oci-image/Cargo.toml b/oci-image/Cargo.toml new file mode 100644 index 0000000..3815c86 --- /dev/null +++ b/oci-image/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "oci-image" +version = "0.1.0" +edition = "2021" +authors = ["Thomas Gouveia = std::result::Result; + +/// The `ImageManager` should be responsible of the OCI images management. +/// +/// It provides method to pull and unpack OCI images from OCI registries. It maintains a consistent state +/// in order to avoid pulling existing layers, to improve performance. +pub struct ImageManager { + /// The directory where images and bundles will be stored on the host. + data_dir: PathBuf, + /// The image manager state + state: Arc>, + /// The snapshot to use + snapshot: Box, +} + +impl ImageManager { + pub fn new(data_dir: &Path, snapshot: Box) -> Result { + // If data_dir not is not existing + // we must create it + if !data_dir.exists() { + create_dir_all(data_dir) + .map_err(|e| Error::ManagerDataDirectoryCreation(e.to_string()))?; + } + + let state = State::try_from(&data_dir.join(STATE_FILE))?; + + Ok(Self { + data_dir: data_dir.to_path_buf(), + state: Arc::new(Mutex::new(state)), + snapshot, + }) + } + + /// Unpack an OCI image. + /// + /// Returns an error if the image is not found. + pub async fn mount(&mut self, image_id: &str) -> Result { + // Get the image, and if not found, throw an error + let image = self + .state + .lock() + .await + .image(image_id) + .ok_or_else(|| Error::ImageNotFound(format!("No image found with id={}", image_id)))? + .clone(); + + let snapshot_index = self.state.lock().await.snapshot_index(); + let mount_path = self.bundles_dir().join(&image.id); + + log::debug!("creating new OCI bundle into {}", mount_path.display()); + + self.snapshot.mount( + image + .layers + .into_iter() + .map(|layer| layer.store_path) + .collect(), + mount_path.join(OCI_ROOTFS).as_path(), + &snapshot_index, + false, + )?; + + log::debug!("generating oci runtime configuration based on image"); + // Generate a new runtime configuration based on image configuration + container::spec::new_runtime_config(Some(&image.config)) + .map_err(Error::GenerateOCIConfig)? + .save(mount_path.join(OCI_CONFIG)) + .map_err(Error::GenerateOCIConfig)?; + + self.state.lock().await.save(&self.state_file())?; + + Ok(mount_path) + } + + /// Pull an image. + /// + /// This method pull an OCI image and generate a directory containing the image layers and the + /// image manifest in the file `index.json`. + pub async fn pull( + &mut self, + image: &str, + remove_existing: &bool, + id: &Option, + ) -> Result { + let mut puller = Puller::new(image, &self.images_dir())?; + + log::info!("Getting {} manifest...", &image); + + // Pull the image manifest and configuration + let (image_manifest, image_digest, image_config_raw) = puller.pull_manifest().await?; + + let image_id = match id { + None => to_uid(&image_digest), + Some(id) => id.clone(), + }; + + // We don't want to pull images each time if the image is already present to improve performance. + // If the argument `--rm` is provided, `remove_existing` will be true so we have to check + // if the pull must be forced or not. + if self.state.lock().await.has_image(&image_id) && !*remove_existing { + log::info!( + "Image {} already present on disk. To force image pulling, please specify `--rm`.", + &image + ); + return Ok(image_id); + } + + // Parse the image configuration + let config = ImageConfiguration::from_reader(image_config_raw.as_bytes()) + .map_err(|e| Error::ParseImageConfiguration(e.to_string()))?; + + // Check if the number of layers in manifest are equals to the number of layers parsed in the image configuration. + if config.rootfs().diff_ids().len() != image_manifest.layers.len() { + return Err(Error::InvalidPulledLayers( + "Pulled number of layers is not the same defined in image configuration." + .to_string(), + )); + } + + log::info!("Pulling image {}...", &image); + + // Pull the image layers + let layers = puller + .pull_layers(self.state.clone(), config.rootfs().diff_ids()) + .await?; + + self.state + .lock() + .await + .add_image(&ImageMetadata { + id: image_id.clone(), + digest: image_digest.clone(), + reference: image.to_string(), + layers, + config, + }) + .save(&self.state_file())?; + + Ok(image_id) + } + /// Get the state file path + fn state_file(&self) -> PathBuf { + self.data_dir.join(STATE_FILE) + } + + /// Get the default images directory path. + fn images_dir(&self) -> PathBuf { + self.data_dir.join(IMAGES_DIR) + } + + /// Get the default bundles directory path. + fn bundles_dir(&self) -> PathBuf { + self.data_dir.join(BUNDLES_DIR) + } +} + +#[cfg(test)] +mod tests { + use crate::snapshots::overlay::OverlayFS; + use crate::ImageManager; + use std::env::temp_dir; + use std::fs::remove_dir_all; + + #[test] + fn test_it_create_a_manager_instance() { + let data_dir = temp_dir().join("kaps"); + let im = ImageManager::new( + &data_dir, + Box::new(OverlayFS { + data_dir: data_dir.join("snapshots"), + }), + ); + assert!(im.is_ok()) + } + + #[tokio::test] + async fn test_it_pull_images() { + let fixtures = vec!["docker.io/amd64/alpine", "mcr.microsoft.com/hello-world"]; + + let data_dir = temp_dir().join("kaps_tests"); + let mut im = ImageManager::new( + &data_dir, + Box::new(OverlayFS { + data_dir: data_dir.join("snapshots"), + }), + ) + .unwrap(); + + for image in fixtures { + assert!(im.pull(image, &true, &None).await.is_ok()); + } + + remove_dir_all(data_dir).expect("failed to clean up") + } +} diff --git a/oci-image/src/pull.rs b/oci-image/src/pull.rs new file mode 100644 index 0000000..a507483 --- /dev/null +++ b/oci-image/src/pull.rs @@ -0,0 +1,188 @@ +use crate::state::LayerMetadata; +use crate::{Error, MetadataManager, Result, State}; +use flate2::read::GzDecoder; +use oci_distribution::manifest::OciManifest; +use oci_distribution::secrets::RegistryAuth; +use oci_distribution::{Client, Reference}; +use sha2::Digest; +use std::fs::create_dir_all; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use tar::Archive; +use tokio::sync::Mutex; + +/// `Puller` is responsible about pull OCI images from registries. +/// +/// It provides methods to simply pull an image on your machine. +pub(crate) struct Puller { + /// The OCI client which will be used to interact with registries. + client: Client, + + /// The OCI registry auth information. + auth: RegistryAuth, + + /// The OCI image reference + reference: Reference, + + /// The directory where the image will be stored + image_dir: PathBuf, + + /// The accepted media types which will be required to pull images. + /// Example : + accepted_media_types: Vec, +} + +impl Puller { + /// Create a new instance of `Puller`. + /// + /// Configure client, authentication and image reference + /// in order to properly pull the image. + pub fn new(image: &str, image_dir: &Path) -> Result { + let reference = Reference::try_from(image) + .map_err(|_| Error::InvalidOCIReference(image.to_string()))?; + let auth = RegistryAuth::Anonymous; + + Ok(Self { + auth, + reference, + client: Client::default(), + image_dir: image_dir.to_path_buf(), + accepted_media_types: vec![], + }) + } + + /// Pull the image manifest and the configuration from the registry. + pub async fn pull_manifest(&mut self) -> Result<(OciManifest, String, String)> { + let (manifest, digest, config) = self + .client + .pull_manifest_and_config(&self.reference, &self.auth) + .await + .map_err(|e| Error::PullManifest(e.to_string()))?; + + // Build a list of the accepted media types + self.accepted_media_types = manifest + .layers + .iter() + .map(|x| x.media_type.clone()) + .collect::>(); + + // Remove duplicates + self.accepted_media_types.dedup(); + + Ok((manifest, digest, config)) + } + + /// Pull the image from the registry. The method will return a vector containing the layers metadata for the image. + /// + /// The image will be stored into the `image_dir` argument provided in `Puller` constructor. + /// This will produce a fully compliant OCI image with a `blobs` directory and a `manifest.json` file. + pub async fn pull_layers( + &mut self, + state: Arc>, + diffs: &[String], + ) -> Result> { + // Get the image data + let data = self + .client + .pull( + &self.reference, + &self.auth, + Vec::from_iter(self.accepted_media_types.iter().map(String::as_str)), + ) + .await + .map_err(|e| Error::PullImage(e.to_string()))?; + + let layers_dir = self.image_dir.join("layers"); + // Create the directories to store layers if not exists + create_dir_all(&layers_dir).map_err(|e| Error::LayerDirectoryCreation(e.to_string()))?; + + let layers = data.layers.into_iter().enumerate().map(|(i, layer)| { + let state = state.clone(); + let layer_data = layer.data.clone(); + let layer_sha256_digest = layer.sha256_digest(); + let layer_path = layers_dir.join(&layer_sha256_digest.replace(':', "_")); + // This block defines the behavior for one layer. + async move { + // If the layer already exists on the disk, return directly + if let Some(layer_meta) = state.lock().await.layer(&layer_sha256_digest) { + log::info!("Layer {} : {} (cached)", &i, &layer_sha256_digest); + return Ok::<_, crate::Error>(layer_meta.clone()); + } + + log::info!("Layer {} : {}", &i, &layer_sha256_digest); + + // Decompress the layer + let mut out: Vec = Vec::new(); + let mut decoder = GzDecoder::new(layer_data.as_slice()); + std::io::copy(&mut decoder, &mut out).unwrap(); + + let uncompressed_digest = format!("{}:{:x}", "sha256", sha2::Sha256::digest(&out)); + if uncompressed_digest != diffs[i] { + return Err(Error::UncompressedLayerInvalid(format!("uncompressed digest is different than the digest defined in the image configuration. digest={:?}, image_digest={:?}", &uncompressed_digest, &diffs[i]))); + } + + // Finally, unpack the layer + let mut archive = Archive::new(out.as_slice()); + archive.unpack(PathBuf::from(&layer_path)).map_err(|e| Error::UnpackLayer(format!("failed to unpack layer {}. details = {:?}", &layer_path.display(), e)))?; + + let layer_meta = LayerMetadata { + id: layer_sha256_digest.clone(), + compressed_digest: layer_sha256_digest.clone(), + store_path: layer_path.display().to_string(), + uncompressed_digest, + }; + + state.lock().await.add_layer(&layer_meta.clone()); + + Ok::<_, crate::Error>(layer_meta) + } + }); + + futures_util::future::try_join_all(layers).await + } +} + +#[cfg(test)] +mod tests { + use crate::pull::Puller; + use crate::State; + use oci_spec::image::ImageConfiguration; + use std::env::temp_dir; + use std::path::PathBuf; + use std::sync::Arc; + use tokio::sync::Mutex; + + fn test_dir() -> PathBuf { + temp_dir().join("kaps_tests").join("images") + } + + #[test] + fn it_create_a_puller_instance() { + assert!(Puller::new("docker.io/library/busybox", test_dir().as_path()).is_ok()); + } + + #[test] + fn it_throw_an_error_if_invalid_image_reference() { + assert!(Puller::new("$", test_dir().as_path()).is_err()); + } + + #[tokio::test] + async fn it_pull_a_manifest() { + let mut client = Puller::new("docker.io/amd64/ubuntu", test_dir().as_path()).unwrap(); + assert!(client.pull_manifest().await.is_ok()) + } + + #[tokio::test] + async fn it_pull_an_image_layers() { + let state = Arc::new(Mutex::new(State::default())); + let mut client = Puller::new("docker.io/amd64/ubuntu", test_dir().as_path()).unwrap(); + let manifest_result = client.pull_manifest().await; + assert!(manifest_result.is_ok()); + + let (_, _, config) = manifest_result.unwrap(); + + let cfg = ImageConfiguration::from_reader(config.as_bytes()).unwrap(); + let pull_result = client.pull_layers(state, cfg.rootfs().diff_ids()).await; + assert!(pull_result.is_ok()); + } +} diff --git a/oci-image/src/snapshots/mod.rs b/oci-image/src/snapshots/mod.rs new file mode 100644 index 0000000..1e2d96e --- /dev/null +++ b/oci-image/src/snapshots/mod.rs @@ -0,0 +1,27 @@ +pub mod overlay; + +use crate::Result; +use std::path::{Path, PathBuf}; + +/// The `Snapshotter` trait defines methods that can be implemented in order to create a container image snapshot. +pub trait Snapshotter: Send + Sync { + fn mount( + &mut self, + layers: Vec, + mount_path: &Path, + index: &usize, + read_only: bool, + ) -> Result; + fn umount(&self, mount_point: &MountPoint) -> Result<()>; +} + +/// `MountPoint` holds information about a mount point on the host. +#[derive(Debug)] +pub struct MountPoint { + /// The FS type for the mount point + #[allow(dead_code)] + pub typ: String, + /// The mount destination path + #[allow(dead_code)] + pub mount_path: PathBuf, +} diff --git a/oci-image/src/snapshots/overlay.rs b/oci-image/src/snapshots/overlay.rs new file mode 100644 index 0000000..242aba1 --- /dev/null +++ b/oci-image/src/snapshots/overlay.rs @@ -0,0 +1,139 @@ +use crate::snapshots::{MountPoint, Snapshotter}; +use crate::{Error, Result}; +use nix::mount::MsFlags; +use std::fs::create_dir_all; +use std::path::{Path, PathBuf}; + +/// `OverlayFS` is used to easily mount and unmount +/// overlay file systems. +#[derive(Debug)] +pub struct OverlayFS { + pub data_dir: PathBuf, +} + +impl Snapshotter for OverlayFS { + /// Create a mount point with OverlayFS. + /// + /// This will ensure all directories required by the mount are created, and if not it'll create them. + /// Once ready, the method will mount all layers into the mount_path. + fn mount( + &mut self, + layers: Vec, + mount_path: &Path, + index: &usize, + read_only: bool, + ) -> Result { + let layers = layers.iter().map(|l| l.as_str()).collect::>(); + let workdir = self.data_dir.join(index.to_string()); + + // OverlayFS configuration + let overlay_lowerdir = layers.join(":"); + let overlay_upperdir = workdir.join("upperdir"); + let overlay_workdir = workdir.join("workdir"); + + if !self.data_dir.exists() { + log::debug!( + "creating OverlayFS data directory = {}", + &self.data_dir.display() + ); + create_dir_all(&self.data_dir) + .map_err(|e| Error::OverlayFSCreateDirectory(e.to_string()))?; + } + create_dir_all(&overlay_upperdir) + .map_err(|e| Error::OverlayFSCreateDirectory(e.to_string()))?; + create_dir_all(&overlay_workdir) + .map_err(|e| Error::OverlayFSCreateDirectory(e.to_string()))?; + + if !mount_path.exists() { + log::debug!("creating overlayfs mount path = {}", mount_path.display()); + create_dir_all(mount_path) + .map_err(|e| Error::OverlayFSCreateDirectory(e.to_string()))?; + } + + let source = Path::new("overlay"); + let flags = match read_only { + true => MsFlags::MS_RDONLY, + false => MsFlags::empty(), + }; + let options = format!( + "lowerdir={},upperdir={},workdir={}", + &overlay_lowerdir, + overlay_upperdir.display(), + overlay_workdir.display() + ); + + log::debug!("mounting layers into {}", &mount_path.display()); + + nix::mount::mount( + Some(source), + mount_path, + Some("overlay"), + flags, + Some(options.as_str()), + ) + .map_err(|e| Error::OverlayFSMount(e.to_string()))?; + + log::debug!("new overlay mountpoint at {}", &mount_path.display()); + + Ok(MountPoint { + typ: "overlay".to_string(), + mount_path: mount_path.to_path_buf(), + }) + } + + /// Execute a `umount` sys call on the `mount_point`. + #[allow(dead_code)] + fn umount(&self, mount_point: &MountPoint) -> Result<()> { + nix::mount::umount(mount_point.mount_path.as_path()).map_err(Error::OverlayFSUmount)?; + log::debug!( + "successfully unmounted = {}", + &mount_point.mount_path.display() + ); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + // TODO uncomment this when privileges issue solved + + // use crate::snapshots::OverlayFS; + // use std::env::temp_dir; + // use std::fs::{create_dir_all, remove_dir_all, File}; + // use std::path::PathBuf; + + // #[test] + // fn test_mount_overlayfs() { + // let tmp = temp_dir().join("kaps_mount_tests"); + // + // let dir_a = tmp.join("a"); + // let dir_b = tmp.join("b"); + // + // let file_a = dir_a.join("a.txt"); + // let file_b = dir_b.join("b.txt"); + // + // create_dir_all(&file_a.parent().unwrap()).expect("failed to create file a hierarchy"); + // create_dir_all(&file_b.parent().unwrap()).expect("failed to create file b hierarchy"); + // + // File::create(&file_a).expect("failed to create file a"); + // File::create(&file_b).expect("failed to create file b"); + // + // assert!(&file_a.exists()); + // assert!(&file_b.exists()); + // + // let mut layers = Vec::::new(); + // layers.push(dir_a.clone()); + // layers.push(dir_b.clone()); + // + // let mount_path = tmp.join("rootfs"); + // let mount = OverlayFS::mount(layers, &mount_path, &tmp.join("overlayfs")); + // + // assert!(mount.is_ok()); + // + // assert!(mount_path.join("a").join("a.txt").exists()); + // assert!(mount_path.join("b").join("b.txt").exists()); + // + // // Clean up + // remove_dir_all(&tmp).expect(format!("failed to clean up {}", tmp.display()).as_str()) + // } +} diff --git a/oci-image/src/state.rs b/oci-image/src/state.rs new file mode 100644 index 0000000..9d60067 --- /dev/null +++ b/oci-image/src/state.rs @@ -0,0 +1,197 @@ +use crate::Error; +use oci_spec::image::ImageConfiguration; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fs::{File, OpenOptions}; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicUsize, Ordering}; + +/// The `MetadataManager` defines the contract that must be implemented by +/// our state implementation. +pub(crate) trait MetadataManager { + /// Add an `Image` to the state. + fn add_image(&mut self, image: &ImageMetadata) -> &mut Self; + /// Add a `Layer` to the state. + fn add_layer(&mut self, layer: &LayerMetadata) -> &mut Self; + /// Check if the state contains an `Image` by it's identifier + fn has_image(&self, image_id: &str) -> bool; + /// Check if the state contains a `Layer` by it's identifier + fn has_layer(&self, layer_id: &str) -> bool; + /// Get an image from the state by it's identifier + fn image(&self, image_id: &str) -> Option<&ImageMetadata>; + /// Get a layer from the state by it's identifier + fn layer(&self, layer_id: &str) -> Option<&LayerMetadata>; + /// Get a new snapshot index + fn snapshot_index(&mut self) -> usize; +} + +/// `LayerMetadata` struct holds information's about a layer in the state. +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)] +pub struct LayerMetadata { + /// The id of the layer + pub id: String, + /// The compressed layer digest + pub compressed_digest: String, + /// The decompressed layer digest, + pub uncompressed_digest: String, + /// The path where the layer is stored + pub store_path: String, +} + +/// `ImageMetadata` struct holds information's about an image in the state. +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)] +pub struct ImageMetadata { + /// The image id + pub id: String, + /// The image reference, such as `docker.io/amd64/ubuntu` + pub reference: String, + /// The image digest + pub digest: String, + /// The image layers metas + pub layers: Vec, + /// The image configuration + pub config: ImageConfiguration, +} + +/// `State` is responsible about storing information's about images and layers. +#[derive(Clone, Default, Deserialize, Debug, Serialize, PartialEq)] +pub(crate) struct State { + /// An hashmap that holds every images pulled + images: HashMap, + /// An hashmap that holds every layers pulled + layers: HashMap, + /// An index to track the last snapshot identifier + index: usize, +} + +impl State { + /// Save the state into the file. + pub fn save(&self, path: &Path) -> crate::Result<()> { + let serialized = serde_json::to_string_pretty(&self) + .map_err(|e| Error::SerializeState(e.to_string()))?; + + OpenOptions::new() + .write(true) + .open(path) + .map_err(|e| Error::OpenStateFile(e.to_string()))? + .write_all(serialized.as_bytes()) + .map_err(|e| Error::WriteStateFile(e.to_string())) + } +} + +impl MetadataManager for State { + fn add_image(&mut self, image: &ImageMetadata) -> &mut State { + self.images.insert(image.id.clone(), image.clone()); + self + } + + fn add_layer(&mut self, layer: &LayerMetadata) -> &mut State { + self.layers + .insert(layer.compressed_digest.clone(), layer.clone()); + self + } + + fn has_image(&self, image_id: &str) -> bool { + self.images.contains_key(image_id) + } + + fn has_layer(&self, layer_id: &str) -> bool { + self.layers.contains_key(layer_id) + } + + fn image(&self, image_id: &str) -> Option<&ImageMetadata> { + self.images.get(image_id) + } + + fn layer(&self, layer_id: &str) -> Option<&LayerMetadata> { + self.layers.get(layer_id) + } + + fn snapshot_index(&mut self) -> usize { + let current_index = AtomicUsize::new(self.index); + current_index.fetch_add(1, Ordering::SeqCst); + // Load the new index from the current + let new_index = current_index.load(Ordering::SeqCst); + self.index = new_index; + new_index + } +} + +impl TryFrom<&PathBuf> for State { + type Error = crate::Error; + + fn try_from(state_file: &PathBuf) -> Result { + let file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(state_file) + .map_err(|e| Error::OpenStateFile(e.to_string()))?; + + Ok(serde_json::from_reader::(file).map_or_else(|_| State::default(), |s| s)) + } +} + +#[cfg(test)] +mod tests { + use crate::state::LayerMetadata; + use crate::{ImageMetadata, MetadataManager, State}; + use std::env::temp_dir; + use std::fs::create_dir_all; + + #[test] + fn test_save_state_in_file() { + let state_file = temp_dir().join("kaps_tests"); + + create_dir_all(&state_file).expect("Failed to create directories for state file"); + + let state = State::try_from(&state_file.join("test_state.json")).unwrap(); + + assert!(state.save(&state_file.join("test_state.json")).is_ok()); + } + + #[test] + fn test_add_image() { + let state_file = temp_dir().join("kaps_tests"); + create_dir_all(&state_file).expect("Failed to create directories for state file"); + let mut state = State::try_from(&state_file.join("test_add_image.json")).unwrap(); + + state.add_image(&ImageMetadata::default()); + + assert_eq!(state.images.len(), 1); + } + + #[test] + fn test_add_layer() { + let state_file = temp_dir().join("kaps_tests"); + create_dir_all(&state_file).expect("Failed to create directories for state file"); + let mut state = State::try_from(&state_file.join("test_add_layer.json")).unwrap(); + + state.add_layer(&LayerMetadata::default()); + + assert_eq!(state.layers.len(), 1); + } + + #[test] + fn test_load_state_from_file() { + let fixture_layer = LayerMetadata::default(); + let fixture_image = ImageMetadata::default(); + + let state_file = temp_dir().join("kaps_tests"); + + create_dir_all(&state_file).expect("Failed to create directories for state file"); + + let mut state = State::try_from(&state_file.join("test_state_2.json")).unwrap(); + + state.add_image(&fixture_image).add_layer(&fixture_layer); + + assert!(state.save(&state_file.join("test_state_2.json")).is_ok()); + + let new_state = State::try_from(&state_file.join("test_state_2.json")).unwrap(); + + assert_eq!(new_state, state); + assert_eq!(new_state.images.len(), 1); + assert_eq!(new_state.layers.len(), 1); + } +} diff --git a/oci-image/src/utils.rs b/oci-image/src/utils.rs new file mode 100644 index 0000000..acd407f --- /dev/null +++ b/oci-image/src/utils.rs @@ -0,0 +1,9 @@ +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; + +/// Generate a new unique identifier from a string entry +pub fn to_uid(param: &str) -> String { + let mut hasher = DefaultHasher::new(); + param.hash(&mut hasher); + hasher.finish().to_string() +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 8179f32..ec1eb6f 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,17 +1,32 @@ +mod mount; +mod pull; mod run; +mod spec; +use crate::cli::mount::MountCommand; +use crate::cli::pull::PullCommand; use crate::cli::run::RunCommand; +use crate::cli::spec::SpecCommand; +use async_trait::async_trait; use clap::{Parser, Subcommand}; /// CLI related errors #[derive(Debug)] pub enum Error { - Run(container::Error), + Spec(oci_spec::OciSpecError), + Runtime(container::Error), + Image(oci_image::Error), } impl From for Error { fn from(error: container::Error) -> Self { - Self::Run(error) + Self::Runtime(error) + } +} + +impl From for Error { + fn from(error: oci_image::Error) -> Self { + Self::Image(error) } } @@ -21,18 +36,23 @@ pub type Result = std::result::Result; /// `Handler` is a trait that should be implemented for each of our commands. /// /// It defines the contract & the input / output of a command execution. +#[async_trait] pub trait Handler { /// Executes the command handler. /// /// Every command should take no argument, has it is built at runtime with these arguments. /// Also, a command must always return a `Result<()>`. - fn handler(&self) -> crate::Result<()>; + async fn handler(&self, logger: &mut env_logger::Builder) -> crate::Result<()>; } #[derive(Parser, Debug)] #[clap(version, author)] pub struct Cli { - /// Container bundle + /// The level of verbosity. + #[clap(short, long, parse(from_occurrences))] + pub(crate) verbose: usize, + + /// The subcommand to apply #[clap(subcommand)] pub(crate) command: Command, } @@ -46,6 +66,9 @@ impl Cli { pub fn command(self) -> Box { match self.command { Command::Run(cmd) => Box::new(cmd), + Command::Spec(cmd) => Box::new(cmd), + Command::Pull(cmd) => Box::new(cmd), + Command::Mount(cmd) => Box::new(cmd), } } } @@ -64,4 +87,10 @@ impl Cli { pub enum Command { /// Run a container Run(RunCommand), + /// Generate container spec + Spec(SpecCommand), + // Pull a container image + Pull(PullCommand), + /// Mount an image into a rootfs to be used by a container + Mount(MountCommand), } diff --git a/src/cli/mount.rs b/src/cli/mount.rs new file mode 100644 index 0000000..9fa583f --- /dev/null +++ b/src/cli/mount.rs @@ -0,0 +1,44 @@ +use crate::helper::get_image_manager_instance; +use crate::{Handler, Result}; +use async_trait::async_trait; +use clap::Args; +use log::LevelFilter; + +/// Arguments for our `MountCommand`. +/// +/// These arguments are parsed by `clap` and an instance of `PullCommand` containing +/// arguments is provided. +/// +/// Example : +/// +/// `kaps pull registry.hub.docker.com/library/busybox` +/// +/// The `handler` method provided below will be executed. +#[derive(Debug, Args)] +pub struct MountCommand { + /// The image identifier. + image_id: String, + /// If set, the command will be executed silently. + #[clap(long, short)] + quiet: bool, +} + +#[async_trait] +impl Handler for MountCommand { + async fn handler(&self, logger: &mut env_logger::Builder) -> Result<()> { + // Change logger behavior and init it + // If the logger was not initialized, nothing will be displayed into the console. + if self.quiet { + logger.filter_level(LevelFilter::Off); + } + logger.init(); + + let mut im = get_image_manager_instance()?; + + // Create the bundle + let bundle = im.mount(&self.image_id).await?; + + println!("{}", &bundle.display()); + Ok(()) + } +} diff --git a/src/cli/pull.rs b/src/cli/pull.rs new file mode 100644 index 0000000..f732b27 --- /dev/null +++ b/src/cli/pull.rs @@ -0,0 +1,57 @@ +use crate::helper::get_image_manager_instance; +use crate::{Handler, Result}; +use async_trait::async_trait; +use clap::Args; +use log::LevelFilter; + +/// Arguments for our `PullCommand`. +/// +/// These arguments are parsed by `clap` and an instance of `PullCommand` containing +/// arguments is provided. +/// +/// Example : +/// +/// `kaps pull registry.hub.docker.com/library/busybox` +/// +/// The `handler` method provided below will be executed. +#[derive(Debug, Args)] +pub struct PullCommand { + /// The image to pull. + /// Example : registry.hub.docker.com/library/busybox + image: String, + /// By default, the image id will be generated by creating a unique hash for the image digest. + /// By using --name, you can provide a friendly identifier your image. + #[clap(long)] + name: Option, + /// By default, the command will not pull the image if it was already present. + /// If `--rm` is provided in arguments, the command will remove the previous image + /// before pulling the image. + #[clap(long = "rm")] + remove_existing: bool, + /// If set, the command will be executed silently. + #[clap(long, short)] + quiet: bool, +} + +#[async_trait] +impl Handler for PullCommand { + async fn handler(&self, logger: &mut env_logger::Builder) -> Result<()> { + // Change logger behavior and init it + // If the logger was not initialized, nothing will be displayed into the console. + if self.quiet { + logger.filter_level(LevelFilter::Off); + } + logger.init(); + + let mut im = get_image_manager_instance()?; + + let image_id = im + .pull(&self.image, &self.remove_existing, &self.name) + .await?; + + // Here we want to print the image_id + // so the user can get the output for scripting purposes + println!("{}", image_id); + Ok(()) + } +} diff --git a/src/cli/run.rs b/src/cli/run.rs index 80204c3..0f0b568 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -1,4 +1,5 @@ use crate::{Handler, Result}; +use async_trait::async_trait; use clap::Args; use container::Container; @@ -14,15 +15,19 @@ use container::Container; /// The `handler` method provided below will be executed. #[derive(Debug, Args)] pub struct RunCommand { + /// Name of the container instance that will be start. It must me unique on your host + name: String, + /// The bundle used by the container. #[clap(short, long)] bundle: String, } +#[async_trait] impl Handler for RunCommand { - fn handler(&self) -> Result<()> { - // Create a container by passing the bundle provided in arguments to it's constructor. - let container = Container::new(&self.bundle)?; + async fn handler(&self, _: &mut env_logger::Builder) -> Result<()> { + // Create a container by passing the bundle and the id provided in arguments to it's constructor. + let mut container = Container::new(&self.bundle, &self.name)?; // Run the container // At the moment, we don't have a detached mode for the container, diff --git a/src/cli/spec.rs b/src/cli/spec.rs new file mode 100644 index 0000000..5115417 --- /dev/null +++ b/src/cli/spec.rs @@ -0,0 +1,23 @@ +use std::path::PathBuf; + +use crate::{Handler, Result}; +use async_trait::async_trait; +use clap::Args; +use container::spec::{new_runtime_config, BUNDLE_CONFIG}; +use oci_spec::image::ImageConfiguration; + +use super::Error; + +#[derive(Debug, Args)] +pub struct SpecCommand {} + +#[async_trait] +impl Handler for SpecCommand { + async fn handler(&self, _: &mut env_logger::Builder) -> Result<()> { + let image_configuration = ImageConfiguration::default(); + let spec = new_runtime_config(Some(&image_configuration)).map_err(Error::Spec)?; + let bundle_config = PathBuf::from(".").join(BUNDLE_CONFIG); + spec.save(&bundle_config).map_err(Error::Spec)?; + Ok(()) + } +} diff --git a/src/helper.rs b/src/helper.rs new file mode 100644 index 0000000..4027a0d --- /dev/null +++ b/src/helper.rs @@ -0,0 +1,16 @@ +use crate::KAPS_DATA_DIR; +use oci_image::snapshots::overlay::OverlayFS; +use oci_image::ImageManager; +use std::path::Path; + +/// Create a new image manager instance, with OverlayFS as the snapshotter +pub fn get_image_manager_instance() -> oci_image::Result { + let data_dir = Path::new(KAPS_DATA_DIR); + let snapshots_dir = data_dir.join("snapshots"); + ImageManager::new( + data_dir, + Box::new(OverlayFS { + data_dir: snapshots_dir, + }), + ) +} diff --git a/src/main.rs b/src/main.rs index ac9c96a..c5ebdf2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,45 @@ use clap::Parser; +use log::{log_enabled, Level, LevelFilter}; +use std::io::Write; use crate::cli::{Cli, Handler, Result}; mod cli; +mod helper; -fn main() -> Result<()> { +pub const KAPS_DATA_DIR: &str = "/var/lib/kaps"; + +#[tokio::main] +async fn main() -> Result<()> { let cli: Cli = Cli::parse(); - cli.command().handler()?; + // Configure the logger + let mut builder = env_logger::Builder::new(); + let logger = builder + .filter_level(match cli.verbose { + 1 => LevelFilter::Debug, + 2 => LevelFilter::Trace, + _ => LevelFilter::Info, + }) + .format(|buf, record| { + if record.level() != Level::Info + || log_enabled!(Level::Trace) + || log_enabled!(Level::Debug) + { + return writeln!( + buf, + "{}: {}", + record.level().to_string().to_lowercase(), + record.args() + ); + } + writeln!(buf, "{}", record.args()) + }); + + // We have to pass the logger to our downstream command + // so that way the logger behavior can be changed by the command. + // The downstream command must INIT the logger before doing any task. + cli.command().handler(logger).await?; Ok(()) }