├── .cbindgen.toml ├── .codecov.yml ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml ├── grcov.yml ├── setup └── workflows │ ├── ci.yml │ └── gh-pages.yml ├── .gitignore ├── .rustfmt.toml ├── Cargo.lock ├── Cargo.toml ├── Cross.toml ├── LICENSE ├── Makefile ├── OWNERS ├── README.md ├── build.rs ├── crates ├── common │ ├── Cargo.toml │ └── src │ │ ├── capability.rs │ │ ├── lib.rs │ │ ├── seccomp.rs │ │ └── unix_stream.rs ├── container │ ├── Cargo.toml │ └── src │ │ ├── conmon.rs │ │ ├── container │ │ ├── local.rs │ │ └── mod.rs │ │ ├── lib.rs │ │ └── oci_runtime.rs ├── ffi │ ├── Cargo.toml │ ├── build.rs │ └── src │ │ ├── error.rs │ │ ├── ffi.h │ │ ├── lib.rs │ │ ├── log.rs │ │ └── network │ │ └── mod.rs ├── image │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── network │ ├── Cargo.toml │ └── src │ │ ├── cni │ │ ├── config.rs │ │ ├── exec.rs │ │ ├── iptables.rs │ │ ├── mod.rs │ │ ├── namespace.rs │ │ ├── netlink.rs │ │ ├── plugin.rs │ │ └── port.rs │ │ └── lib.rs ├── sandbox │ ├── Cargo.toml │ └── src │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── pinned.rs │ │ └── pinns.rs ├── server │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── services │ ├── Cargo.toml │ ├── build.rs │ └── src │ │ ├── cri │ │ ├── api │ │ │ ├── mod.rs │ │ │ └── runtime.v1alpha2.rs │ │ ├── cri_service.rs │ │ ├── image_service │ │ │ ├── image_fs_info.rs │ │ │ ├── image_status.rs │ │ │ ├── list_images.rs │ │ │ ├── mod.rs │ │ │ ├── pull_image.rs │ │ │ └── remove_image.rs │ │ ├── mod.rs │ │ ├── proto │ │ │ └── criapi.proto │ │ └── runtime_service │ │ │ ├── attach.rs │ │ │ ├── container_stats.rs │ │ │ ├── container_status.rs │ │ │ ├── create_container.rs │ │ │ ├── exec.rs │ │ │ ├── exec_sync.rs │ │ │ ├── list_container_stats.rs │ │ │ ├── list_containers.rs │ │ │ ├── list_pod_sandbox.rs │ │ │ ├── mod.rs │ │ │ ├── pod_sandbox_status.rs │ │ │ ├── port_forward.rs │ │ │ ├── remove_container.rs │ │ │ ├── remove_pod_sandbox.rs │ │ │ ├── reopen_container_log.rs │ │ │ ├── run_pod_sandbox.rs │ │ │ ├── start_container.rs │ │ │ ├── status.rs │ │ │ ├── stop_container.rs │ │ │ ├── stop_pod_sandbox.rs │ │ │ ├── update_container_resources.rs │ │ │ ├── update_runtime_config.rs │ │ │ └── version.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ └── server │ │ ├── config.rs │ │ └── mod.rs └── storage │ ├── Cargo.toml │ └── src │ ├── default_key_value_storage.rs │ ├── lib.rs │ └── memory_key_value_storage.rs └── tests ├── Cargo.toml ├── cni └── 99-loopback.conf └── src ├── common.rs ├── criapi ├── mod.rs └── runtime.v1alpha2.rs ├── e2e └── mod.rs ├── integration_tests ├── mod.rs ├── network │ ├── cni │ │ ├── config.rs │ │ ├── mod.rs │ │ └── port.rs │ └── mod.rs └── services │ ├── cri │ ├── mod.rs │ ├── run_pod_sandbox.rs │ └── version.rs │ └── mod.rs └── lib.rs /.cbindgen.toml: -------------------------------------------------------------------------------- 1 | language = "C" 2 | pragma_once = true 3 | line_length = 80 4 | documentation_style = "doxy" 5 | 6 | [enum] 7 | rename_variants = "SnakeCase" 8 | prefix_with_name = true 9 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | --- 2 | codecov: 3 | notify: 4 | after_n_builds: 1 5 | require_ci_to_pass: false 6 | 7 | coverage: 8 | precision: 2 9 | round: down 10 | range: 50..75 11 | 12 | comment: 13 | layout: "header, diff" 14 | behavior: default 15 | require_changes: false 16 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 24 | 25 | #### What type of PR is this? 26 | 27 | 31 | 32 | > /kind api-change 33 | > /kind bug 34 | > /kind cleanup 35 | > /kind dependency-change 36 | > /kind deprecation 37 | > /kind design 38 | > /kind documentation 39 | > /kind failing-test 40 | > /kind feature 41 | > /kind flake 42 | 43 | #### What this PR does / why we need it: 44 | 45 | #### Which issue(s) this PR fixes: 46 | 47 | 51 | 52 | 57 | 58 | #### Special notes for your reviewer: 59 | 60 | #### Does this PR introduce a user-facing change? 61 | 62 | 71 | 72 | ```release-note 73 | 74 | ``` 75 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "11:00" 8 | open-pull-requests-limit: 10 9 | labels: 10 | - "release-note-none" 11 | allow: 12 | - dependency-type: direct 13 | - dependency-type: indirect 14 | - package-ecosystem: "github-actions" 15 | directory: "/" 16 | schedule: 17 | interval: daily 18 | open-pull-requests-limit: 10 19 | labels: 20 | - "release-note-none" 21 | -------------------------------------------------------------------------------- /.github/grcov.yml: -------------------------------------------------------------------------------- 1 | branch: true 2 | ignore-not-existing: true 3 | llvm: true 4 | filter: covered 5 | output-type: lcov 6 | output-path: ./lcov.info 7 | prefix-dir: /home/user/build/ 8 | -------------------------------------------------------------------------------- /.github/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euox pipefail 3 | 4 | sudo apt-get install -y protobuf-compiler 5 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | pull_request: {} 4 | push: 5 | branches: 6 | - master 7 | env: 8 | CARGO_TERM_COLOR: always 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | - run: .github/setup 16 | - uses: actions-rs/toolchain@v1 17 | with: 18 | toolchain: stable 19 | override: true 20 | - name: Setup Cache 21 | uses: actions/cache@v3 22 | with: 23 | path: | 24 | ~/.cargo/registry 25 | ~/.cargo/git 26 | target 27 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 28 | - name: Build 29 | run: make 30 | 31 | build-lib: 32 | strategy: 33 | matrix: 34 | target: 35 | - x86_64-unknown-linux-gnu 36 | - i686-unknown-linux-gnu 37 | - aarch64-unknown-linux-gnu 38 | name: build-lib-release-${{ matrix.target }} 39 | runs-on: ubuntu-latest 40 | steps: 41 | - name: Checkout 42 | uses: actions/checkout@v3 43 | - run: .github/setup 44 | - uses: actions-rs/toolchain@v1 45 | with: 46 | toolchain: stable 47 | target: ${{ matrix.target }} 48 | override: true 49 | - name: Setup Cache 50 | uses: actions/cache@v3 51 | with: 52 | path: | 53 | ~/.cargo/registry 54 | ~/.cargo/git 55 | target 56 | key: build-lib-release-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }} 57 | - name: Install cross-rs 58 | run: | 59 | cargo install cross --git https://github.com/cross-rs/cross 60 | cross --version 61 | - name: Ensure the latest base image 62 | run: docker pull ghcr.io/cross-rs/${{matrix.target}}:main 63 | - name: Build for ${{matrix.target}} 64 | run: cross build -v --target ${{matrix.target}} 65 | - uses: actions/upload-artifact@v3 66 | with: 67 | name: lib 68 | path: lib-*.tar.gz 69 | 70 | build-bin-release-static: 71 | runs-on: ubuntu-latest 72 | steps: 73 | - name: Checkout 74 | uses: actions/checkout@v3 75 | - name: Build Release Static 76 | run: | 77 | sudo make build-bin-release-static 78 | sudo chown -R $(id -u):$(id -g) ~/.cargo target 79 | - uses: actions/upload-artifact@v3 80 | with: 81 | name: server 82 | path: target/x86_64-unknown-linux-musl/release/server 83 | 84 | doc: 85 | runs-on: ubuntu-latest 86 | steps: 87 | - name: Checkout 88 | uses: actions/checkout@v3 89 | - run: .github/setup 90 | - uses: actions-rs/toolchain@v1 91 | with: 92 | toolchain: stable 93 | override: true 94 | - name: Setup Cache 95 | uses: actions/cache@v3 96 | with: 97 | path: | 98 | ~/.cargo/registry 99 | ~/.cargo/git 100 | target 101 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 102 | - name: Documentation 103 | run: make doc 104 | 105 | lint-clippy: 106 | runs-on: ubuntu-latest 107 | steps: 108 | - name: Checkout 109 | uses: actions/checkout@v3 110 | - run: .github/setup 111 | - uses: actions-rs/toolchain@v1 112 | with: 113 | toolchain: stable 114 | override: true 115 | - name: Setup Cache 116 | uses: actions/cache@v3 117 | with: 118 | path: | 119 | ~/.cargo/registry 120 | ~/.cargo/git 121 | target 122 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 123 | - name: Clippy Lint 124 | run: make lint-clippy 125 | 126 | lint-rustfmt: 127 | runs-on: ubuntu-latest 128 | steps: 129 | - name: Checkout 130 | uses: actions/checkout@v3 131 | - uses: actions-rs/toolchain@v1 132 | with: 133 | toolchain: stable 134 | override: true 135 | - name: Rustfmt 136 | run: make lint-rustfmt 137 | 138 | test-coverage: 139 | runs-on: ubuntu-latest 140 | steps: 141 | - name: Checkout 142 | uses: actions/checkout@v3 143 | - run: .github/setup 144 | - name: Select nightly Toolchain 145 | uses: actions-rs/toolchain@v1 146 | with: 147 | toolchain: nightly 148 | override: true 149 | - name: Install rustfmt 150 | shell: bash 151 | run: rustup component add rustfmt 152 | - name: Install pinns 153 | run: make download-pinns 154 | - name: Unit tests 155 | uses: actions-rs/cargo@v1 156 | with: 157 | command: test 158 | args: --lib --workspace --exclude tests --no-fail-fast 159 | env: 160 | CARGO_INCREMENTAL: '0' 161 | RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests' 162 | RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests' 163 | - name: Coverage 164 | uses: actions-rs/grcov@v0.1 165 | with: 166 | config: .github/grcov.yml 167 | - name: Upload Results 168 | uses: codecov/codecov-action@v3 169 | 170 | test-unit: 171 | runs-on: ubuntu-latest 172 | steps: 173 | - name: Checkout 174 | uses: actions/checkout@v3 175 | - run: .github/setup 176 | - uses: actions-rs/toolchain@v1 177 | with: 178 | toolchain: stable 179 | override: true 180 | - name: Setup Cache 181 | uses: actions/cache@v3 182 | with: 183 | path: | 184 | ~/.cargo/registry 185 | ~/.cargo/git 186 | target 187 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 188 | - name: Install pinns 189 | run: make download-pinns 190 | - name: Unit Tests 191 | run: make test-unit 192 | 193 | test-integration: 194 | runs-on: ubuntu-latest 195 | steps: 196 | - name: Checkout 197 | uses: actions/checkout@v3 198 | - run: .github/setup 199 | - uses: actions-rs/toolchain@v1 200 | with: 201 | toolchain: stable 202 | override: true 203 | - name: Install CNI plugins 204 | shell: bash 205 | run: | 206 | curl -sfL -o - https://github.com/containernetworking/plugins/releases/download/$VERSION/cni-plugins-linux-amd64-$VERSION.tgz | 207 | sudo tar xfz - -C /usr/local/bin 208 | bridge 209 | env: 210 | VERSION: v1.0.1 211 | - name: Setup Cache 212 | uses: actions/cache@v3 213 | with: 214 | path: | 215 | ~/.cargo/registry 216 | ~/.cargo/git 217 | target 218 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 219 | - name: Install pinns 220 | run: make download-pinns 221 | - name: Integration Tests 222 | run: | 223 | # Run all integration tests 224 | sudo -E env "PATH=$PATH" make test-integration 225 | 226 | # Fix permissions 227 | sudo chown -R $(id -u):$(id -g) ~/.cargo target 228 | env: 229 | RUST_TEST_THREADS: '1' 230 | 231 | test-e2e: 232 | runs-on: ubuntu-latest 233 | steps: 234 | - name: Checkout 235 | uses: actions/checkout@v3 236 | - uses: actions-rs/toolchain@v1 237 | with: 238 | toolchain: stable 239 | override: true 240 | - name: Setup Cache 241 | uses: actions/cache@v3 242 | with: 243 | path: | 244 | ~/.cargo/registry 245 | ~/.cargo/git 246 | target 247 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 248 | - name: Install pinns 249 | run: make download-pinns 250 | - name: End-to-End Tests 251 | run: make test-e2e 252 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: gh-pages 2 | on: 3 | push: 4 | branches: 5 | - master 6 | env: 7 | CARGO_TERM_COLOR: always 8 | jobs: 9 | update: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | - name: Setup Cache 15 | uses: actions/cache@v3 16 | with: 17 | path: | 18 | ~/.cargo/registry 19 | ~/.cargo/git 20 | target 21 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 22 | - name: Build Documentation 23 | run: make doc 24 | - name: Deploy Documentation 25 | uses: peaceiris/actions-gh-pages@v3 26 | with: 27 | deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} 28 | publish_branch: gh-pages 29 | publish_dir: ./target/doc 30 | force_orphan: true 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.orig 2 | /target 3 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 100 2 | hard_tabs = false 3 | tab_spaces = 4 4 | newline_style = "Auto" 5 | use_small_heuristics = "Default" 6 | indent_style = "Block" 7 | wrap_comments = false 8 | format_code_in_doc_comments = false 9 | comment_width = 80 10 | normalize_comments = false 11 | normalize_doc_attributes = false 12 | license_template_path = "" 13 | format_strings = false 14 | format_macro_matchers = false 15 | format_macro_bodies = true 16 | empty_item_single_line = true 17 | struct_lit_single_line = true 18 | fn_single_line = false 19 | where_single_line = false 20 | imports_indent = "Block" 21 | imports_layout = "Mixed" 22 | merge_imports = true 23 | reorder_imports = true 24 | reorder_modules = true 25 | reorder_impl_items = false 26 | type_punctuation_density = "Wide" 27 | space_before_colon = false 28 | space_after_colon = true 29 | spaces_around_ranges = false 30 | binop_separator = "Front" 31 | remove_nested_parens = true 32 | combine_control_expr = true 33 | overflow_delimited_expr = false 34 | struct_field_align_threshold = 0 35 | enum_discrim_align_threshold = 0 36 | match_arm_blocks = true 37 | force_multiline_blocks = false 38 | fn_args_layout = "Tall" 39 | brace_style = "SameLineWhere" 40 | control_brace_style = "AlwaysSameLine" 41 | trailing_semicolon = true 42 | trailing_comma = "Vertical" 43 | match_block_trailing_comma = false 44 | blank_lines_upper_bound = 1 45 | blank_lines_lower_bound = 0 46 | edition = "2018" 47 | version = "One" 48 | inline_attribute_width = 0 49 | merge_derives = true 50 | use_try_shorthand = false 51 | use_field_init_shorthand = false 52 | force_explicit_abi = true 53 | condense_wildcard_suffixes = false 54 | color = "Auto" 55 | unstable_features = false 56 | disable_all_formatting = false 57 | skip_children = false 58 | hide_parse_errors = false 59 | error_on_line_overflow = false 60 | error_on_unformatted = false 61 | report_todo = "Never" 62 | report_fixme = "Never" 63 | ignore = [] 64 | emit_mode = "Files" 65 | make_backup = false 66 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["crates/*", "tests"] 3 | -------------------------------------------------------------------------------- /Cross.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | pre-build = ["apt-get update && apt-get install -y protobuf-compiler"] 3 | 4 | [target.i686-unknown-linux-gnu] 5 | pre-build = ["dpkg --add-architecture i386 && apt-get update && apt-get install -y protobuf-compiler"] 6 | 7 | [target.aarch64-unknown-linux-gnu] 8 | pre-build = ["dpkg --add-architecture arm64 && apt-get update && apt-get install -y protobuf-compiler"] 9 | 10 | [target.powerpc64le-unknown-linux-gnu] 11 | pre-build = ["dpkg --add-architecture ppc64el && apt-get update && apt-get install -y protobuf-compiler"] 12 | 13 | [target.s390x-unknown-linux-gnu] 14 | pre-build = ["dpkg --add-architecture s390x && apt-get update && apt-get install -y protobuf-compiler"] 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CARGO ?= cargo 2 | 3 | export RUST_TEST_NOCAPTURE=1 4 | 5 | all: build ## Run the 'build' target 6 | 7 | .PHONY: build 8 | build: ## Build the main binary 9 | $(CARGO) build 10 | 11 | .PHONY: build-release 12 | build-release: ## Build the main binary in release mode 13 | $(CARGO) build --release 14 | 15 | .PHONY: build-bin-release-static 16 | build-bin-release-static: ## Build the main binary in release mode statically linked 17 | podman run -it \ 18 | --pull always \ 19 | -v "$(shell pwd)":/volume \ 20 | clux/muslrust:stable \ 21 | bash -c "\ 22 | rustup component add rustfmt && \ 23 | make build-release && \ 24 | strip -s target/x86_64-unknown-linux-musl/release/server" 25 | 26 | .PHONY: clean 27 | clean: ## Clean the work tree 28 | $(CARGO) clean 29 | 30 | .PHONY: doc 31 | doc: ## Build the documentation 32 | $(CARGO) doc --no-deps 33 | 34 | .PHONY: lint ## Run all linters 35 | lint: lint-clippy lint-rustfmt 36 | 37 | .PHONY: lint-clippy 38 | lint-clippy: ## Run the clippy linter 39 | $(CARGO) clippy --all -- -D warnings 40 | 41 | .PHONY: lint-rustfmt 42 | lint-rustfmt: ## Run the rustfmt linter 43 | $(CARGO) fmt && git diff --exit-code 44 | 45 | .PHONY: run 46 | run: ## Run the main binary 47 | $(CARGO) run 48 | 49 | define test 50 | $(CARGO) test \ 51 | --package tests \ 52 | $(1) $(ARGS) \ 53 | -- \ 54 | --nocapture \ 55 | $(FOCUS) 56 | endef 57 | 58 | .PHONY: test-integration 59 | test-integration: ## Run the integration tests 60 | $(call test,integration) 61 | 62 | .PHONY: test-e2e 63 | test-e2e: ## Run the e2e tests 64 | $(call test,e2e) 65 | 66 | .PHONY: test-unit 67 | test-unit: ## Run the unit tests 68 | $(CARGO) test --lib --workspace --exclude tests $(FOCUS) 69 | 70 | .PHONY: help 71 | help: ## Display this help 72 | @awk \ 73 | -v "col=${COLOR}" -v "nocol=${NOCOLOR}" \ 74 | ' \ 75 | BEGIN { \ 76 | FS = ":.*##" ; \ 77 | printf "Available targets:\n"; \ 78 | } \ 79 | /^[a-zA-Z0-9_-]+:.*?##/ { \ 80 | printf " %s%-25s%s %s\n", col, $$1, nocol, $$2 \ 81 | } \ 82 | /^##@/ { \ 83 | printf "\n%s%s%s\n", col, substr($$0, 5), nocol \ 84 | } \ 85 | ' $(MAKEFILE_LIST) 86 | 87 | .PHONY: download-pinns 88 | download-pinns: 89 | sudo curl -fsSLo /usr/local/bin/pinns https://github.com/Furisto/pinns.rs/releases/download/v0.1.1/pinns 90 | sudo chmod +x /usr/local/bin/pinns 91 | 92 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | approvers: 2 | - furisto 3 | - saschagrunert 4 | - utam0k 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # containrs - General purpose container library 2 | 3 | [![ci](https://github.com/containers/containrs/workflows/ci/badge.svg)](https://github.com/containers/containrs/actions) 4 | [![codecov](https://codecov.io/gh/containers/containrs/branch/master/graph/badge.svg)](https://codecov.io/gh/containers/containrs) 5 | [![docs](https://img.shields.io/badge/docs-master-blue.svg)](https://containers.github.io/containrs/containrs) 6 | 7 | This repository is a hacking space for a new general purpose container library 8 | written in Rust. 9 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{format_err, Context, Result}; 2 | use cbindgen::{Builder, Config}; 3 | use std::{env, path::PathBuf}; 4 | 5 | const PROTO_FILE: &str = "src/kubernetes/cri/proto/criapi.proto"; 6 | 7 | fn main() -> Result<()> { 8 | Builder::new() 9 | .with_crate(env::var("CARGO_MANIFEST_DIR")?) 10 | .with_config(Config::from_file(".cbindgen.toml").map_err(|e| format_err!(e))?) 11 | .generate() 12 | .context("generate bindings")? 13 | .write_to_file("src/ffi/ffi.h"); 14 | 15 | Ok(()) 16 | } 17 | -------------------------------------------------------------------------------- /crates/common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "common" 3 | version = "0.1.0" 4 | edition = "2018" 5 | authors = [ 6 | "Furisto", 7 | "Mrunal Patel ", 8 | "Sascha Grunert ", 9 | "utam0k ", 10 | ] 11 | documentation = "https://docs.rs/containrs" 12 | homepage = "https://github.com/containers/containrs" 13 | repository = "https://github.com/containers/containrs" 14 | license = "Apache-2.0" 15 | keywords = ["runtime", "kubernetes", "cri", "container", "pod"] 16 | categories = ["network-programming", "api-bindings"] 17 | 18 | [dependencies] 19 | anyhow = "1.0.66" 20 | derive_builder = "0.11.2" 21 | log = { version = "0.4.17", features = ["serde", "std"] } 22 | oci-spec = { version = "0.5.8", features = ["runtime"] } 23 | serde = { version = "1.0.147", features = ["derive"] } 24 | serde_json = "1.0.87" 25 | strum = { version = "0.24.1", features = ["derive"] } 26 | tokio = { version = "1.21.2", features = ["full"] } 27 | tonic = "0.8.2" 28 | 29 | [dev-dependencies] 30 | tempfile = "3.3.0" 31 | -------------------------------------------------------------------------------- /crates/common/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | pub mod capability; 4 | pub mod seccomp; 5 | pub mod unix_stream; 6 | 7 | #[derive(Clone, Debug)] 8 | pub struct Namespace { 9 | pub typ: NamespaceType, 10 | pub path: PathBuf, 11 | } 12 | 13 | #[derive(Clone, Debug)] 14 | pub enum NamespaceType { 15 | UTS, 16 | IPC, 17 | USER, 18 | NET, 19 | MOUNT, 20 | PID, 21 | } 22 | -------------------------------------------------------------------------------- /crates/common/src/unix_stream.rs: -------------------------------------------------------------------------------- 1 | #[cfg(unix)] 2 | use std::{ 3 | pin::Pin, 4 | task::{Context, Poll}, 5 | }; 6 | 7 | use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; 8 | use tonic::transport::server::Connected; 9 | 10 | #[derive(Debug)] 11 | pub struct UnixStream(pub tokio::net::UnixStream); 12 | 13 | impl Connected for UnixStream { 14 | type ConnectInfo = NoneConnectInfo; 15 | 16 | fn connect_info(&self) -> Self::ConnectInfo { 17 | NoneConnectInfo 18 | } 19 | } 20 | 21 | #[derive(Clone, Copy)] 22 | pub struct NoneConnectInfo; 23 | 24 | impl AsyncRead for UnixStream { 25 | fn poll_read( 26 | mut self: Pin<&mut Self>, 27 | cx: &mut Context<'_>, 28 | buf: &mut ReadBuf<'_>, 29 | ) -> Poll> { 30 | Pin::new(&mut self.0).poll_read(cx, buf) 31 | } 32 | } 33 | 34 | impl AsyncWrite for UnixStream { 35 | fn poll_write( 36 | mut self: Pin<&mut Self>, 37 | cx: &mut Context<'_>, 38 | buf: &[u8], 39 | ) -> Poll> { 40 | Pin::new(&mut self.0).poll_write(cx, buf) 41 | } 42 | 43 | fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 44 | Pin::new(&mut self.0).poll_flush(cx) 45 | } 46 | 47 | fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 48 | Pin::new(&mut self.0).poll_shutdown(cx) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /crates/container/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "container" 3 | version = "0.1.0" 4 | edition = "2018" 5 | authors = [ 6 | "Furisto", 7 | "Mrunal Patel ", 8 | "Sascha Grunert ", 9 | "utam0k ", 10 | ] 11 | documentation = "https://docs.rs/containrs" 12 | homepage = "https://github.com/containers/containrs" 13 | repository = "https://github.com/containers/containrs" 14 | license = "Apache-2.0" 15 | keywords = ["runtime", "kubernetes", "cri", "container", "pod"] 16 | categories = ["network-programming", "api-bindings"] 17 | 18 | [dependencies] 19 | anyhow = "1.0.66" 20 | async-trait = "0.1.58" 21 | derive_builder = "0.11.2" 22 | dyn-clone = "1.0.9" 23 | getset = "0.1.2" 24 | log = { version = "0.4.17", features = ["serde", "std"] } 25 | oci-spec = { version = "0.5.8", features = ["runtime"] } 26 | serde = { version = "1.0.147", features = ["derive"] } 27 | strum = { version = "0.24.1", features = ["derive"] } 28 | tokio = { version = "1.21.2", features = ["full"] } 29 | 30 | [dev-dependencies] 31 | which = "4.3.0" 32 | -------------------------------------------------------------------------------- /crates/container/src/conmon.rs: -------------------------------------------------------------------------------- 1 | //! Interface to [conmon][0], the OCI container runtime monitor. 2 | //! 3 | //! [0]: https://github.com/containers/conmon 4 | 5 | #![allow(dead_code)] 6 | 7 | use anyhow::{Context, Result}; 8 | use async_trait::async_trait; 9 | use derive_builder::Builder; 10 | use dyn_clone::{clone_trait_object, DynClone}; 11 | use getset::{Getters, Setters}; 12 | use log::LevelFilter; 13 | use std::{ 14 | fmt::{self, Debug}, 15 | path::{Path, PathBuf}, 16 | process::Output, 17 | string::ToString, 18 | time::Duration, 19 | }; 20 | use strum::AsRefStr; 21 | use tokio::process::Command; 22 | 23 | #[derive(Builder, Debug, Getters, Setters)] 24 | #[builder(pattern = "owned", setter(into))] 25 | // Conmon is the main structure to be used when interacting with the container monitor. 26 | pub struct Conmon { 27 | #[getset(get, set)] 28 | #[builder(private, default = "Box::new(DefaultExecCommand)")] 29 | /// The executor for conmon 30 | exec: Box, 31 | 32 | #[get] 33 | /// Path to the conmon binary 34 | binary: PathBuf, 35 | } 36 | 37 | impl Conmon { 38 | /// Run conmon with the provided args and return the output if the command execution succeeds. 39 | /// This can still mean that conmon itself failed, which can be verified via the exist status 40 | /// of the output. 41 | pub async fn run(&self, args: &[Arg]) -> Result { 42 | self.exec().run_output(self.binary(), args).await 43 | } 44 | } 45 | 46 | #[derive(Clone, Default, Debug)] 47 | /// DefaultExecCommand is a wrapper which can be used to execute conmon in a standard way. 48 | struct DefaultExecCommand; 49 | 50 | impl ExecCommand for DefaultExecCommand {} 51 | 52 | #[async_trait] 53 | trait ExecCommand: Debug + DynClone + Send + Sync { 54 | /// Run a command and return its `Output`. 55 | async fn run_output(&self, binary: &Path, args: &[Arg]) -> Result { 56 | Command::new(binary) 57 | .args(args.iter().map(ToString::to_string)) 58 | .output() 59 | .await 60 | .context("run conmon") 61 | } 62 | } 63 | 64 | clone_trait_object!(ExecCommand); 65 | 66 | #[derive(AsRefStr, Clone, Debug, Hash, Eq, PartialEq)] 67 | #[strum(serialize_all = "kebab_case")] 68 | #[allow(clippy::enum_variant_names)] 69 | /// Available arguments for conmon. 70 | pub enum Arg { 71 | /// Terminal. 72 | Terminal, 73 | 74 | /// Stdin. 75 | Stdin, 76 | 77 | /// Leave stdin open when attached client disconnects. 78 | LeaveStdinOpen, 79 | 80 | /// Container ID. 81 | Cid(String), 82 | 83 | /// Container UUID. 84 | Cuuid(String), 85 | 86 | /// Container name. 87 | Name(String), 88 | 89 | /// Runtime path. 90 | Runtime(PathBuf), 91 | 92 | /// Restore a container from a checkpoint. 93 | Restore(String), 94 | 95 | /// Additional opts to pass to the restore or exec command. Can be specified multiple times. 96 | RuntimeOpt(String), 97 | 98 | /// Additional arg to pass to the runtime. Can be specified multiple times. 99 | RuntimeArg(String), 100 | 101 | /// Attach to an exec session. 102 | ExecAttach, 103 | 104 | /// Do not create a new session keyring for the container. 105 | NoNewKeyring, 106 | 107 | /// Do not use pivot_root. 108 | NoPivot, 109 | 110 | /// Replace listen pid if set for oci-runtime pid. 111 | ReplaceListenPid, 112 | 113 | /// Bundle path. 114 | Bundle(PathBuf), 115 | 116 | /// Persistent directory for a container that can be used for storing container data. 117 | PersistDir(PathBuf), 118 | 119 | /// Container PID file. 120 | ContainerPidfile(PathBuf), 121 | 122 | /// Conmon daemon PID file. 123 | ConmonPidfile(PathBuf), 124 | 125 | /// Enable systemd cgroup manager. 126 | SystemdCgroup, 127 | 128 | /// Exec a command in a running container. 129 | Exec, 130 | 131 | /// Conmon API version to use. 132 | ApiVersion(String), 133 | 134 | /// Path to the process spec for exec. 135 | ExecProcessSpec(PathBuf), 136 | 137 | /// Path to the directory where exit files are written. 138 | ExitDir(PathBuf), 139 | 140 | /// Path to the program to execute when the container terminates its execution. 141 | ExitCommand(PathBuf), 142 | 143 | /// Delay before invoking the exit command (in seconds). 144 | ExitDelay(Duration), 145 | 146 | /// Additional arg to pass to the exit command. Can be specified multiple times. 147 | ExitCommandArg(String), 148 | 149 | /// Log file path. 150 | LogPath(PathBuf), 151 | 152 | /// Timeout in seconds. 153 | Timeout(Duration), 154 | 155 | /// Maximum size of log file. 156 | LogSizeMax(u64), 157 | 158 | /// Location of container attach sockets. 159 | SocketDirPath(PathBuf), 160 | 161 | /// Log to syslog (use with cgroupfs cgroup manager). 162 | Syslog, 163 | 164 | /// Print debug logs based on log level. 165 | LogLevel(LevelFilter), 166 | 167 | /// Additional tag to use for logging. 168 | LogTag(String), 169 | 170 | /// Do not manually call sync on logs after container shutdown. 171 | NoSyncLog, 172 | 173 | /// Allowing caller to keep the main conmon process as its child by only forking once. 174 | Sync, 175 | } 176 | 177 | impl fmt::Display for Arg { 178 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 179 | use crate::conmon::Arg::*; 180 | write!(f, "--")?; 181 | 182 | fn write_kv(f: &mut fmt::Formatter<'_>, key: K, value: V) -> fmt::Result 183 | where 184 | K: AsRef, 185 | V: fmt::Display, 186 | { 187 | write!(f, "{}={}", key.as_ref(), value) 188 | } 189 | 190 | match self { 191 | Cid(id) => write_kv(f, self, id), 192 | Cuuid(uuid) => write_kv(f, self, uuid), 193 | Name(name) => write_kv(f, self, name), 194 | Runtime(path) => write_kv(f, self, path.display()), 195 | Restore(checkpoint) => write_kv(f, self, checkpoint), 196 | RuntimeOpt(opt) => write_kv(f, self, opt), 197 | RuntimeArg(opt) => write_kv(f, self, opt), 198 | Bundle(path) => write_kv(f, self, path.display()), 199 | PersistDir(path) => write_kv(f, self, path.display()), 200 | ContainerPidfile(path) => write_kv(f, self, path.display()), 201 | ConmonPidfile(path) => write_kv(f, self, path.display()), 202 | ApiVersion(version) => write_kv(f, self, version), 203 | ExecProcessSpec(path) => write_kv(f, self, path.display()), 204 | ExitDir(path) => write_kv(f, self, path.display()), 205 | ExitCommand(path) => write_kv(f, self, path.display()), 206 | ExitDelay(duration) => write_kv(f, self, duration.as_secs()), 207 | ExitCommandArg(arg) => write_kv(f, self, arg), 208 | LogPath(path) => write_kv(f, self, path.display()), 209 | Timeout(duration) => write_kv(f, self, duration.as_secs()), 210 | LogSizeMax(size) => write_kv(f, self, size), 211 | SocketDirPath(path) => write_kv(f, self, path.display()), 212 | LogLevel(level) => write_kv(f, self, level), 213 | LogTag(tag) => write_kv(f, self, tag), 214 | _ => write!(f, "{}", self.as_ref()), 215 | } 216 | } 217 | } 218 | 219 | #[cfg(test)] 220 | mod tests { 221 | use super::*; 222 | use std::{os::unix::process::ExitStatusExt, process::ExitStatus}; 223 | 224 | #[derive(Clone, Debug)] 225 | struct MockExecCommand(Output); 226 | 227 | #[async_trait] 228 | impl ExecCommand for MockExecCommand { 229 | async fn run_output(&self, _binary: &Path, _args: &[Arg]) -> Result { 230 | Ok(self.0.clone()) 231 | } 232 | } 233 | 234 | #[tokio::test] 235 | async fn conmon_success_run() -> Result<()> { 236 | let conmon = ConmonBuilder::default() 237 | .binary(which::which("echo")?) 238 | .build()?; 239 | let output = conmon.run(&[Arg::Exec]).await?; 240 | assert!(output.status.success()); 241 | assert!(String::from_utf8(output.stderr)?.is_empty()); 242 | assert_eq!(String::from_utf8(output.stdout)?, "--exec\n"); 243 | Ok(()) 244 | } 245 | 246 | #[tokio::test] 247 | async fn conmon_success_run_mocked() -> Result<()> { 248 | let mut conmon = ConmonBuilder::default().binary("").build()?; 249 | conmon.set_exec(Box::new(MockExecCommand(Output { 250 | status: ExitStatus::from_raw(0), 251 | stdout: vec![1, 2, 3], 252 | stderr: vec![4, 5, 6], 253 | }))); 254 | 255 | let output = conmon 256 | .run(&[Arg::Cid("cid".into()), Arg::Cuuid("uuid".into())]) 257 | .await?; 258 | 259 | assert!(output.status.success()); 260 | assert_eq!(output.stdout, vec![1, 2, 3]); 261 | assert_eq!(output.stderr, vec![4, 5, 6]); 262 | Ok(()) 263 | } 264 | 265 | #[test] 266 | fn conmon_success() { 267 | assert!(ConmonBuilder::default() 268 | .binary("/some/binary") 269 | .build() 270 | .is_ok()) 271 | } 272 | 273 | #[test] 274 | fn conmon_failure_no_binary() { 275 | assert!(ConmonBuilder::default().build().is_err()) 276 | } 277 | 278 | #[test] 279 | fn conmon_success_arg_to_string() { 280 | assert_eq!(&Arg::Terminal.to_string(), "--terminal"); 281 | assert_eq!( 282 | &Arg::LogPath("/test/path".into()).to_string(), 283 | "--log-path=/test/path" 284 | ); 285 | assert_eq!( 286 | &Arg::LogLevel(LevelFilter::Info).to_string(), 287 | "--log-level=INFO" 288 | ); 289 | assert_eq!(&Arg::LogTag("test".into()).to_string(), "--log-tag=test"); 290 | assert_eq!(&Arg::NoSyncLog.to_string(), "--no-sync-log"); 291 | assert_eq!(&Arg::ReplaceListenPid.to_string(), "--replace-listen-pid"); 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /crates/container/src/container/local.rs: -------------------------------------------------------------------------------- 1 | //! A local Command Line Interface based OCI runtime implementation. The most commonly known are 2 | //! [runc][0] and [crun][1]. 3 | //! 4 | //! [0]: https://github.com/opencontainers/runc 5 | //! [1]: https://github.com/containers/crun 6 | 7 | use std::path::PathBuf; 8 | 9 | use anyhow::Result; 10 | use async_trait::async_trait; 11 | use derive_builder::Builder; 12 | use getset::Getters; 13 | use oci_spec::runtime::{LinuxResources, Spec}; 14 | use serde::{Deserialize, Serialize}; 15 | use tokio::{process::Command, signal::unix::SignalKind}; 16 | 17 | use super::{Container, ContainerState, ContainerStats}; 18 | 19 | #[derive(Debug, Default, Builder, Getters, Serialize, Deserialize)] 20 | #[builder(default, pattern = "owned", setter(into, strip_option))] 21 | /// A general OCI container implementation. 22 | pub struct OCIContainer { 23 | #[get = "pub"] 24 | /// Unique identifier of the container. 25 | id: String, 26 | 27 | #[get = "pub"] 28 | log_path: PathBuf, 29 | 30 | #[get = "pub"] 31 | /// OCI Runtime Specification of the container. 32 | spec: Spec, 33 | } 34 | 35 | #[async_trait] 36 | impl Container for OCIContainer { 37 | /// Create a new container, which should be in the `Created` state afterwards. 38 | async fn create(&mut self) -> Result<()> { 39 | Ok(()) 40 | } 41 | 42 | /// Execute the user defined process in a created container. 43 | async fn start(&mut self) -> Result<()> { 44 | unimplemented!() 45 | } 46 | 47 | /// Delete any resources held by the container often used with detached container. 48 | async fn delete(&mut self) -> Result<()> { 49 | unimplemented!() 50 | } 51 | 52 | /// Suspend all processes inside the container. 53 | async fn pause(&mut self) -> Result<()> { 54 | unimplemented!() 55 | } 56 | 57 | /// Resumes all processes that have been previously paused. 58 | async fn resume(&mut self) -> Result<()> { 59 | unimplemented!() 60 | } 61 | 62 | /// Send the specified signal to the container's init process. 63 | async fn kill(&mut self, _signal_kind: SignalKind) -> Result<()> { 64 | unimplemented!() 65 | } 66 | 67 | /// Update container resource constraints. 68 | async fn update(&mut self, _resources: &LinuxResources) -> Result<()> { 69 | unimplemented!() 70 | } 71 | 72 | /// Execute the provided process inside the container. 73 | async fn exec(&self, _command: &Command) -> Result<()> { 74 | unimplemented!() 75 | } 76 | 77 | /// Retrieve container resource statistics. 78 | async fn stats(&self) -> Result { 79 | unimplemented!() 80 | } 81 | 82 | /// Retrieve the state of a container. 83 | async fn state(&self) -> Result { 84 | unimplemented!() 85 | } 86 | } 87 | 88 | #[cfg(test)] 89 | mod tests { 90 | use super::*; 91 | 92 | #[test] 93 | fn container_create() -> Result<()> { 94 | let container = OCIContainerBuilder::default().id("id").build()?; 95 | assert_eq!(container.id(), "id"); 96 | assert_eq!(container.spec(), &Spec::default()); 97 | Ok(()) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /crates/container/src/container/mod.rs: -------------------------------------------------------------------------------- 1 | //! OCI container implementations. 2 | 3 | use anyhow::Result; 4 | use async_trait::async_trait; 5 | use oci_spec::runtime::LinuxResources; 6 | use serde::{de::DeserializeOwned, Serialize}; 7 | use strum::{AsRefStr, Display, EnumString, IntoStaticStr}; 8 | use tokio::{process::Command, signal::unix::SignalKind}; 9 | 10 | pub mod local; 11 | 12 | #[async_trait] 13 | /// Container is the trait for implementing possible interactions with an OCI compatible container. 14 | pub trait Container 15 | where 16 | Self: Sized + Send + Sync + Serialize + DeserializeOwned, 17 | { 18 | /// Create a new container, which should be in the `Created` state afterwards. 19 | async fn create(&mut self) -> Result<()>; 20 | 21 | /// Execute the user defined process in a created container. 22 | async fn start(&mut self) -> Result<()>; 23 | 24 | /// Delete any resources held by the container often used with detached container. 25 | async fn delete(&mut self) -> Result<()>; 26 | 27 | /// Suspend all processes inside the container. 28 | async fn pause(&mut self) -> Result<()>; 29 | 30 | /// Resumes all processes that have been previously paused. 31 | async fn resume(&mut self) -> Result<()>; 32 | 33 | /// Send the specified signal to the container's init process. 34 | async fn kill(&mut self, signal_kind: SignalKind) -> Result<()>; 35 | 36 | /// Update container resource constraints. 37 | async fn update(&mut self, resources: &LinuxResources) -> Result<()>; 38 | 39 | /// Execute the provided process inside the container. 40 | async fn exec(&self, command: &Command) -> Result<()>; 41 | 42 | /// Retrieve container resource statistics. 43 | async fn stats(&self) -> Result; 44 | 45 | /// Retrieve the state of a container. 46 | async fn state(&self) -> Result; 47 | } 48 | 49 | #[derive(Debug, Default)] 50 | /// Container resource statistics. 51 | pub struct ContainerStats; 52 | 53 | #[derive(AsRefStr, Clone, Copy, Debug, Display, EnumString, Eq, Hash, IntoStaticStr, PartialEq)] 54 | #[strum(serialize_all = "snake_case")] 55 | /// Possible container states. 56 | pub enum ContainerState { 57 | /// The container has been created (default state). 58 | Created, 59 | 60 | /// The container is running, usually after calling its `start()` trait method. 61 | Started, 62 | 63 | /// The container is paused, usually after calling its `pause()` trait method. 64 | Paused, 65 | 66 | /// The container is stopped, usually after calling its `kill()` trait method. 67 | Killed, 68 | } 69 | -------------------------------------------------------------------------------- /crates/container/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Open Container Initiative (OCI) related implementations 2 | 3 | mod conmon; 4 | pub mod container; 5 | pub mod oci_runtime; 6 | -------------------------------------------------------------------------------- /crates/ffi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ffi" 3 | version = "0.1.0" 4 | edition = "2018" 5 | authors = [ 6 | "Furisto", 7 | "Mrunal Patel ", 8 | "Sascha Grunert ", 9 | "utam0k ", 10 | ] 11 | documentation = "https://docs.rs/containrs" 12 | homepage = "https://github.com/containers/containrs" 13 | repository = "https://github.com/containers/containrs" 14 | license = "Apache-2.0" 15 | keywords = ["runtime", "kubernetes", "cri", "container", "pod"] 16 | categories = ["network-programming", "api-bindings"] 17 | 18 | [dependencies] 19 | anyhow = "1.0.66" 20 | clap = { version = "4.0.26", features = ["cargo", "derive", "env", "wrap_help"] } 21 | env_logger = "0.9.3" 22 | libc = "0.2.137" 23 | log = { version = "0.4.17", features = ["serde", "std"] } 24 | strum = { version = "0.24.1", features = ["derive"] } 25 | 26 | [build-dependencies] 27 | anyhow = "1.0.66" 28 | cbindgen = "0.24.3" 29 | -------------------------------------------------------------------------------- /crates/ffi/build.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{format_err, Context, Result}; 2 | use cbindgen::{Builder, Config}; 3 | use std::{env, path::PathBuf}; 4 | 5 | fn main() -> Result<()> { 6 | let bindgen_config = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?) 7 | .join("..") 8 | .join("..") 9 | .join(".cbindgen.toml"); 10 | 11 | Builder::new() 12 | .with_crate(env::var("CARGO_MANIFEST_DIR")?) 13 | .with_config(Config::from_file(bindgen_config).map_err(|e| format_err!(e))?) 14 | .generate() 15 | .context("generate bindings")? 16 | .write_to_file("src/ffi.h"); 17 | 18 | Ok(()) 19 | } 20 | -------------------------------------------------------------------------------- /crates/ffi/src/error.rs: -------------------------------------------------------------------------------- 1 | //! error handling FFI interface 2 | 3 | use anyhow::{Error, Result}; 4 | use libc::{c_char, c_int}; 5 | use log::{trace, warn}; 6 | use std::{cell::RefCell, ptr, slice}; 7 | 8 | thread_local! { 9 | static LAST_ERROR: RefCell> = RefCell::new(None); 10 | } 11 | 12 | /// Update the last error by the provided one. 13 | pub fn update_last_error(err: Error) { 14 | trace!("Setting last error: {:#}", err); 15 | LAST_ERROR.with(|prev| *prev.borrow_mut() = Some(err)); 16 | } 17 | 18 | /// Remove the last error. 19 | pub fn remove_last_error() { 20 | trace!("Removing last error"); 21 | LAST_ERROR.with(|prev| *prev.borrow_mut() = None); 22 | } 23 | 24 | /// Update the last erorr if the provided result contains one. 25 | pub fn update_last_err_if_required(res: Result) { 26 | match res { 27 | Err(e) => update_last_error(e), 28 | Ok(_) => remove_last_error(), 29 | }; 30 | } 31 | 32 | /// Calculate the number of bytes in the last error's error message including a 33 | /// trailing `null` character. If there are no recent error, then this returns 34 | /// `0`. 35 | #[no_mangle] 36 | pub extern "C" fn last_error_length() -> c_int { 37 | LAST_ERROR.with(|prev| match *prev.borrow() { 38 | Some(ref err) => format!("{:#}", err).len() as c_int + 1, 39 | None => 0, 40 | }) 41 | } 42 | 43 | /// Write the most recent error message into a caller-provided buffer as a UTF-8 44 | /// string, returning the number of bytes written. 45 | /// 46 | /// # Note 47 | /// 48 | /// This writes a **UTF-8** string into the buffer. Windows users may need to 49 | /// convert it to a UTF-16 "unicode" afterwards. 50 | /// 51 | /// If there are no recent errors then this returns `0` (because we wrote 0 52 | /// bytes). `-1` is returned if there are argument based errors, for example 53 | /// when passed a `null` pointer or a buffer of insufficient size. 54 | #[no_mangle] 55 | pub extern "C" fn last_error_message(buffer: *mut c_char, length: c_int) -> c_int { 56 | if buffer.is_null() { 57 | warn!("provided buffer is null"); 58 | return -1; 59 | } 60 | 61 | // Retrieve the most recent error, clearing it in the process. 62 | let last_error = match LAST_ERROR.with(|prev| prev.borrow_mut().take()) { 63 | Some(err) => err, 64 | None => return 0, 65 | }; 66 | 67 | let error_message = format!("{:#}", last_error); 68 | let buffer = unsafe { slice::from_raw_parts_mut(buffer as *mut u8, length as usize) }; 69 | 70 | if error_message.len() >= buffer.len() { 71 | warn!("Buffer provided for writing the last error message is too small"); 72 | warn!( 73 | "Expected at least {} bytes but got {}", 74 | error_message.len() + 1, 75 | buffer.len() 76 | ); 77 | return -1; 78 | } 79 | 80 | unsafe { 81 | ptr::copy_nonoverlapping( 82 | error_message.as_ptr(), 83 | buffer.as_mut_ptr(), 84 | error_message.len(), 85 | ) 86 | }; 87 | 88 | // Add a trailing null so people using the string as a `char *` don't 89 | // accidentally read into garbage. 90 | buffer[error_message.len()] = 0; 91 | 92 | error_message.len() as c_int 93 | } 94 | 95 | #[cfg(test)] 96 | mod tests { 97 | use super::*; 98 | use anyhow::{anyhow, Result}; 99 | 100 | #[test] 101 | fn update_last_error_success() -> Result<()> { 102 | let mut buf = Vec::with_capacity(100); 103 | 104 | // No error 105 | assert!(LAST_ERROR.with(|prev| prev.borrow().is_none())); 106 | assert_eq!(last_error_length(), 0); 107 | assert_eq!(last_error_message(buf.as_mut_ptr() as *mut c_char, 0), 0); 108 | 109 | // Some error 110 | let err = anyhow!("some error"); 111 | update_last_error(err.context("some other error")); 112 | assert!(LAST_ERROR.with(|prev| prev.borrow().is_some())); 113 | assert_eq!(last_error_length(), 29); 114 | 115 | // But no buffer 116 | assert_eq!(last_error_message(ptr::null_mut(), 0), -1); 117 | 118 | // Or buffer is too small 119 | assert_eq!(last_error_message(buf.as_mut_ptr() as *mut c_char, 1), -1); 120 | 121 | // Error already taken 122 | assert!(LAST_ERROR.with(|prev| prev.borrow().is_none())); 123 | assert_eq!(last_error_length(), 0); 124 | 125 | // Insert new error 126 | update_last_error(anyhow!("new error")); 127 | assert!(LAST_ERROR.with(|prev| prev.borrow().is_some())); 128 | assert_eq!(last_error_length(), 10); 129 | 130 | // Buffer is large enough 131 | assert_eq!(last_error_message(buf.as_mut_ptr() as *mut c_char, 100), 9); 132 | 133 | Ok(()) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /crates/ffi/src/ffi.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | /** 9 | * An enum representing the available verbosity level filters of the logger. 10 | */ 11 | typedef enum LogLevel { 12 | /** 13 | * A level lower than all log levels. 14 | */ 15 | log_level_off, 16 | /** 17 | * Corresponds to the `Error` log level. 18 | */ 19 | log_level_error, 20 | /** 21 | * Corresponds to the `Warn` log level. 22 | */ 23 | log_level_warn, 24 | /** 25 | * Corresponds to the `Info` log level. 26 | */ 27 | log_level_info, 28 | /** 29 | * Corresponds to the `Debug` log level. 30 | */ 31 | log_level_debug, 32 | /** 33 | * Corresponds to the `Trace` log level. 34 | */ 35 | log_level_trace, 36 | } LogLevel; 37 | 38 | /** 39 | * Calculate the number of bytes in the last error's error message including a 40 | * trailing `null` character. If there are no recent error, then this returns 41 | * `0`. 42 | */ 43 | int last_error_length(void); 44 | 45 | /** 46 | * Write the most recent error message into a caller-provided buffer as a UTF-8 47 | * string, returning the number of bytes written. 48 | * 49 | * # Note 50 | * 51 | * This writes a **UTF-8** string into the buffer. Windows users may need to 52 | * convert it to a UTF-16 "unicode" afterwards. 53 | * 54 | * If there are no recent errors then this returns `0` (because we wrote 0 55 | * bytes). `-1` is returned if there are argument based errors, for example 56 | * when passed a `null` pointer or a buffer of insufficient size. 57 | */ 58 | int last_error_message(char *buffer, int length); 59 | 60 | /** 61 | * Init the log level by the provided level string. 62 | * Populates the last error on any failure. 63 | */ 64 | void log_init(enum LogLevel level); 65 | -------------------------------------------------------------------------------- /crates/ffi/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! FFI related implementations 2 | 3 | mod error; 4 | mod log; 5 | mod network; 6 | -------------------------------------------------------------------------------- /crates/ffi/src/log.rs: -------------------------------------------------------------------------------- 1 | //! Logging faccade for the FFI 2 | 3 | use crate::error::update_last_err_if_required; 4 | use anyhow::{Context, Result}; 5 | use clap::crate_name; 6 | use std::env; 7 | use strum::AsRefStr; 8 | 9 | #[repr(C)] 10 | #[allow(dead_code)] 11 | #[derive(Debug, AsRefStr)] 12 | #[strum(serialize_all = "snake_case")] 13 | /// An enum representing the available verbosity level filters of the logger. 14 | pub enum LogLevel { 15 | /// A level lower than all log levels. 16 | Off, 17 | 18 | /// Corresponds to the `Error` log level. 19 | Error, 20 | 21 | /// Corresponds to the `Warn` log level. 22 | Warn, 23 | 24 | /// Corresponds to the `Info` log level. 25 | Info, 26 | 27 | /// Corresponds to the `Debug` log level. 28 | Debug, 29 | 30 | /// Corresponds to the `Trace` log level. 31 | Trace, 32 | } 33 | 34 | #[no_mangle] 35 | /// Init the log level by the provided level string. 36 | /// Populates the last error on any failure. 37 | pub extern "C" fn log_init(level: LogLevel) { 38 | update_last_err_if_required(log_init_res(level)) 39 | } 40 | 41 | fn log_init_res(level: LogLevel) -> Result<()> { 42 | env::set_var("RUST_LOG", format!("{}={}", crate_name!(), level.as_ref())); 43 | env_logger::try_init().context("init log level") 44 | } 45 | 46 | #[cfg(test)] 47 | mod tests { 48 | use super::*; 49 | use crate::error::last_error_length; 50 | 51 | #[test] 52 | fn log_init_success() { 53 | log_init(LogLevel::Error); 54 | assert_eq!(last_error_length(), 0); 55 | } 56 | 57 | #[test] 58 | fn log_level() { 59 | assert_eq!(LogLevel::Error.as_ref(), "error"); 60 | assert_eq!(LogLevel::Debug.as_ref(), "debug"); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /crates/ffi/src/network/mod.rs: -------------------------------------------------------------------------------- 1 | //! Network related FFI interfaces 2 | -------------------------------------------------------------------------------- /crates/image/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "image" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | -------------------------------------------------------------------------------- /crates/image/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | #[test] 4 | fn it_works() { 5 | assert_eq!(2 + 2, 4); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /crates/network/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "network" 3 | version = "0.1.0" 4 | edition = "2018" 5 | authors = [ 6 | "Furisto", 7 | "Mrunal Patel ", 8 | "Sascha Grunert ", 9 | "utam0k ", 10 | ] 11 | documentation = "https://docs.rs/containrs" 12 | homepage = "https://github.com/containers/containrs" 13 | repository = "https://github.com/containers/containrs" 14 | license = "Apache-2.0" 15 | keywords = ["runtime", "kubernetes", "cri", "container", "pod"] 16 | categories = ["network-programming", "api-bindings"] 17 | 18 | [dependencies] 19 | anyhow = "1.0.66" 20 | async-trait = "0.1.58" 21 | crossbeam-channel = "0.5.6" 22 | derive_builder = "0.11.2" 23 | dyn-clone = "1.0.9" 24 | futures = "0.3.25" 25 | futures-util = "0.3.25" 26 | getset = "0.1.2" 27 | ipnetwork = "0.20.0" 28 | log = { version = "0.4.17", features = ["serde", "std"] } 29 | nix = "0.25.0" 30 | notify = { version = "5.0.0", features = ["serde"] } 31 | netlink-packet-route = "0.13.0" 32 | rtnetlink = "0.11.0" 33 | serde = { version = "1.0.147", features = ["derive"] } 34 | serde_json = "1.0.87" 35 | strum = { version = "0.24.1", features = ["derive"] } 36 | storage = { path = "../storage" } 37 | sandbox = { path = "../sandbox" } 38 | sysctl = "0.5.2" 39 | tokio = { version = "1.21.2", features = ["full"] } 40 | which = "4.3.0" 41 | 42 | [dev-dependencies] 43 | tempfile = "3.3.0" 44 | -------------------------------------------------------------------------------- /crates/network/src/cni/exec.rs: -------------------------------------------------------------------------------- 1 | //! CNI plugin interaction via command execution 2 | 3 | use anyhow::{bail, Context, Result}; 4 | use async_trait::async_trait; 5 | use derive_builder::Builder; 6 | use dyn_clone::{clone_trait_object, DynClone}; 7 | use getset::Getters; 8 | use log::trace; 9 | use std::{collections::HashMap, fmt::Debug, path::Path, process::Stdio}; 10 | use tokio::{ 11 | io::{AsyncReadExt, AsyncWriteExt}, 12 | process::Command, 13 | }; 14 | 15 | #[async_trait] 16 | /// The CNI command execution trait. 17 | pub trait Exec: DynClone + Send + Sync { 18 | /// Run a command and return the output as result. 19 | async fn run(&self, binary: &Path, args: &Args) -> Result; 20 | 21 | /// Run a command with standard input and return the output as result. 22 | async fn run_with_stdin(&self, binary: &Path, args: &Args, stdin: &[u8]) -> Result; 23 | } 24 | 25 | clone_trait_object!(Exec); 26 | 27 | #[derive(Clone, Debug, Default)] 28 | /// DefaultExec is a wrapper which can be used to execute CNI plugins in a standard way. 29 | pub struct DefaultExec; 30 | 31 | #[async_trait] 32 | impl Exec for DefaultExec { 33 | /// Run a command and return the output as result. 34 | async fn run(&self, binary: &Path, args: &Args) -> Result { 35 | let output = Command::new(binary).envs(args.envs()).output().await?; 36 | 37 | if !output.status.success() { 38 | bail!( 39 | "command failed with error: {}", 40 | String::from_utf8(output.stdout)? 41 | ) 42 | } 43 | 44 | Ok(String::from_utf8(output.stdout).context("cannot convert output to string")?) 45 | } 46 | 47 | /// Run a command with standard input and return the output as result. 48 | async fn run_with_stdin(&self, binary: &Path, args: &Args, stdin: &[u8]) -> Result { 49 | let mut child = Command::new(binary) 50 | .envs(args.envs()) 51 | .stdin(Stdio::piped()) 52 | .stdout(Stdio::piped()) 53 | .spawn() 54 | .context("spawn process")?; 55 | 56 | child 57 | .stdin 58 | .take() 59 | .context("no stdin")? 60 | .write_all(stdin) 61 | .await 62 | .context("write stdin")?; 63 | 64 | let mut output = String::new(); 65 | child 66 | .stdout 67 | .take() 68 | .context("no stdout")? 69 | .read_to_string(&mut output) 70 | .await 71 | .context("read stdout")?; 72 | 73 | if !child.wait().await?.success() { 74 | bail!(output) 75 | } 76 | 77 | Ok(output) 78 | } 79 | } 80 | 81 | #[derive(Clone, Debug, Default, Builder, Getters)] 82 | #[builder(default, pattern = "owned", setter(into))] 83 | // CNI arguments abstraction 84 | pub struct Args { 85 | #[get] 86 | /// CNI command. 87 | command: String, 88 | 89 | #[get] 90 | /// CNI Container ID. 91 | container_id: String, 92 | 93 | #[get] 94 | /// Network Namespace to be used. 95 | network_namespace: String, 96 | 97 | #[get] 98 | /// Additional plugin arguments. 99 | plugin_args: Vec, 100 | 101 | #[get] 102 | /// The interface name. 103 | interface_name: String, 104 | 105 | #[get] 106 | /// Additional CNI $PATH. 107 | path: String, 108 | } 109 | 110 | impl Args { 111 | /// Returns a HashMap for passing them as environment variables to the CNI plugin. 112 | fn envs(&self) -> HashMap { 113 | let mut env = HashMap::new(); 114 | env.insert("CNI_COMMAND".into(), self.command().clone()); 115 | env.insert("CNI_CONTAINERID".into(), self.container_id().clone()); 116 | env.insert("CNI_NETNS".into(), self.network_namespace().clone()); 117 | env.insert("CNI_ARGS".into(), self.plugin_args().join(";")); 118 | env.insert("CNI_IFNAME".into(), self.interface_name().clone()); 119 | env.insert("CNI_PATH".into(), self.path().clone()); 120 | trace!("Using CNI env: {:?}", env); 121 | env 122 | } 123 | } 124 | 125 | #[cfg(test)] 126 | mod tests { 127 | use super::*; 128 | use std::path::PathBuf; 129 | 130 | #[tokio::test] 131 | async fn exec_success() -> Result<()> { 132 | let binary = which::which("ls")?; 133 | let output = DefaultExec 134 | .run(&binary, &ArgsBuilder::default().build()?) 135 | .await?; 136 | assert!(output.contains("Cargo.toml")); 137 | Ok(()) 138 | } 139 | 140 | #[tokio::test] 141 | async fn exec_failure() -> Result<()> { 142 | let binary = PathBuf::from("/should/not/exist"); 143 | let res = DefaultExec 144 | .run(&binary, &ArgsBuilder::default().build()?) 145 | .await; 146 | assert!(res.is_err()); 147 | Ok(()) 148 | } 149 | 150 | #[tokio::test] 151 | async fn exec_stdin_success() -> Result<()> { 152 | let binary = which::which("cat")?; 153 | let output = DefaultExec 154 | .run_with_stdin(&binary, &ArgsBuilder::default().build()?, "test".as_bytes()) 155 | .await?; 156 | assert!(output.contains("test")); 157 | Ok(()) 158 | } 159 | 160 | #[tokio::test] 161 | async fn exec_stdin_failure() -> Result<()> { 162 | let binary = PathBuf::from("/should/not/exist"); 163 | let res = DefaultExec 164 | .run_with_stdin(&binary, &ArgsBuilder::default().build()?, "test".as_bytes()) 165 | .await; 166 | assert!(res.is_err()); 167 | Ok(()) 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /crates/network/src/cni/namespace.rs: -------------------------------------------------------------------------------- 1 | //! Network namespace helpers and structures. 2 | 3 | use anyhow::{Context, Result}; 4 | use futures::executor; 5 | use getset::Getters; 6 | use log::trace; 7 | use nix::sched::{setns, CloneFlags}; 8 | use std::{ 9 | fs, 10 | future::Future, 11 | os::unix::io::{AsRawFd, RawFd}, 12 | path::{Path, PathBuf}, 13 | }; 14 | use tokio::{fs::File, task}; 15 | 16 | #[derive(Debug, Getters)] 17 | /// A basic network namespace abstraction. 18 | pub struct Namespace { 19 | #[get] 20 | /// The current namespace as File. 21 | current: File, 22 | 23 | #[get] 24 | /// The target namespace as File. 25 | target: File, 26 | } 27 | 28 | impl Namespace { 29 | /// Create a new namespace. 30 | pub async fn new

(path: P) -> Result 31 | where 32 | P: AsRef, 33 | { 34 | let current = File::open(Self::current_thread_namespace_path()) 35 | .await 36 | .context("open current thread namespace file")?; 37 | 38 | let target = File::open(&path) 39 | .await 40 | .context("open target namespace file")?; 41 | 42 | Ok(Self { current, target }) 43 | } 44 | 45 | /// Run a future inside this network namespace 46 | pub async fn run(&self, fun: F) -> Result<()> 47 | where 48 | F: Future> + Send + 'static, 49 | { 50 | trace!( 51 | "Using file as target network namespace: {:?}", 52 | self.target() 53 | ); 54 | let current_fd = self.current().as_raw_fd(); 55 | let target_fd = self.target().as_raw_fd(); 56 | 57 | task::spawn_blocking(move || { 58 | // Switch to the target namespace 59 | trace!("Switching to target namespace"); 60 | Self::switch_namespace(target_fd)?; 61 | 62 | // Run the future 63 | let result = executor::block_on(fun).context("run namespace future"); 64 | 65 | // Ensure that we will switch back to the original network namespace 66 | trace!("Switching back to host network namespace"); 67 | Self::switch_namespace(current_fd)?; 68 | 69 | result 70 | }) 71 | .await 72 | .context("spawn namespace thread")? 73 | .context("run in namespace thread") 74 | } 75 | 76 | /// Switch the network namespace to the provided raw file descriptor. 77 | fn switch_namespace(fd: RawFd) -> Result<()> { 78 | trace!( 79 | "Current thread network namespace: {}", 80 | Self::current_thread_namespace()?.display(), 81 | ); 82 | 83 | setns(fd, CloneFlags::CLONE_NEWNET).context("switch to network namespace")?; 84 | 85 | trace!( 86 | "Switched to network namespace: {}", 87 | Self::current_thread_namespace()?.display(), 88 | ); 89 | Ok(()) 90 | } 91 | 92 | /// Returns the current threads network namespace identifier. 93 | pub fn current_thread_namespace() -> Result { 94 | fs::read_link(Self::current_thread_namespace_path()) 95 | .context("get current thread network namespace") 96 | } 97 | 98 | /// Retrieve the current network namespace path of the thread. 99 | pub fn current_thread_namespace_path() -> &'static str { 100 | "/proc/thread-self/ns/net" 101 | } 102 | } 103 | 104 | #[cfg(test)] 105 | mod tests { 106 | use super::*; 107 | use tempfile::NamedTempFile; 108 | 109 | #[tokio::test] 110 | async fn new_success() -> Result<()> { 111 | let temp_file = NamedTempFile::new()?; 112 | Namespace::new(temp_file.path()).await?; 113 | Ok(()) 114 | } 115 | 116 | #[tokio::test] 117 | async fn new_failure_not_existing() { 118 | assert!(Namespace::new("/path/does/not/exist").await.is_err()); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /crates/network/src/cni/netlink.rs: -------------------------------------------------------------------------------- 1 | //! Netlink related helpers and structures. 2 | 3 | use anyhow::{bail, Context, Result}; 4 | use async_trait::async_trait; 5 | use derive_builder::Builder; 6 | use dyn_clone::{clone_trait_object, DynClone}; 7 | use futures_util::stream::TryStreamExt; 8 | use getset::Getters; 9 | use log::{debug, trace}; 10 | use netlink_packet_route::rtnl::RouteMessage; 11 | use rtnetlink::{ 12 | packet::rtnl::{link::nlas::Nla, LinkMessage}, 13 | IpVersion, 14 | }; 15 | use std::fmt; 16 | 17 | #[async_trait] 18 | /// The netlink behavior trait. 19 | pub trait Netlink: DynClone + Send + Sync { 20 | /// Get the loopback link. 21 | async fn loopback(&self) -> Result { 22 | bail!("no loopback") 23 | } 24 | 25 | /// Get a link referenced by its name. 26 | async fn link_by_name(&self, _name: &str) -> Result { 27 | bail!("no link for name") 28 | } 29 | 30 | /// Get a link referenced by its index. 31 | async fn link_by_index(&self, _index: u32) -> Result { 32 | bail!("no link for index") 33 | } 34 | 35 | /// Set a link down. 36 | async fn set_link_down(&self, _link: &Link) -> Result<()> { 37 | Ok(()) 38 | } 39 | 40 | /// Set a link up. 41 | async fn set_link_up(&self, _link: &Link) -> Result<()> { 42 | Ok(()) 43 | } 44 | 45 | /// Get all routes for the provided IP version. 46 | async fn route_get(&self, _ip_version: IpVersion) -> Result> { 47 | Ok(vec![]) 48 | } 49 | } 50 | 51 | clone_trait_object!(Netlink); 52 | 53 | #[derive(Clone, Debug, Getters)] 54 | /// The default Netlink interface implementation. 55 | pub struct DefaultNetlink { 56 | #[get] 57 | handle: rtnetlink::Handle, 58 | } 59 | 60 | #[derive(Builder, Clone, Debug, Getters, Default)] 61 | #[builder(default, pattern = "owned", setter(into))] 62 | /// A link returned by netlink usage. 63 | pub struct Link { 64 | #[get = "pub"] 65 | name: String, 66 | 67 | #[get = "pub"] 68 | message: LinkMessage, 69 | } 70 | 71 | impl fmt::Display for Link { 72 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 73 | write!(f, "{}", self.name()) 74 | } 75 | } 76 | 77 | impl DefaultNetlink { 78 | /// Create a new netlink instance. 79 | pub async fn new() -> Result { 80 | debug!("Creating new netlink connection"); 81 | 82 | let (connection, handle, _) = 83 | rtnetlink::new_connection().context("create new netlink connection")?; 84 | tokio::spawn(connection); 85 | 86 | Ok(Self { handle }) 87 | } 88 | } 89 | 90 | #[async_trait] 91 | impl Netlink for DefaultNetlink { 92 | /// Get the loopback link. 93 | async fn loopback(&self) -> Result { 94 | self.link_by_name("lo").await 95 | } 96 | 97 | /// Get a link referenced by its name. 98 | async fn link_by_name(&self, name: &str) -> Result { 99 | let link = Link { 100 | name: name.into(), 101 | message: self 102 | .handle() 103 | .link() 104 | .get() 105 | .match_name(name.into()) 106 | .execute() 107 | .try_next() 108 | .await 109 | .context("get links")? 110 | .with_context(|| format!("no link found for name {}", name))?, 111 | }; 112 | trace!("Got link by name {}: {:?}", name, link.message.header); 113 | Ok(link) 114 | } 115 | 116 | /// Get a link referenced by its index. 117 | async fn link_by_index(&self, index: u32) -> Result { 118 | let message = self 119 | .handle() 120 | .link() 121 | .get() 122 | .match_index(index) 123 | .execute() 124 | .try_next() 125 | .await 126 | .context("get links")? 127 | .with_context(|| format!("no link found for index {}", index))?; 128 | trace!("Got link by index {}: {:?}", index, message.header); 129 | 130 | let name = || { 131 | for nla in message.nlas.iter() { 132 | if let Nla::IfName(name) = nla { 133 | trace!("Found name {} for link index {}", name, index); 134 | return Ok(name.clone()); 135 | } 136 | } 137 | bail!("no name found for interface") 138 | }; 139 | 140 | Ok(Link { 141 | name: name()?, 142 | message, 143 | }) 144 | } 145 | 146 | /// Set a link down. 147 | async fn set_link_down(&self, link: &Link) -> Result<()> { 148 | trace!("Setting link {} down", link); 149 | self.handle() 150 | .link() 151 | .set(link.message().header.index) 152 | .down() 153 | .execute() 154 | .await 155 | .context("set link down") 156 | } 157 | 158 | /// Set a link up. 159 | async fn set_link_up(&self, link: &Link) -> Result<()> { 160 | trace!("Setting link {} up", link); 161 | self.handle() 162 | .link() 163 | .set(link.message().header.index) 164 | .up() 165 | .execute() 166 | .await 167 | .context("set link up") 168 | } 169 | 170 | /// Get all routes for the provided IP version. 171 | async fn route_get(&self, ip_version: IpVersion) -> Result> { 172 | self.handle() 173 | .route() 174 | .get(ip_version) 175 | .execute() 176 | .try_collect::>() 177 | .await 178 | .context("get IP routes") 179 | } 180 | } 181 | 182 | #[cfg(test)] 183 | mod tests { 184 | use super::*; 185 | 186 | #[tokio::test] 187 | async fn netlink() -> Result<()> { 188 | let netlink = DefaultNetlink::new().await?; 189 | netlink.loopback().await?; 190 | Ok(()) 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /crates/network/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Network types and implementations. 2 | 3 | use anyhow::Result; 4 | use async_trait::async_trait; 5 | use derive_builder::Builder; 6 | use sandbox::SandboxConfig; 7 | 8 | pub mod cni; 9 | 10 | #[derive(Builder)] 11 | #[builder(pattern = "owned", setter(into))] 12 | /// Network is the main structure for working with the Container Network Interface. 13 | /// The implementation `T` can vary and is being defined in the `Pod` trait. 14 | pub struct Network 15 | where 16 | T: Default, 17 | { 18 | #[builder(default = "T::default()")] 19 | /// Trait implementation for the network. 20 | implementation: T, 21 | } 22 | 23 | #[async_trait] 24 | /// Common network behavior trait 25 | pub trait PodNetwork { 26 | /// Start a new network for the provided `SandboxData`. 27 | async fn start(&mut self, _: &SandboxConfig) -> Result<()> { 28 | Ok(()) 29 | } 30 | 31 | /// Stop the network of the provided `SandboxData`. 32 | async fn stop(&mut self, _: &SandboxConfig) -> Result<()> { 33 | Ok(()) 34 | } 35 | 36 | /// Cleanup the network implementation on server shutdown. 37 | async fn cleanup(&mut self) -> Result<()> { 38 | Ok(()) 39 | } 40 | } 41 | 42 | impl Network 43 | where 44 | T: Send + Default + PodNetwork, 45 | { 46 | #[allow(dead_code)] 47 | /// Wrapper for the implementations `start` method. 48 | pub async fn start(&mut self, sandbox_data: &SandboxConfig) -> Result<()> { 49 | self.implementation.start(sandbox_data).await 50 | } 51 | 52 | #[allow(dead_code)] 53 | /// Wrapper for the implementations `stop` method. 54 | pub async fn stop(&mut self, sandbox_data: &SandboxConfig) -> Result<()> { 55 | self.implementation.stop(sandbox_data).await 56 | } 57 | 58 | /// Cleanup the network implementation on server shutdown. 59 | pub async fn cleanup(&mut self) -> Result<()> { 60 | self.implementation.cleanup().await 61 | } 62 | } 63 | 64 | #[cfg(test)] 65 | pub mod tests { 66 | use std::{collections::HashMap, path::PathBuf}; 67 | 68 | use sandbox::{LinuxNamespaces, SandboxConfigBuilder}; 69 | 70 | use super::*; 71 | 72 | pub fn new_sandbox_data() -> Result { 73 | let mut annotations: HashMap = HashMap::new(); 74 | annotations.insert("annotationkey1".into(), "annotationvalue1".into()); 75 | 76 | Ok(SandboxConfigBuilder::default() 77 | .id("uid") 78 | .name("name") 79 | .namespace("namespace") 80 | .attempt(1u32) 81 | .linux_namespaces(LinuxNamespaces::NET) 82 | .cgroup_parent(PathBuf::from("/sys/fs/cgroup/containrs/pod")) 83 | .hostname("hostname") 84 | .log_directory("log_directory") 85 | .annotations(annotations) 86 | .build()?) 87 | } 88 | 89 | #[derive(Default)] 90 | struct Mock { 91 | start_called: bool, 92 | stop_called: bool, 93 | } 94 | 95 | #[async_trait] 96 | impl PodNetwork for Mock { 97 | async fn start(&mut self, _: &SandboxConfig) -> Result<()> { 98 | self.start_called = true; 99 | Ok(()) 100 | } 101 | 102 | async fn stop(&mut self, _: &SandboxConfig) -> Result<()> { 103 | self.stop_called = true; 104 | Ok(()) 105 | } 106 | } 107 | 108 | #[tokio::test] 109 | async fn create() -> Result<()> { 110 | let implementation = Mock::default(); 111 | 112 | assert!(!implementation.start_called); 113 | assert!(!implementation.stop_called); 114 | 115 | let mut network = NetworkBuilder::::default() 116 | .implementation(implementation) 117 | .build()?; 118 | 119 | let sandbox_data = new_sandbox_data()?; 120 | 121 | network.start(&sandbox_data).await?; 122 | assert!(network.implementation.start_called); 123 | 124 | network.stop(&sandbox_data).await?; 125 | assert!(network.implementation.stop_called); 126 | 127 | Ok(()) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /crates/sandbox/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sandbox" 3 | version = "0.1.0" 4 | edition = "2018" 5 | authors = [ 6 | "Furisto", 7 | "Mrunal Patel ", 8 | "Sascha Grunert ", 9 | "utam0k ", 10 | ] 11 | documentation = "https://docs.rs/containrs" 12 | homepage = "https://github.com/containers/containrs" 13 | repository = "https://github.com/containers/containrs" 14 | license = "Apache-2.0" 15 | keywords = ["runtime", "kubernetes", "cri", "container", "pod"] 16 | categories = ["network-programming", "api-bindings"] 17 | 18 | [dependencies] 19 | async-trait = "0.1.58" 20 | thiserror = "1.0.37" 21 | bitflags = "1.3.2" 22 | common = { path= "../common" } 23 | derive_builder = "0.11.2" 24 | dyn-clone = "1.0.9" 25 | getset = "0.1.2" 26 | tokio = { version = "1.21.2", features = ["process"] } 27 | strum = { version = "0.24.1", features = ["derive"] } 28 | uuid = { version = "1.2.2", features = ["v4"] } 29 | which = "4.3.0" 30 | 31 | [dev-dependencies] 32 | anyhow = "1.0.66" 33 | tokio = { version = "1.21.2", features = ["macros"] } 34 | tempfile = "3.3.0" 35 | nix = "0.25.0" 36 | -------------------------------------------------------------------------------- /crates/sandbox/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | use tokio::io; 3 | 4 | pub type Result = std::result::Result; 5 | 6 | #[derive(Error, Debug)] 7 | pub enum SandboxError { 8 | #[error("uninitialized field")] 9 | Builder(#[from] derive_builder::UninitializedFieldError), 10 | #[error("{0}")] 11 | Pinning(String), 12 | #[error("IO")] 13 | IO(#[from] io::Error), 14 | } 15 | -------------------------------------------------------------------------------- /crates/sandbox/src/pinned.rs: -------------------------------------------------------------------------------- 1 | //! A pod sandbox implementation which does pin it's namespaces to file descriptors. 2 | 3 | use super::{LinuxNamespaces, Pod}; 4 | use crate::error::{Result, SandboxError}; 5 | use crate::pinns::Arg; 6 | use crate::{Pinns, SandboxContext}; 7 | use async_trait::async_trait; 8 | use tokio::fs; 9 | use uuid::Uuid; 10 | 11 | #[derive(Default)] 12 | pub struct PinnedSandbox {} 13 | 14 | #[async_trait] 15 | impl Pod for PinnedSandbox { 16 | async fn run(&mut self, context: &SandboxContext) -> Result<()> { 17 | let config = &context.config; 18 | 19 | Self::pin_namespaces( 20 | Uuid::new_v4().to_string(), 21 | config.pinns(), 22 | config.linux_namespaces(), 23 | ) 24 | .await?; 25 | 26 | Ok(()) 27 | } 28 | 29 | fn stop(&mut self, _: &SandboxContext) -> Result<()> { 30 | Ok(()) 31 | } 32 | 33 | fn remove(&mut self, _: &SandboxContext) -> Result<()> { 34 | Ok(()) 35 | } 36 | 37 | fn ready(&mut self, _: &SandboxContext) -> Result { 38 | Ok(false) 39 | } 40 | } 41 | 42 | impl PinnedSandbox { 43 | async fn pin_namespaces( 44 | pod_id: String, 45 | pinns: &Pinns, 46 | namespaces: &Option, 47 | ) -> Result<()> { 48 | if let Some(ns) = namespaces { 49 | let mut args = Vec::new(); 50 | if ns.contains(LinuxNamespaces::IPC) { 51 | args.push(Arg::Ipc); 52 | } 53 | 54 | if ns.contains(LinuxNamespaces::UTS) { 55 | args.push(Arg::Uts); 56 | } 57 | 58 | if ns.contains(LinuxNamespaces::NET) { 59 | args.push(Arg::Net); 60 | } 61 | 62 | if ns.contains(LinuxNamespaces::CGROUP) { 63 | args.push(Arg::Cgroup) 64 | } 65 | 66 | fs::create_dir_all(&pinns.pin_dir()).await?; 67 | 68 | args.push(Arg::Dir(pinns.pin_dir().clone())); 69 | args.push(Arg::FileName(pod_id)); 70 | args.push(Arg::LogLevel(pinns.log_level())); 71 | 72 | let output = pinns.run(&args).await?; 73 | if !output.status.success() { 74 | return Err(SandboxError::Pinning(format!( 75 | "failed to pin namespaces. Pinns exited with {}. Output: {}", 76 | output.status, 77 | String::from_utf8(output.stderr).unwrap() 78 | ))); 79 | } 80 | } 81 | 82 | Ok(()) 83 | } 84 | } 85 | 86 | #[cfg(test)] 87 | mod tests { 88 | use std::path::PathBuf; 89 | 90 | use super::*; 91 | use crate::pinns::PinnsBuilder; 92 | use anyhow::{Context, Result}; 93 | use nix::mount::{umount2, MntFlags}; 94 | use tempfile::TempDir; 95 | 96 | // pinned namespaces need to be cleaned up, otherwise the test 97 | // directory can not be deleted 98 | fn cleanup_pinned_dir(namespaces: &[PathBuf]) { 99 | for ns_path in namespaces { 100 | let _ = umount2(ns_path, MntFlags::MNT_DETACH); 101 | } 102 | } 103 | 104 | #[tokio::test] 105 | #[ignore = "requires root"] 106 | async fn pin_namespaces() -> Result<()> { 107 | let pod_id = Uuid::new_v4().to_string(); 108 | let pin_dir = TempDir::new().context("create temp dir")?; 109 | let namespaces = Some(LinuxNamespaces::IPC | LinuxNamespaces::UTS | LinuxNamespaces::NET); 110 | let pinns = PinnsBuilder::default() 111 | .binary(which::which("pinns")?) 112 | .pin_dir(pin_dir.path()) 113 | .build() 114 | .context("build pinns")?; 115 | 116 | PinnedSandbox::pin_namespaces(pod_id.clone(), &pinns, &namespaces) 117 | .await 118 | .context("pin namespaces")?; 119 | 120 | let pinned_ns: Vec = ["ipcns", "utsns", "netns"] 121 | .iter() 122 | .map(|ns| pin_dir.path().join(ns).join(&pod_id)) 123 | .collect(); 124 | 125 | for ns in &pinned_ns { 126 | assert!(ns.exists()); 127 | } 128 | 129 | cleanup_pinned_dir(&pinned_ns); 130 | Ok(()) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /crates/sandbox/src/pinns.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{Result, SandboxError}; 2 | use async_trait::async_trait; 3 | use derive_builder::Builder; 4 | use dyn_clone::clone_trait_object; 5 | use dyn_clone::DynClone; 6 | use getset::{CopyGetters, Getters, Setters}; 7 | use std::fmt::Debug; 8 | use std::fmt::Display; 9 | use std::path::{Path, PathBuf}; 10 | use std::process::Output; 11 | use strum::{AsRefStr, Display}; 12 | use tokio::process::Command; 13 | 14 | #[derive(Builder, Clone, Debug, CopyGetters, Getters, Setters)] 15 | #[builder( 16 | pattern = "owned", 17 | setter(into, strip_option), 18 | build_fn(error = "SandboxError") 19 | )] 20 | pub struct Pinns { 21 | #[get = "pub"] 22 | #[builder(default = "Pinns::default_pin_tool()?")] 23 | binary: PathBuf, 24 | 25 | #[get = "pub"] 26 | #[builder(default = "Pinns::default_pin_dir()?")] 27 | pin_dir: PathBuf, 28 | 29 | #[get_copy = "pub"] 30 | #[builder(default = "LogLevel::Info")] 31 | log_level: LogLevel, 32 | 33 | #[getset(get, set)] 34 | #[builder(private, default = "Box::new(DefaultExecCommand{})")] 35 | exec: Box, 36 | } 37 | 38 | impl Pinns { 39 | pub(crate) async fn run(&self, args: &[Arg]) -> Result { 40 | self.exec().run_output(self.binary(), args).await 41 | } 42 | 43 | fn default_pin_tool() -> Result { 44 | which::which("pinns").map_err(|e| SandboxError::Pinning(e.to_string())) 45 | } 46 | 47 | fn default_pin_dir() -> Result { 48 | Ok(PathBuf::from("/run/containrs")) 49 | } 50 | } 51 | 52 | impl Default for Pinns { 53 | fn default() -> Self { 54 | Self { 55 | binary: Self::default_pin_tool().unwrap(), 56 | pin_dir: Self::default_pin_dir().unwrap(), 57 | log_level: Default::default(), 58 | exec: Box::new(DefaultExecCommand {}), 59 | } 60 | } 61 | } 62 | 63 | #[async_trait] 64 | trait ExecCommand: Debug + DynClone + Send + Sync { 65 | async fn run_output(&self, binary: &Path, args: &[Arg]) -> Result { 66 | Command::new(binary) 67 | .args(args.iter().map(ToString::to_string)) 68 | .output() 69 | .await 70 | .map_err(|e| SandboxError::Pinning(e.to_string())) 71 | } 72 | } 73 | 74 | clone_trait_object!(ExecCommand); 75 | 76 | #[derive(Clone, Default, Debug)] 77 | struct DefaultExecCommand {} 78 | 79 | impl ExecCommand for DefaultExecCommand {} 80 | 81 | #[derive(AsRefStr, Clone, Debug)] 82 | #[strum(serialize_all = "lowercase")] 83 | pub(crate) enum Arg { 84 | Cgroup, 85 | Ipc, 86 | Net, 87 | #[allow(dead_code)] 88 | Pid, 89 | Uts, 90 | Dir(PathBuf), 91 | FileName(String), 92 | #[strum(serialize = "log-level")] 93 | LogLevel(LogLevel), 94 | } 95 | 96 | impl Display for Arg { 97 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 98 | fn write_kv(f: &mut std::fmt::Formatter<'_>, k: K, v: V) -> std::fmt::Result 99 | where 100 | K: AsRef, 101 | V: Display, 102 | { 103 | write!(f, "{}={}", k.as_ref(), v) 104 | } 105 | 106 | write!(f, "--")?; 107 | match self { 108 | Arg::Dir(dir) => write_kv(f, self, dir.display()), 109 | Arg::FileName(file) => write_kv(f, self, file), 110 | Arg::LogLevel(level) => write_kv(f, self, level), 111 | _ => write!(f, "{}", self.as_ref()), 112 | } 113 | } 114 | } 115 | 116 | #[derive(AsRefStr, Display, Clone, Copy, Debug)] 117 | #[strum(serialize_all = "lowercase")] 118 | pub enum LogLevel { 119 | Trace, 120 | Debug, 121 | Info, 122 | Warn, 123 | Error, 124 | Off, 125 | } 126 | 127 | impl Default for LogLevel { 128 | fn default() -> Self { 129 | LogLevel::Info 130 | } 131 | } 132 | 133 | #[cfg(test)] 134 | mod tests { 135 | use super::*; 136 | use anyhow::{Context, Result}; 137 | use which; 138 | 139 | fn setup_pinns() -> Result { 140 | let pinns = PinnsBuilder::default() 141 | .binary(which::which("echo")?) 142 | .build() 143 | .context("build pinns")?; 144 | 145 | Ok(pinns) 146 | } 147 | 148 | #[tokio::test] 149 | async fn pinns_cmd_flags() -> Result<()> { 150 | let pinns = setup_pinns()?; 151 | let test_data = &[ 152 | (Arg::Cgroup, "--cgroup\n"), 153 | (Arg::Ipc, "--ipc\n"), 154 | (Arg::Net, "--net\n"), 155 | (Arg::Pid, "--pid\n"), 156 | (Arg::Uts, "--uts\n"), 157 | ]; 158 | 159 | for t in test_data { 160 | let output = pinns.run(&[t.0.clone()]).await.context("run pinns")?; 161 | assert!(output.status.success()); 162 | assert!(String::from_utf8(output.stderr)?.is_empty()); 163 | assert_eq!(String::from_utf8(output.stdout)?, t.1); 164 | } 165 | 166 | Ok(()) 167 | } 168 | 169 | #[tokio::test] 170 | async fn pinns_cmd_options() -> Result<()> { 171 | let pinns = setup_pinns()?; 172 | let test_data = &[ 173 | ( 174 | Arg::Dir(PathBuf::from("/tmp/containrs")), 175 | "--dir=/tmp/containrs\n", 176 | ), 177 | ( 178 | Arg::FileName("containrs".to_owned()), 179 | "--filename=containrs\n", 180 | ), 181 | ]; 182 | 183 | for t in test_data { 184 | let output = pinns.run(&[t.0.clone()]).await.context("run pinns")?; 185 | assert!(output.status.success()); 186 | assert!(String::from_utf8(output.stderr)?.is_empty()); 187 | assert_eq!(String::from_utf8(output.stdout)?, t.1); 188 | } 189 | 190 | Ok(()) 191 | } 192 | 193 | #[tokio::test] 194 | async fn pinns_cmd_log_level() -> Result<()> { 195 | let pinns = setup_pinns()?; 196 | let test_data = &[ 197 | (Arg::LogLevel(LogLevel::Trace), "--log-level=trace\n"), 198 | (Arg::LogLevel(LogLevel::Debug), "--log-level=debug\n"), 199 | (Arg::LogLevel(LogLevel::Info), "--log-level=info\n"), 200 | (Arg::LogLevel(LogLevel::Warn), "--log-level=warn\n"), 201 | (Arg::LogLevel(LogLevel::Error), "--log-level=error\n"), 202 | (Arg::LogLevel(LogLevel::Off), "--log-level=off\n"), 203 | ]; 204 | 205 | for t in test_data { 206 | let output = pinns.run(&[t.0.clone()]).await.context("run pinns")?; 207 | assert!(output.status.success()); 208 | assert!(String::from_utf8(output.stderr)?.is_empty()); 209 | assert_eq!(String::from_utf8(output.stdout)?, t.1); 210 | } 211 | 212 | Ok(()) 213 | } 214 | 215 | #[tokio::test] 216 | async fn pinns_cmd_multiple_args() -> Result<()> { 217 | let pinns = setup_pinns()?; 218 | let args = &[ 219 | Arg::Ipc, 220 | Arg::Uts, 221 | Arg::Net, 222 | Arg::Dir(PathBuf::from("/tmp/containrs")), 223 | Arg::FileName("containrs".to_owned()), 224 | Arg::LogLevel(LogLevel::Warn), 225 | ]; 226 | 227 | let output = pinns.run(args).await.context("run pinns")?; 228 | assert!(output.status.success()); 229 | assert!(String::from_utf8(output.stderr)?.is_empty()); 230 | assert_eq!( 231 | String::from_utf8(output.stdout)?, 232 | "--ipc --uts --net --dir=/tmp/containrs --filename=containrs --log-level=warn\n" 233 | ); 234 | 235 | Ok(()) 236 | } 237 | 238 | #[test] 239 | fn default_values_set() -> Result<()> { 240 | let pinns = PinnsBuilder::default() 241 | .binary(which::which("echo")?) 242 | .build()?; 243 | 244 | assert_eq!(pinns.pin_dir(), &PathBuf::from("/run/containrs")); 245 | Ok(()) 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /crates/server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "server" 3 | version = "0.1.0" 4 | edition = "2018" 5 | authors = [ 6 | "Furisto", 7 | "Mrunal Patel ", 8 | "Sascha Grunert ", 9 | "utam0k ", 10 | ] 11 | documentation = "https://docs.rs/containrs" 12 | homepage = "https://github.com/containers/containrs" 13 | repository = "https://github.com/containers/containrs" 14 | license = "Apache-2.0" 15 | keywords = ["runtime", "kubernetes", "cri", "container", "pod"] 16 | categories = ["network-programming", "api-bindings"] 17 | 18 | [dependencies] 19 | anyhow = "1.0.66" 20 | services = { path = "../services" } 21 | tokio = { version = "1.21.2", features = ["full"] } 22 | -------------------------------------------------------------------------------- /crates/server/src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use services::server::{Config, Server}; 3 | use std::process::exit; 4 | 5 | #[tokio::main] 6 | async fn main() -> Result<()> { 7 | // Parse CLI arguments 8 | let config = Config::default(); 9 | 10 | // Spawn the server based on the configuration 11 | if let Err(e) = Server::new(config).start().await { 12 | // Collect all errors and chain them together. Do not use the logger 13 | // for printing here, because it could be possible that it fails before 14 | // initializing it. 15 | println!("Unable to run server: {:#}", e); 16 | exit(1); 17 | } 18 | 19 | Ok(()) 20 | } 21 | -------------------------------------------------------------------------------- /crates/services/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "services" 3 | version = "0.1.0" 4 | edition = "2018" 5 | authors = [ 6 | "Furisto", 7 | "Mrunal Patel ", 8 | "Sascha Grunert ", 9 | "utam0k ", 10 | ] 11 | documentation = "https://docs.rs/containrs" 12 | homepage = "https://github.com/containers/containrs" 13 | repository = "https://github.com/containers/containrs" 14 | license = "Apache-2.0" 15 | keywords = ["runtime", "kubernetes", "cri", "container", "pod"] 16 | categories = ["network-programming", "api-bindings"] 17 | 18 | [dependencies] 19 | anyhow = "1.0.66" 20 | async-stream = "0.3.3" 21 | container = { path = "../container" } 22 | derive_builder = "0.11.2" 23 | log = { version = "0.4.17", features = ["serde", "std"] } 24 | oci-spec = { version = "0.5.8", features = ["runtime"] } 25 | prost = "0.11.2" 26 | sandbox = { path = "../sandbox" } 27 | storage = { path = "../storage" } 28 | thiserror = "1.0.37" 29 | tonic = "0.8.2" 30 | # TODO: these dependencies are only needed for the server 31 | # should be moved to the server crate 32 | lazy_static = "1.4.0" 33 | tokio = { version = "1.21.2", features = ["full"] } 34 | serde = { version = "1.0.147", features = ["derive"] } 35 | network = { path = "../network" } 36 | nix = "0.25.0" 37 | clap = { version = "4.0.26", features = ["cargo", "derive", "env", "wrap_help"] } 38 | getset = "0.1.2" 39 | strum = { version = "0.24.1", features = ["derive"] } 40 | futures = "0.3.25" 41 | env_logger = "0.9.3" 42 | common = { path="../common" } 43 | 44 | [build-dependencies] 45 | anyhow = "1.0.66" 46 | tonic-build = "0.8.2" 47 | 48 | [dev-dependencies] 49 | tempfile = "3.3.0" 50 | -------------------------------------------------------------------------------- /crates/services/build.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use std::path::PathBuf; 3 | 4 | const PROTO_FILE: &str = "src/cri/proto/criapi.proto"; 5 | 6 | fn main() -> Result<()> { 7 | tonic_build::configure() 8 | .out_dir("src/cri/api") 9 | .compile( 10 | &[PROTO_FILE], 11 | &[&PathBuf::from(PROTO_FILE) 12 | .parent() 13 | .context("no path parent")? 14 | .display() 15 | .to_string()], 16 | ) 17 | .context("compile CRI protocol buffers")?; 18 | Ok(()) 19 | } 20 | -------------------------------------------------------------------------------- /crates/services/src/cri/api/mod.rs: -------------------------------------------------------------------------------- 1 | //! Kubernetes Container Runtime Interface (CRI) protobuf API 2 | #![allow(missing_docs)] 3 | #![allow(clippy::all)] 4 | 5 | include!("runtime.v1alpha2.rs"); 6 | 7 | use crate::error::ServiceError; 8 | use oci_spec::runtime::MountBuilder; 9 | use std::{convert::TryFrom, fmt::Display, fs, path::PathBuf}; 10 | 11 | use crate::cri::api::Mount as CRIMount; 12 | use oci_spec::runtime::Mount as OCIMount; 13 | 14 | impl Display for MountPropagation { 15 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 16 | let print = match self { 17 | MountPropagation::PropagationBidirectional => "rshared", 18 | MountPropagation::PropagationHostToContainer => "rslave", 19 | MountPropagation::PropagationPrivate => "rprivate", 20 | }; 21 | 22 | write!(f, "{}", print) 23 | } 24 | } 25 | 26 | impl TryFrom<&CRIMount> for OCIMount { 27 | type Error = ServiceError; 28 | 29 | fn try_from(mount: &CRIMount) -> Result { 30 | if mount.container_path.is_empty() { 31 | return Err(ServiceError::Other( 32 | "mount container path cannot be empty".to_owned(), 33 | )); 34 | } 35 | 36 | if mount.host_path.is_empty() { 37 | return Err(ServiceError::Other( 38 | "mount host path cannot be empty".to_owned(), 39 | )); 40 | } 41 | 42 | let mut host_path = PathBuf::from(&mount.host_path); 43 | if fs::symlink_metadata(&host_path)?.file_type().is_symlink() { 44 | host_path = fs::read_link(&mount.host_path)?; 45 | } 46 | 47 | let mut options = Vec::new(); 48 | if mount.readonly { 49 | options.push("ro".to_owned()); 50 | } 51 | 52 | options.push(mount.propagation().to_string()); 53 | 54 | let oci_mount = MountBuilder::default() 55 | .source(host_path) 56 | .destination(mount.container_path.as_str()) 57 | .typ("bind") 58 | .options(options) 59 | .build()?; 60 | 61 | Ok(oci_mount) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /crates/services/src/cri/cri_service.rs: -------------------------------------------------------------------------------- 1 | //! A CRI API service implementation. 2 | 3 | use anyhow::Result; 4 | use derive_builder::Builder; 5 | use log::debug; 6 | use std::fmt::{Debug, Display}; 7 | use storage::default_key_value_storage::DefaultKeyValueStorage; 8 | use tonic::{Request, Response, Status}; 9 | 10 | #[derive(Clone, Builder)] 11 | #[builder(pattern = "owned", setter(into))] 12 | /// The service implementation for the CRI API 13 | pub struct CRIService { 14 | /// Storage used by the service. 15 | #[allow(dead_code)] 16 | storage: DefaultKeyValueStorage, 17 | } 18 | 19 | impl CRIService { 20 | /// Debug log a request. 21 | pub fn debug_request(&self, request: &Request) 22 | where 23 | T: Debug, 24 | { 25 | debug!("{:?}", request.get_ref()); 26 | } 27 | 28 | /// Debug log a response. 29 | pub fn debug_response(&self, response: &Result, Status>) 30 | where 31 | T: Debug, 32 | { 33 | debug!("{:?}", response.as_ref().map(|x| x.get_ref())); 34 | } 35 | } 36 | 37 | /// Option to Status transformer for less verbose request unpacking. 38 | pub trait OptionStatus { 39 | /// Maps the self type to an invalid argument status containing the provided `msg`. 40 | fn ok_or_invalid(self, msg: impl Into) -> Result 41 | where 42 | Self: Sized, 43 | { 44 | self.ok_or_else(|| Status::invalid_argument(msg)) 45 | } 46 | 47 | /// Transforms the `OptionStatus` into a [`Result`], mapping [`Some(v)`] to 48 | /// [`Ok(v)`] and [`None`] to [`Err(err())`]. 49 | /// 50 | /// [`Result`]: Result 51 | /// [`Ok(v)`]: Ok 52 | /// [`Err(err())`]: Err 53 | /// [`Some(v)`]: Some 54 | fn ok_or_else(self, err: F) -> Result 55 | where 56 | F: FnOnce() -> E; 57 | } 58 | 59 | impl OptionStatus for Option { 60 | fn ok_or_else(self, err: F) -> Result 61 | where 62 | F: FnOnce() -> E, 63 | { 64 | self.ok_or_else(err) 65 | } 66 | } 67 | 68 | /// Result to Status transformer for less verbose request unpacking. 69 | pub trait ResultStatus 70 | where 71 | E: Display, 72 | { 73 | /// Maps the self type to an internal error status containing the provided `msg`. 74 | fn map_internal(self, msg: impl Into + Display) -> Result 75 | where 76 | Self: Sized, 77 | { 78 | self.map_err(|e| Status::internal(format!("{}: {}", msg, e))) 79 | } 80 | 81 | /// Maps a `ResultStatus` to `Result` by applying a function to a 82 | /// contained [`Err`] value, leaving an [`Ok`] value untouched. 83 | /// 84 | /// This function can be used to pass through a successful result while handling 85 | /// an error. 86 | fn map_err(self, op: O) -> Result 87 | where 88 | O: FnOnce(E) -> F; 89 | } 90 | 91 | impl ResultStatus for Result 92 | where 93 | E: Display, 94 | { 95 | fn map_err(self, op: O) -> Result 96 | where 97 | O: FnOnce(E) -> F, 98 | { 99 | self.map_err(op) 100 | } 101 | } 102 | 103 | #[cfg(test)] 104 | pub mod tests { 105 | use super::*; 106 | use anyhow::Result; 107 | use storage::KeyValueStorage; 108 | use tempfile::TempDir; 109 | 110 | pub fn new_cri_service() -> Result { 111 | let dir = TempDir::new()?; 112 | Ok(CRIService { 113 | storage: DefaultKeyValueStorage::open(dir.path())?, 114 | }) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /crates/services/src/cri/image_service/image_fs_info.rs: -------------------------------------------------------------------------------- 1 | use crate::cri::{ 2 | api::{ImageFsInfoRequest, ImageFsInfoResponse}, 3 | cri_service::CRIService, 4 | }; 5 | use tonic::{Request, Response, Status}; 6 | 7 | impl CRIService { 8 | /// handle_image_fs_info returns information of the filesystem that is used to 9 | /// store images. 10 | pub async fn handle_image_fs_info( 11 | &self, 12 | _request: Request, 13 | ) -> Result, Status> { 14 | let resp = ImageFsInfoResponse { 15 | image_filesystems: Vec::new(), 16 | }; 17 | Ok(Response::new(resp)) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /crates/services/src/cri/image_service/image_status.rs: -------------------------------------------------------------------------------- 1 | use crate::cri::{ 2 | api::{ImageStatusRequest, ImageStatusResponse}, 3 | cri_service::CRIService, 4 | }; 5 | use std::collections::HashMap; 6 | use tonic::{Request, Response, Status}; 7 | 8 | impl CRIService { 9 | /// handle_image_status returns the status of the image. If the image is not 10 | /// present, returns a response with ImageStatusResponse.image set to 11 | /// None. 12 | pub async fn handle_image_status( 13 | &self, 14 | _request: Request, 15 | ) -> Result, Status> { 16 | let resp = ImageStatusResponse { 17 | image: None, 18 | info: HashMap::new(), 19 | }; 20 | Ok(Response::new(resp)) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /crates/services/src/cri/image_service/list_images.rs: -------------------------------------------------------------------------------- 1 | use crate::cri::{ 2 | api::{ListImagesRequest, ListImagesResponse}, 3 | cri_service::CRIService, 4 | }; 5 | use tonic::{Request, Response, Status}; 6 | 7 | impl CRIService { 8 | /// handle_list_images lists existing images. 9 | pub async fn handle_list_images( 10 | &self, 11 | _request: Request, 12 | ) -> Result, Status> { 13 | let resp = ListImagesResponse { images: Vec::new() }; 14 | Ok(Response::new(resp)) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /crates/services/src/cri/image_service/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::cri::{ 2 | api::{self, image_service_server::ImageService}, 3 | cri_service::CRIService, 4 | }; 5 | use tonic::{Request, Response, Status}; 6 | 7 | mod image_fs_info; 8 | mod image_status; 9 | mod list_images; 10 | mod pull_image; 11 | mod remove_image; 12 | 13 | #[tonic::async_trait] 14 | impl ImageService for CRIService { 15 | async fn list_images( 16 | &self, 17 | request: Request, 18 | ) -> Result, Status> { 19 | self.debug_request(&request); 20 | let response = self.handle_list_images(request).await; 21 | self.debug_response(&response); 22 | response 23 | } 24 | 25 | async fn pull_image( 26 | &self, 27 | request: Request, 28 | ) -> Result, Status> { 29 | self.debug_request(&request); 30 | let response = self.handle_pull_image(request).await; 31 | self.debug_response(&response); 32 | response 33 | } 34 | 35 | async fn image_status( 36 | &self, 37 | request: Request, 38 | ) -> Result, Status> { 39 | self.debug_request(&request); 40 | let response = self.handle_image_status(request).await; 41 | self.debug_response(&response); 42 | response 43 | } 44 | 45 | async fn remove_image( 46 | &self, 47 | request: Request, 48 | ) -> Result, Status> { 49 | self.debug_request(&request); 50 | let response = self.handle_remove_image(request).await; 51 | self.debug_response(&response); 52 | response 53 | } 54 | 55 | async fn image_fs_info( 56 | &self, 57 | request: Request, 58 | ) -> Result, Status> { 59 | self.debug_request(&request); 60 | let response = self.handle_image_fs_info(request).await; 61 | self.debug_response(&response); 62 | response 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /crates/services/src/cri/image_service/pull_image.rs: -------------------------------------------------------------------------------- 1 | use crate::cri::{ 2 | api::{PullImageRequest, PullImageResponse}, 3 | cri_service::CRIService, 4 | }; 5 | use tonic::{Request, Response, Status}; 6 | 7 | impl CRIService { 8 | /// handle_pull_image pulls an image with authentication config. 9 | pub async fn handle_pull_image( 10 | &self, 11 | _request: Request, 12 | ) -> Result, Status> { 13 | let resp = PullImageResponse { 14 | image_ref: "some_image".into(), 15 | }; 16 | Ok(Response::new(resp)) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /crates/services/src/cri/image_service/remove_image.rs: -------------------------------------------------------------------------------- 1 | use crate::cri::{ 2 | api::{RemoveImageRequest, RemoveImageResponse}, 3 | cri_service::CRIService, 4 | }; 5 | use tonic::{Request, Response, Status}; 6 | 7 | impl CRIService { 8 | /// handle_remove_image removes the image. This call is idempotent, and must not return an 9 | /// error if the image has already been removed. 10 | pub async fn handle_remove_image( 11 | &self, 12 | _request: Request, 13 | ) -> Result, Status> { 14 | let resp = RemoveImageResponse {}; 15 | Ok(Response::new(resp)) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /crates/services/src/cri/mod.rs: -------------------------------------------------------------------------------- 1 | //! Kubernetes Container Runtime Interface implementations 2 | 3 | mod image_service; 4 | mod runtime_service; 5 | 6 | pub mod api; 7 | pub mod cri_service; 8 | -------------------------------------------------------------------------------- /crates/services/src/cri/runtime_service/attach.rs: -------------------------------------------------------------------------------- 1 | use crate::cri::{ 2 | api::{AttachRequest, AttachResponse}, 3 | cri_service::CRIService, 4 | }; 5 | use tonic::{Request, Response, Status}; 6 | 7 | impl CRIService { 8 | /// handle_attach prepares a streaming endpoint to attach to a running container. 9 | pub async fn handle_attach( 10 | &self, 11 | _request: Request, 12 | ) -> Result, Status> { 13 | let resp = AttachResponse { url: "url".into() }; 14 | Ok(Response::new(resp)) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /crates/services/src/cri/runtime_service/container_stats.rs: -------------------------------------------------------------------------------- 1 | use crate::cri::{ 2 | api::{ContainerStatsRequest, ContainerStatsResponse}, 3 | cri_service::CRIService, 4 | }; 5 | use tonic::{Request, Response, Status}; 6 | 7 | impl CRIService { 8 | /// handle_container_stats returns stats of the container. If the container does not exist, the 9 | /// call returns an error. 10 | pub async fn handle_container_stats( 11 | &self, 12 | _request: Request, 13 | ) -> Result, Status> { 14 | let resp = ContainerStatsResponse { stats: None }; 15 | Ok(Response::new(resp)) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /crates/services/src/cri/runtime_service/container_status.rs: -------------------------------------------------------------------------------- 1 | use crate::cri::{ 2 | api::{ContainerStatusRequest, ContainerStatusResponse}, 3 | cri_service::CRIService, 4 | }; 5 | use std::collections::HashMap; 6 | use tonic::{Request, Response, Status}; 7 | 8 | impl CRIService { 9 | /// handle_container_status returns status of the container. If the container is not present, 10 | /// returns an error. 11 | pub async fn handle_container_status( 12 | &self, 13 | _request: Request, 14 | ) -> Result, Status> { 15 | let resp = ContainerStatusResponse { 16 | info: HashMap::new(), 17 | status: None, 18 | }; 19 | Ok(Response::new(resp)) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /crates/services/src/cri/runtime_service/create_container.rs: -------------------------------------------------------------------------------- 1 | use std::convert::{TryFrom, TryInto}; 2 | 3 | use crate::{ 4 | cri::{ 5 | api::{CreateContainerRequest, CreateContainerResponse}, 6 | cri_service::{CRIService, OptionStatus, ResultStatus}, 7 | }, 8 | error::ServiceError, 9 | }; 10 | use container::container::local::OCIContainerBuilder; 11 | use container::container::Container; 12 | use oci_spec::runtime::{LinuxBuilder, ProcessBuilder, RootBuilder, SpecBuilder, UserBuilder}; 13 | use tonic::{Request, Response, Status}; 14 | 15 | use crate::cri::api::Mount as CRIMount; 16 | use oci_spec::runtime::Mount as OCIMount; 17 | 18 | impl CRIService { 19 | /// handle_create_container creates a new container in specified PodSandbox. 20 | pub async fn handle_create_container( 21 | &self, 22 | request: Request, 23 | ) -> Result, Status> { 24 | let config = request 25 | .into_inner() 26 | .config 27 | .take() 28 | .ok_or_invalid("no container config provided")?; 29 | 30 | let metadata = config 31 | .metadata 32 | .ok_or_invalid("no container metadata provided")?; 33 | 34 | let linux_config = config 35 | .linux 36 | .ok_or_invalid("no container linux config provided")?; 37 | 38 | let security_context = linux_config 39 | .security_context 40 | .ok_or_invalid("no container security context provided")?; 41 | 42 | let spec = SpecBuilder::default() 43 | .process( 44 | ProcessBuilder::default() 45 | .args( 46 | config 47 | .command 48 | .into_iter() 49 | .chain(config.args) 50 | .collect::>(), 51 | ) 52 | .env( 53 | config 54 | .envs 55 | .iter() 56 | .map(|kv| format!("{}={}", kv.key, kv.value)) 57 | .collect::>(), 58 | ) 59 | .cwd(config.working_dir) 60 | .apparmor_profile(security_context.apparmor_profile) 61 | .no_new_privileges(security_context.no_new_privs) 62 | .user( 63 | UserBuilder::default() 64 | .uid( 65 | u32::try_from( 66 | security_context 67 | .run_as_user 68 | .as_ref() 69 | .map(|id| id.value) 70 | .unwrap_or_default(), 71 | ) 72 | .map_internal("failed to convert uid")?, 73 | ) 74 | .gid( 75 | u32::try_from( 76 | security_context 77 | .run_as_group 78 | .as_ref() 79 | .map(|id| id.value) 80 | .unwrap_or_default(), 81 | ) 82 | .map_internal("failed to convert gid")?, 83 | ) 84 | .additional_gids( 85 | security_context 86 | .supplemental_groups 87 | .iter() 88 | .copied() 89 | .map(u32::try_from) 90 | .collect::, _>>() 91 | .map_internal("failed to convert supplemental groups")?, 92 | ) 93 | .build() 94 | .map_internal("failed to build runtime spec user")?, 95 | ) 96 | .build() 97 | .map_internal("failed to build runtime spec process")?, 98 | ) 99 | .linux( 100 | LinuxBuilder::default() 101 | .masked_paths(security_context.masked_paths) 102 | .readonly_paths(security_context.readonly_paths) 103 | .build() 104 | .map_internal("failed to build runtime spec linux")?, 105 | ) 106 | .root( 107 | RootBuilder::default() 108 | .readonly(security_context.readonly_rootfs) 109 | .build() 110 | .map_internal("failed to build")?, 111 | ) 112 | .mounts( 113 | prepare_mounts(&config.mounts) 114 | .map_internal("failed to build oci runtime spec mounts")?, 115 | ) 116 | .annotations(config.annotations) 117 | .build() 118 | .map_internal("failed to create runtime spec")?; 119 | 120 | let mut container = OCIContainerBuilder::default() 121 | .id(format!("{}.{}", metadata.name, metadata.attempt)) 122 | .log_path(config.log_path) 123 | .spec(spec) 124 | .build() 125 | .map_internal("failed to build container")?; 126 | 127 | container 128 | .create() 129 | .await 130 | .map_internal("failed to create container")?; 131 | 132 | let resp = CreateContainerResponse { 133 | container_id: container.id().into(), 134 | }; 135 | 136 | Ok(Response::new(resp)) 137 | } 138 | } 139 | 140 | fn prepare_mounts(cri_mounts: &[CRIMount]) -> Result, ServiceError> { 141 | let mut oci_mounts = cri_mounts 142 | .iter() 143 | .map(|m| m.try_into()) 144 | .collect::, ServiceError>>()?; 145 | oci_mounts.append(&mut oci_spec::runtime::get_default_mounts()); 146 | 147 | Ok(oci_mounts) 148 | } 149 | 150 | #[cfg(test)] 151 | mod tests { 152 | use super::*; 153 | use crate::cri::{ 154 | api::{ 155 | ContainerConfig, ContainerMetadata, CreateContainerRequest, Int64Value, KeyValue, 156 | LinuxContainerConfig, LinuxContainerSecurityContext, Mount, 157 | }, 158 | cri_service::tests::new_cri_service, 159 | }; 160 | use anyhow::Result; 161 | use std::collections::HashMap; 162 | 163 | fn create_request(config: Option) -> Result { 164 | let request = CreateContainerRequest { 165 | pod_sandbox_id: "123".to_owned(), 166 | sandbox_config: None, 167 | config, 168 | }; 169 | 170 | Ok(request) 171 | } 172 | 173 | fn create_config(linux: Option) -> Result { 174 | let tmp = tempfile::tempdir()?; 175 | 176 | let mut labels = HashMap::with_capacity(2); 177 | labels.insert("label1".to_owned(), "lvalue1".to_owned()); 178 | labels.insert("label2".to_owned(), "lvalue2".to_owned()); 179 | 180 | let mut annotations = HashMap::with_capacity(2); 181 | annotations.insert("annotation1".to_owned(), "avalue1".to_owned()); 182 | annotations.insert("annotation2".to_owned(), "avalue2".to_owned()); 183 | 184 | Ok(ContainerConfig { 185 | metadata: Some(ContainerMetadata { 186 | name: "vicious_tuna".to_owned(), 187 | attempt: 1, 188 | }), 189 | image: None, 190 | command: vec!["sleep".to_owned()], 191 | args: vec!["9000".to_owned()], 192 | working_dir: "/var/run/containrs".to_owned(), 193 | envs: vec![ 194 | KeyValue { 195 | key: "PATH".to_owned(), 196 | value: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" 197 | .to_owned(), 198 | }, 199 | KeyValue { 200 | key: "LOG_LEVEL".to_owned(), 201 | value: "debug".to_owned(), 202 | }, 203 | ], 204 | mounts: vec![Mount { 205 | container_path: "/path/in/container".to_owned(), 206 | host_path: tmp.into_path().to_string_lossy().to_string(), 207 | readonly: false, 208 | selinux_relabel: false, 209 | propagation: 0, 210 | }], 211 | devices: Vec::new(), 212 | labels, 213 | annotations, 214 | log_path: "/var/run/containrs/".to_owned(), 215 | stdin: false, 216 | stdin_once: false, 217 | tty: false, 218 | windows: None, 219 | linux, 220 | }) 221 | } 222 | 223 | fn create_linux( 224 | security_context: Option, 225 | ) -> LinuxContainerConfig { 226 | LinuxContainerConfig { 227 | resources: None, 228 | security_context, 229 | } 230 | } 231 | 232 | fn create_security_context() -> LinuxContainerSecurityContext { 233 | LinuxContainerSecurityContext { 234 | capabilities: None, 235 | privileged: false, 236 | run_as_user: Some(Int64Value { value: 1000 }), 237 | run_as_group: Some(Int64Value { value: 1000 }), 238 | supplemental_groups: vec![1000, 1001], 239 | run_as_username: "somebody".to_owned(), 240 | readonly_rootfs: false, 241 | apparmor_profile: "containrs_secure_profile".to_owned(), 242 | no_new_privs: true, 243 | masked_paths: vec!["/proc/kcore".to_owned()], 244 | readonly_paths: vec!["/proc/sys".to_owned()], 245 | namespace_options: None, 246 | seccomp_profile_path: "localhost/docker-default".to_owned(), 247 | selinux_options: None, 248 | } 249 | } 250 | 251 | #[tokio::test] 252 | async fn create_container_success() -> Result<()> { 253 | let sut = new_cri_service()?; 254 | let security_context = create_security_context(); 255 | let linux_config = create_linux(Some(security_context)); 256 | let config = create_config(Some(linux_config))?; 257 | let request = create_request(Some(config))?; 258 | 259 | let response = sut.handle_create_container(Request::new(request)).await?; 260 | assert_eq!(response.get_ref().container_id, "vicious_tuna.1".to_owned()); 261 | Ok(()) 262 | } 263 | 264 | #[tokio::test] 265 | async fn create_container_fail_no_metadata() -> Result<()> { 266 | let sut = new_cri_service()?; 267 | let security_context = create_security_context(); 268 | let linux_config = create_linux(Some(security_context)); 269 | let mut config = create_config(Some(linux_config))?; 270 | config.metadata = None; 271 | let request = create_request(Some(config))?; 272 | 273 | let response = sut.handle_create_container(Request::new(request)).await; 274 | assert!(response.is_err()); 275 | Ok(()) 276 | } 277 | 278 | #[tokio::test] 279 | async fn create_container_fail_no_config() -> Result<()> { 280 | let sut = new_cri_service()?; 281 | let request = create_request(None)?; 282 | 283 | let response = sut.handle_create_container(Request::new(request)).await; 284 | assert!(response.is_err()); 285 | Ok(()) 286 | } 287 | 288 | #[tokio::test] 289 | async fn create_container_fail_no_security() -> Result<()> { 290 | let sut = new_cri_service()?; 291 | let linux_config = create_linux(None); 292 | let config = create_config(Some(linux_config))?; 293 | let request = create_request(Some(config))?; 294 | 295 | let response = sut.handle_create_container(Request::new(request)).await; 296 | assert!(response.is_err()); 297 | Ok(()) 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /crates/services/src/cri/runtime_service/exec.rs: -------------------------------------------------------------------------------- 1 | use crate::cri::{ 2 | api::{ExecRequest, ExecResponse}, 3 | cri_service::CRIService, 4 | }; 5 | use tonic::{Request, Response, Status}; 6 | 7 | impl CRIService { 8 | /// handle_exec prepares a streaming endpoint to execute a command in the container. 9 | pub async fn handle_exec( 10 | &self, 11 | _request: Request, 12 | ) -> Result, Status> { 13 | let resp = ExecResponse { url: "url".into() }; 14 | Ok(Response::new(resp)) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /crates/services/src/cri/runtime_service/exec_sync.rs: -------------------------------------------------------------------------------- 1 | use crate::cri::{ 2 | api::{ExecSyncRequest, ExecSyncResponse}, 3 | cri_service::CRIService, 4 | }; 5 | use tonic::{Request, Response, Status}; 6 | 7 | impl CRIService { 8 | /// handle_exec_sync runs a command in a container synchronously. 9 | pub async fn handle_exec_sync( 10 | &self, 11 | _request: Request, 12 | ) -> Result, Status> { 13 | let resp = ExecSyncResponse { 14 | exit_code: -1, 15 | stderr: Vec::new(), 16 | stdout: Vec::new(), 17 | }; 18 | Ok(Response::new(resp)) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /crates/services/src/cri/runtime_service/list_container_stats.rs: -------------------------------------------------------------------------------- 1 | use crate::cri::{ 2 | api::{ListContainerStatsRequest, ListContainerStatsResponse}, 3 | cri_service::CRIService, 4 | }; 5 | use tonic::{Request, Response, Status}; 6 | 7 | impl CRIService { 8 | /// handle_list_container_stats returns stats of all running containers. 9 | pub async fn handle_list_container_stats( 10 | &self, 11 | _request: Request, 12 | ) -> Result, Status> { 13 | let resp = ListContainerStatsResponse { stats: vec![] }; 14 | Ok(Response::new(resp)) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /crates/services/src/cri/runtime_service/list_containers.rs: -------------------------------------------------------------------------------- 1 | use crate::cri::{ 2 | api::{ListContainersRequest, ListContainersResponse}, 3 | cri_service::CRIService, 4 | }; 5 | use tonic::{Request, Response, Status}; 6 | 7 | impl CRIService { 8 | /// handle_list_containers lists all containers by filters. 9 | pub async fn handle_list_containers( 10 | &self, 11 | _request: Request, 12 | ) -> Result, Status> { 13 | let resp = ListContainersResponse { containers: vec![] }; 14 | Ok(Response::new(resp)) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /crates/services/src/cri/runtime_service/list_pod_sandbox.rs: -------------------------------------------------------------------------------- 1 | use crate::cri::{ 2 | api::{ListPodSandboxRequest, ListPodSandboxResponse}, 3 | cri_service::CRIService, 4 | }; 5 | use tonic::{Request, Response, Status}; 6 | 7 | impl CRIService { 8 | /// handle_list_pod_sandbox returns a list of PodSandboxes. 9 | pub async fn handle_list_pod_sandbox( 10 | &self, 11 | _request: Request, 12 | ) -> Result, Status> { 13 | let reply = ListPodSandboxResponse { items: vec![] }; 14 | Ok(Response::new(reply)) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /crates/services/src/cri/runtime_service/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::cri::{ 2 | api::{self, runtime_service_server::RuntimeService}, 3 | cri_service::CRIService, 4 | }; 5 | use tonic::{Request, Response, Status}; 6 | 7 | mod attach; 8 | mod container_stats; 9 | mod container_status; 10 | mod create_container; 11 | mod exec; 12 | mod exec_sync; 13 | mod list_container_stats; 14 | mod list_containers; 15 | mod list_pod_sandbox; 16 | mod pod_sandbox_status; 17 | mod port_forward; 18 | mod remove_container; 19 | mod remove_pod_sandbox; 20 | mod reopen_container_log; 21 | mod run_pod_sandbox; 22 | mod start_container; 23 | mod status; 24 | mod stop_container; 25 | mod stop_pod_sandbox; 26 | mod update_container_resources; 27 | mod update_runtime_config; 28 | mod version; 29 | 30 | #[tonic::async_trait] 31 | impl RuntimeService for CRIService { 32 | async fn version( 33 | &self, 34 | request: Request, 35 | ) -> Result, Status> { 36 | self.debug_request(&request); 37 | let response = self.handle_version(request).await; 38 | self.debug_response(&response); 39 | response 40 | } 41 | 42 | async fn create_container( 43 | &self, 44 | request: Request, 45 | ) -> Result, Status> { 46 | self.debug_request(&request); 47 | let response = self.handle_create_container(request).await; 48 | self.debug_response(&response); 49 | response 50 | } 51 | 52 | async fn start_container( 53 | &self, 54 | request: Request, 55 | ) -> Result, Status> { 56 | self.debug_request(&request); 57 | let response = self.handle_start_container(request).await; 58 | self.debug_response(&response); 59 | response 60 | } 61 | 62 | async fn stop_container( 63 | &self, 64 | request: Request, 65 | ) -> Result, Status> { 66 | self.debug_request(&request); 67 | let response = self.handle_stop_container(request).await; 68 | self.debug_response(&response); 69 | response 70 | } 71 | 72 | async fn remove_container( 73 | &self, 74 | request: Request, 75 | ) -> Result, Status> { 76 | self.debug_request(&request); 77 | let response = self.handle_remove_container(request).await; 78 | self.debug_response(&response); 79 | response 80 | } 81 | 82 | async fn list_containers( 83 | &self, 84 | request: Request, 85 | ) -> Result, Status> { 86 | self.debug_request(&request); 87 | let response = self.handle_list_containers(request).await; 88 | self.debug_response(&response); 89 | response 90 | } 91 | 92 | async fn container_status( 93 | &self, 94 | request: Request, 95 | ) -> Result, Status> { 96 | self.debug_request(&request); 97 | let response = self.handle_container_status(request).await; 98 | self.debug_response(&response); 99 | response 100 | } 101 | 102 | async fn container_stats( 103 | &self, 104 | request: Request, 105 | ) -> Result, Status> { 106 | self.debug_request(&request); 107 | let response = self.handle_container_stats(request).await; 108 | self.debug_response(&response); 109 | response 110 | } 111 | 112 | async fn list_container_stats( 113 | &self, 114 | request: Request, 115 | ) -> Result, Status> { 116 | self.debug_request(&request); 117 | let response = self.handle_list_container_stats(request).await; 118 | self.debug_response(&response); 119 | response 120 | } 121 | 122 | async fn update_container_resources( 123 | &self, 124 | request: Request, 125 | ) -> Result, Status> { 126 | self.debug_request(&request); 127 | let response = self.handle_update_container_resources(request).await; 128 | self.debug_response(&response); 129 | response 130 | } 131 | 132 | async fn reopen_container_log( 133 | &self, 134 | request: Request, 135 | ) -> Result, Status> { 136 | self.debug_request(&request); 137 | let response = self.handle_reopen_container_log(request).await; 138 | self.debug_response(&response); 139 | response 140 | } 141 | 142 | async fn exec_sync( 143 | &self, 144 | request: Request, 145 | ) -> Result, Status> { 146 | self.debug_request(&request); 147 | let response = self.handle_exec_sync(request).await; 148 | self.debug_response(&response); 149 | response 150 | } 151 | 152 | async fn exec( 153 | &self, 154 | request: Request, 155 | ) -> Result, Status> { 156 | self.debug_request(&request); 157 | let response = self.handle_exec(request).await; 158 | self.debug_response(&response); 159 | response 160 | } 161 | 162 | async fn attach( 163 | &self, 164 | request: Request, 165 | ) -> Result, Status> { 166 | self.debug_request(&request); 167 | let response = self.handle_attach(request).await; 168 | self.debug_response(&response); 169 | response 170 | } 171 | async fn port_forward( 172 | &self, 173 | request: Request, 174 | ) -> Result, Status> { 175 | self.debug_request(&request); 176 | let response = self.handle_port_forward(request).await; 177 | self.debug_response(&response); 178 | response 179 | } 180 | 181 | async fn run_pod_sandbox( 182 | &self, 183 | request: Request, 184 | ) -> Result, Status> { 185 | self.debug_request(&request); 186 | let response = self.handle_run_pod_sandbox(request).await; 187 | self.debug_response(&response); 188 | response 189 | } 190 | 191 | async fn stop_pod_sandbox( 192 | &self, 193 | request: Request, 194 | ) -> Result, Status> { 195 | self.debug_request(&request); 196 | let response = self.handle_stop_pod_sandbox(request).await; 197 | self.debug_response(&response); 198 | response 199 | } 200 | 201 | async fn remove_pod_sandbox( 202 | &self, 203 | request: Request, 204 | ) -> Result, Status> { 205 | self.debug_request(&request); 206 | let response = self.handle_remove_pod_sandbox(request).await; 207 | self.debug_response(&response); 208 | response 209 | } 210 | 211 | async fn list_pod_sandbox( 212 | &self, 213 | request: Request, 214 | ) -> Result, Status> { 215 | self.debug_request(&request); 216 | let response = self.handle_list_pod_sandbox(request).await; 217 | self.debug_response(&response); 218 | response 219 | } 220 | 221 | async fn pod_sandbox_status( 222 | &self, 223 | request: Request, 224 | ) -> Result, Status> { 225 | self.debug_request(&request); 226 | let response = self.handle_pod_sandbox_status(request).await; 227 | self.debug_response(&response); 228 | response 229 | } 230 | 231 | async fn status( 232 | &self, 233 | request: Request, 234 | ) -> Result, Status> { 235 | self.debug_request(&request); 236 | let response = self.handle_status(request).await; 237 | self.debug_response(&response); 238 | response 239 | } 240 | 241 | async fn update_runtime_config( 242 | &self, 243 | request: Request, 244 | ) -> Result, Status> { 245 | self.debug_request(&request); 246 | let response = self.handle_update_runtime_config(request).await; 247 | self.debug_response(&response); 248 | response 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /crates/services/src/cri/runtime_service/pod_sandbox_status.rs: -------------------------------------------------------------------------------- 1 | use crate::cri::{ 2 | api::{PodSandboxStatusRequest, PodSandboxStatusResponse}, 3 | cri_service::CRIService, 4 | }; 5 | use std::collections::HashMap; 6 | use tonic::{Request, Response, Status}; 7 | 8 | impl CRIService { 9 | /// handle_pod_sandbox_status returns the status of the PodSandbox. If the PodSandbox is not 10 | /// present, returns an error. 11 | pub async fn handle_pod_sandbox_status( 12 | &self, 13 | _request: Request, 14 | ) -> Result, Status> { 15 | let reply = PodSandboxStatusResponse { 16 | info: HashMap::new(), 17 | status: None, 18 | }; 19 | Ok(Response::new(reply)) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /crates/services/src/cri/runtime_service/port_forward.rs: -------------------------------------------------------------------------------- 1 | use crate::cri::{ 2 | api::{PortForwardRequest, PortForwardResponse}, 3 | cri_service::CRIService, 4 | }; 5 | use tonic::{Request, Response, Status}; 6 | 7 | impl CRIService { 8 | /// handle_port_forward prepares a streaming endpoint to forward ports from a PodSandbox. 9 | pub async fn handle_port_forward( 10 | &self, 11 | _request: Request, 12 | ) -> Result, Status> { 13 | let resp = PortForwardResponse { url: "url".into() }; 14 | Ok(Response::new(resp)) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /crates/services/src/cri/runtime_service/remove_container.rs: -------------------------------------------------------------------------------- 1 | use crate::cri::{ 2 | api::{RemoveContainerRequest, RemoveContainerResponse}, 3 | cri_service::CRIService, 4 | }; 5 | use tonic::{Request, Response, Status}; 6 | 7 | impl CRIService { 8 | /// handle_remove_container removes the container. If the container is running, the container 9 | /// must be forcibly removed. This call is idempotent, and must not return an error if the 10 | /// container has already been removed. 11 | pub async fn handle_remove_container( 12 | &self, 13 | _request: Request, 14 | ) -> Result, Status> { 15 | let resp = RemoveContainerResponse {}; 16 | Ok(Response::new(resp)) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /crates/services/src/cri/runtime_service/remove_pod_sandbox.rs: -------------------------------------------------------------------------------- 1 | use crate::cri::{ 2 | api::{RemovePodSandboxRequest, RemovePodSandboxResponse}, 3 | cri_service::CRIService, 4 | }; 5 | use tonic::{Request, Response, Status}; 6 | 7 | impl CRIService { 8 | /// handle_remove_pod_sandbox removes the sandbox. If there are any running containers in the 9 | /// sandbox, they must be forcibly terminated and removed. This call is idempotent, and must 10 | /// not return an error if the sandbox has already been removed. 11 | pub async fn handle_remove_pod_sandbox( 12 | &self, 13 | _request: Request, 14 | ) -> Result, Status> { 15 | let reply = RemovePodSandboxResponse {}; 16 | Ok(Response::new(reply)) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /crates/services/src/cri/runtime_service/reopen_container_log.rs: -------------------------------------------------------------------------------- 1 | use crate::cri::{ 2 | api::{ReopenContainerLogRequest, ReopenContainerLogResponse}, 3 | cri_service::CRIService, 4 | }; 5 | use tonic::{Request, Response, Status}; 6 | 7 | impl CRIService { 8 | /// handle_reopen_container_log asks runtime to reopen the stdout/stderr log file for the 9 | /// container. This is often called after the log file has been rotated. If the container is 10 | /// not running, container runtime can choose to either create a new log file and return None, 11 | /// or return an error. Once it returns error, new container log file MUST NOT be created. 12 | pub async fn handle_reopen_container_log( 13 | &self, 14 | _request: Request, 15 | ) -> Result, Status> { 16 | let resp = ReopenContainerLogResponse {}; 17 | Ok(Response::new(resp)) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /crates/services/src/cri/runtime_service/run_pod_sandbox.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use crate::cri::{ 4 | api::{NamespaceMode, RunPodSandboxRequest, RunPodSandboxResponse}, 5 | cri_service::{CRIService, OptionStatus, ResultStatus}, 6 | }; 7 | use log::{debug, info}; 8 | use sandbox::{ 9 | pinned::PinnedSandbox, LinuxNamespaces, SandboxBuilder, SandboxConfigBuilder, 10 | SandboxContextBuilder, SecurityConfigBuilder, 11 | }; 12 | use tonic::{Request, Response, Status}; 13 | 14 | impl CRIService { 15 | /// handle_run_pod_sandbox creates and starts a pod-level sandbox. Runtimes must ensure the 16 | /// sandbox is in the ready state on success. 17 | pub async fn handle_run_pod_sandbox( 18 | &self, 19 | request: Request, 20 | ) -> Result, Status> { 21 | // Take the pod sandbox config 22 | let config = request 23 | .into_inner() 24 | .config 25 | .take() 26 | .ok_or_invalid("no pod sandbox config provided")?; 27 | 28 | // Verify that the metadata exists 29 | let metadata = config 30 | .metadata 31 | .ok_or_invalid("no pod sandbox metadata provided")?; 32 | 33 | let linux_config = config 34 | .linux 35 | .ok_or_invalid("no linux configuration provided")?; 36 | 37 | let security_context = linux_config 38 | .security_context 39 | .ok_or_invalid("no linux security context provided")?; 40 | 41 | let namespace_options = security_context 42 | .namespace_options 43 | .ok_or_invalid("no namespace options provided")?; 44 | 45 | let mut linux_namespaces = LinuxNamespaces::empty(); 46 | if namespace_options.network == NamespaceMode::Pod as i32 { 47 | linux_namespaces |= LinuxNamespaces::NET; 48 | linux_namespaces |= LinuxNamespaces::UTS; 49 | } 50 | if namespace_options.ipc == NamespaceMode::Pod as i32 { 51 | linux_namespaces |= LinuxNamespaces::IPC; 52 | } 53 | if namespace_options.pid == NamespaceMode::Pod as i32 { 54 | linux_namespaces |= LinuxNamespaces::PID; 55 | } 56 | 57 | // Build a new sandbox from it 58 | let mut sandbox = SandboxBuilder::::default() 59 | .context( 60 | SandboxContextBuilder::default() 61 | .config( 62 | SandboxConfigBuilder::default() 63 | .id(metadata.uid) 64 | .name(metadata.name) 65 | .namespace(metadata.namespace) 66 | .attempt(metadata.attempt) 67 | .linux_namespaces(linux_namespaces) 68 | .hostname(config.hostname) 69 | .log_directory(config.log_directory) 70 | .annotations(config.annotations) 71 | .labels(config.labels) 72 | .sysctls(linux_config.sysctls) 73 | .cgroup_parent(PathBuf::from(linux_config.cgroup_parent)) 74 | .security( 75 | SecurityConfigBuilder::default() 76 | .run_as_user(security_context.run_as_user.map(|v| v.value)) 77 | .run_as_group(security_context.run_as_group.map(|v| v.value)) 78 | .supplemental_groups(security_context.supplemental_groups) 79 | .privileged(security_context.privileged) 80 | .seccomp_profile(security_context.seccomp_profile_path) 81 | .readonly_rootfs(security_context.readonly_rootfs) 82 | .build() 83 | .map_internal("build security config")?, 84 | ) 85 | .build() 86 | .map_internal("build sandbox config from metadata")?, 87 | ) 88 | .build() 89 | .map_internal("build sandbox context")?, 90 | ) 91 | .build() 92 | .map_internal("build sandbox from config")?; 93 | 94 | debug!("Created pod sandbox {:?}", sandbox); 95 | 96 | // Run the sandbox 97 | sandbox.run().await.map_internal("run pod sandbox")?; 98 | info!("Started pod sandbox {}", sandbox); 99 | 100 | // Build and return the response 101 | let reply = RunPodSandboxResponse { 102 | pod_sandbox_id: sandbox.id().into(), 103 | }; 104 | Ok(Response::new(reply)) 105 | } 106 | } 107 | 108 | #[cfg(test)] 109 | mod tests { 110 | use super::*; 111 | use crate::cri::{ 112 | api::{ 113 | runtime_service_server::RuntimeService, LinuxPodSandboxConfig, 114 | LinuxSandboxSecurityContext, NamespaceOption, PodSandboxConfig, PodSandboxMetadata, 115 | }, 116 | cri_service::tests::new_cri_service, 117 | }; 118 | use anyhow::Result; 119 | use std::collections::HashMap; 120 | 121 | #[tokio::test] 122 | #[ignore = "requires root"] 123 | async fn run_pod_sandbox_success() -> Result<()> { 124 | let sut = new_cri_service()?; 125 | let test_id = "123"; 126 | let request = RunPodSandboxRequest { 127 | config: Some(PodSandboxConfig { 128 | metadata: Some(PodSandboxMetadata { 129 | name: "".into(), 130 | uid: test_id.into(), 131 | namespace: "".into(), 132 | attempt: 0, 133 | }), 134 | hostname: "".into(), 135 | log_directory: "".into(), 136 | dns_config: None, 137 | port_mappings: vec![], 138 | labels: HashMap::new(), 139 | annotations: HashMap::new(), 140 | linux: Some(LinuxPodSandboxConfig { 141 | cgroup_parent: String::from("abc-pod.slice"), 142 | sysctls: HashMap::new(), 143 | security_context: Some(LinuxSandboxSecurityContext { 144 | namespace_options: Some(NamespaceOption { 145 | network: 0, 146 | pid: 1, 147 | ipc: 0, 148 | target_id: String::from("container_id"), 149 | }), 150 | selinux_options: None, 151 | run_as_user: None, 152 | run_as_group: None, 153 | readonly_rootfs: false, 154 | supplemental_groups: Vec::new(), 155 | privileged: false, 156 | seccomp_profile_path: String::from("/path/to/seccomp"), 157 | }), 158 | }), 159 | }), 160 | runtime_handler: "".into(), 161 | }; 162 | let response = sut.run_pod_sandbox(Request::new(request)).await?; 163 | assert_eq!(response.get_ref().pod_sandbox_id, test_id); 164 | Ok(()) 165 | } 166 | 167 | #[tokio::test] 168 | async fn run_pod_sandbox_fail_no_config() -> Result<()> { 169 | let sut = new_cri_service()?; 170 | let request = RunPodSandboxRequest { 171 | config: None, 172 | runtime_handler: "".into(), 173 | }; 174 | let response = sut.run_pod_sandbox(Request::new(request)).await; 175 | assert!(response.is_err()); 176 | Ok(()) 177 | } 178 | 179 | #[tokio::test] 180 | async fn run_pod_sandbox_fail_no_config_metadata() -> Result<()> { 181 | let sut = new_cri_service()?; 182 | let request = RunPodSandboxRequest { 183 | config: Some(PodSandboxConfig { 184 | metadata: None, 185 | hostname: "".into(), 186 | log_directory: "".into(), 187 | dns_config: None, 188 | port_mappings: vec![], 189 | labels: HashMap::new(), 190 | annotations: HashMap::new(), 191 | linux: None, 192 | }), 193 | runtime_handler: "".into(), 194 | }; 195 | let response = sut.run_pod_sandbox(Request::new(request)).await; 196 | assert!(response.is_err()); 197 | Ok(()) 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /crates/services/src/cri/runtime_service/start_container.rs: -------------------------------------------------------------------------------- 1 | use crate::cri::{ 2 | api::{StartContainerRequest, StartContainerResponse}, 3 | cri_service::CRIService, 4 | }; 5 | use tonic::{Request, Response, Status}; 6 | 7 | impl CRIService { 8 | /// handle_start_container starts the container. 9 | pub async fn handle_start_container( 10 | &self, 11 | _request: Request, 12 | ) -> Result, Status> { 13 | let resp = StartContainerResponse {}; 14 | Ok(Response::new(resp)) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /crates/services/src/cri/runtime_service/status.rs: -------------------------------------------------------------------------------- 1 | use crate::cri::{ 2 | api::{RuntimeCondition, RuntimeStatus, StatusRequest, StatusResponse}, 3 | cri_service::CRIService, 4 | }; 5 | use std::collections::HashMap; 6 | use tonic::{Request, Response, Status}; 7 | 8 | impl CRIService { 9 | /// handle_status returns the status of the runtime. 10 | pub async fn handle_status( 11 | &self, 12 | _request: Request, 13 | ) -> Result, Status> { 14 | let resp = StatusResponse { 15 | status: Some(RuntimeStatus { 16 | conditions: vec![ 17 | RuntimeCondition { 18 | r#type: "RuntimeReady".into(), 19 | status: true, 20 | reason: "".into(), 21 | message: "".into(), 22 | }, 23 | RuntimeCondition { 24 | r#type: "NetworkReady".into(), 25 | status: true, 26 | reason: "".into(), 27 | message: "".into(), 28 | }, 29 | ], 30 | }), 31 | info: HashMap::new(), 32 | }; 33 | Ok(Response::new(resp)) 34 | } 35 | } 36 | 37 | #[cfg(test)] 38 | mod tests { 39 | use super::*; 40 | use crate::cri::{ 41 | api::runtime_service_server::RuntimeService, cri_service::tests::new_cri_service, 42 | }; 43 | use anyhow::{Context, Result}; 44 | 45 | #[tokio::test] 46 | async fn runtime_status() -> Result<()> { 47 | let sut = new_cri_service()?; 48 | let request = StatusRequest { verbose: true }; 49 | let response = sut.status(Request::new(request)).await?; 50 | let conditions = response 51 | .into_inner() 52 | .status 53 | .context("no status")? 54 | .conditions; 55 | assert_eq!(conditions.len(), 2); 56 | let runtime_condition = conditions.get(0).context("no runtime condition")?; 57 | let network_condition = conditions.get(1).context("no network condition")?; 58 | assert_eq!(runtime_condition.r#type, "RuntimeReady"); 59 | assert_eq!(runtime_condition.status, true); 60 | assert_eq!(network_condition.r#type, "NetworkReady"); 61 | assert_eq!(network_condition.status, true); 62 | Ok(()) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /crates/services/src/cri/runtime_service/stop_container.rs: -------------------------------------------------------------------------------- 1 | use crate::cri::{ 2 | api::{StopContainerRequest, StopContainerResponse}, 3 | cri_service::CRIService, 4 | }; 5 | use tonic::{Request, Response, Status}; 6 | 7 | impl CRIService { 8 | /// handle_stop_container stops a running container with a grace period (i.e., timeout). This 9 | /// call is idempotent, and must not return an error if the container has already been stopped. 10 | pub async fn handle_stop_container( 11 | &self, 12 | _request: Request, 13 | ) -> Result, Status> { 14 | let resp = StopContainerResponse {}; 15 | Ok(Response::new(resp)) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /crates/services/src/cri/runtime_service/stop_pod_sandbox.rs: -------------------------------------------------------------------------------- 1 | use crate::cri::{ 2 | api::{StopPodSandboxRequest, StopPodSandboxResponse}, 3 | cri_service::CRIService, 4 | }; 5 | use tonic::{Request, Response, Status}; 6 | 7 | impl CRIService { 8 | /// handle_stop_pod_sandbox stops any running process that is part of the sandbox and reclaims 9 | /// network resources (e.g., IP addresses) allocated to the sandbox. If there are any running 10 | /// containers in the sandbox, they must be forcibly terminated. This call is idempotent, and 11 | /// must not return an error if all relevant resources have already been reclaimed. kubelet 12 | /// will call StopPodSandbox at least once before calling RemovePodSandbox. It will also 13 | /// attempt to reclaim resources eagerly, as soon as a sandbox is not needed. Hence, multiple 14 | /// StopPodSandbox calls are expected. 15 | pub async fn handle_stop_pod_sandbox( 16 | &self, 17 | _request: Request, 18 | ) -> Result, Status> { 19 | let reply = StopPodSandboxResponse {}; 20 | Ok(Response::new(reply)) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /crates/services/src/cri/runtime_service/update_container_resources.rs: -------------------------------------------------------------------------------- 1 | use crate::cri::{ 2 | api::{UpdateContainerResourcesRequest, UpdateContainerResourcesResponse}, 3 | cri_service::CRIService, 4 | }; 5 | use tonic::{Request, Response, Status}; 6 | 7 | impl CRIService { 8 | /// handle_update_container_resources updates ContainerConfig of the container. 9 | pub async fn handle_update_container_resources( 10 | &self, 11 | _request: Request, 12 | ) -> Result, Status> { 13 | let resp = UpdateContainerResourcesResponse {}; 14 | Ok(Response::new(resp)) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /crates/services/src/cri/runtime_service/update_runtime_config.rs: -------------------------------------------------------------------------------- 1 | use crate::cri::{ 2 | api::{UpdateRuntimeConfigRequest, UpdateRuntimeConfigResponse}, 3 | cri_service::CRIService, 4 | }; 5 | use tonic::{Request, Response, Status}; 6 | 7 | impl CRIService { 8 | /// handle_update_runtime_config updates the runtime configuration based on the given request. 9 | pub async fn handle_update_runtime_config( 10 | &self, 11 | _request: Request, 12 | ) -> Result, Status> { 13 | let resp = UpdateRuntimeConfigResponse {}; 14 | Ok(Response::new(resp)) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /crates/services/src/cri/runtime_service/version.rs: -------------------------------------------------------------------------------- 1 | use crate::cri::{ 2 | api::{VersionRequest, VersionResponse}, 3 | cri_service::CRIService, 4 | }; 5 | use tonic::{Request, Response, Status}; 6 | 7 | impl CRIService { 8 | /// handle_version returns the runtime name, runtime version, and runtime API version. 9 | pub async fn handle_version( 10 | &self, 11 | _request: Request, 12 | ) -> Result, Status> { 13 | let resp = VersionResponse { 14 | version: "0.1.0".into(), 15 | runtime_api_version: "v1alpha1".into(), 16 | runtime_name: "crust".into(), 17 | runtime_version: "0.0.1".into(), 18 | }; 19 | Ok(Response::new(resp)) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /crates/services/src/error.rs: -------------------------------------------------------------------------------- 1 | use oci_spec::OciSpecError; 2 | use std::io; 3 | use thiserror::Error; 4 | 5 | #[derive(Debug, Error)] 6 | pub enum ServiceError { 7 | #[error("{0}")] 8 | Other(String), 9 | #[error("{0}")] 10 | IO(#[from] io::Error), 11 | #[error("{0}")] 12 | Spec(#[from] OciSpecError), 13 | } 14 | -------------------------------------------------------------------------------- /crates/services/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Everything Kubernetes related, like the actual GRPC server implementation and CRI API 2 | //! definition. 3 | 4 | mod cri; 5 | pub mod error; 6 | pub mod server; 7 | -------------------------------------------------------------------------------- /crates/services/src/server/config.rs: -------------------------------------------------------------------------------- 1 | //! Configuration related structures 2 | use clap::{crate_name, crate_version, Parser}; 3 | use derive_builder::Builder; 4 | use getset::{CopyGetters, Getters}; 5 | use lazy_static::lazy_static; 6 | use nix::unistd::{self, Uid}; 7 | use serde::{Deserialize, Serialize}; 8 | use std::{env, path::PathBuf}; 9 | use strum::{AsRefStr, EnumString}; 10 | 11 | lazy_static! { 12 | static ref DEFAULT_SOCK_PATH: String = Config::default_sock_path().display().to_string(); 13 | static ref DEFAULT_STORAGE_PATH: String = Config::default_storage_path().display().to_string(); 14 | static ref DEFAULT_CNI_PLUGIN_PATHS: String = 15 | env::var("PATH").unwrap_or_else(|_| "/opt/cni/bin".into()); 16 | } 17 | 18 | #[derive(Builder, Parser, CopyGetters, Getters, Deserialize, Serialize)] 19 | #[builder(default, pattern = "owned", setter(into, strip_option))] 20 | #[serde(rename_all = "kebab-case")] 21 | #[command( 22 | about("containrs - Container Runtime for Kubernetes (CRI) and friends"), 23 | after_help("More info at: https://github.com/containers/containrs"), 24 | version(crate_version!()), 25 | )] 26 | /// Config is the main configuration structure for the server. 27 | pub struct Config { 28 | #[get = "pub"] 29 | #[arg( 30 | default_value("info"), 31 | env("CRI_LOG_LEVEL"), 32 | long("log-level"), 33 | value_parser(["trace", "debug", "info", "warn", "error", "off"]), 34 | short('l'), 35 | value_name("LEVEL") 36 | )] 37 | /// The logging level of the application. 38 | log_level: String, 39 | 40 | #[get = "pub"] 41 | #[arg( 42 | default_value("lib"), 43 | env("CRI_LOG_SCOPE"), 44 | long("log-scope"), 45 | value_parser([LogScope::Lib.as_ref(), LogScope::Global.as_ref()]), 46 | value_name("SCOPE") 47 | )] 48 | /// The logging scope of the application. If set to `global`, then all dependent crates will 49 | /// log on the provided level, too. Otherwise the logs are scoped to this application only. 50 | log_scope: String, 51 | 52 | #[get = "pub"] 53 | #[arg( 54 | default_value(&**DEFAULT_SOCK_PATH), 55 | env("CRI_SOCK_PATH"), 56 | long("sock-path"), 57 | value_name("PATH") 58 | )] 59 | /// The path to the unix socket for the server. 60 | sock_path: PathBuf, 61 | 62 | #[get = "pub"] 63 | #[arg( 64 | default_value(&**DEFAULT_STORAGE_PATH), 65 | env("CRI_STORAGE_PATH"), 66 | long("storage-path"), 67 | value_name("PATH") 68 | )] 69 | /// The path to the persistent storage for the server. 70 | storage_path: PathBuf, 71 | 72 | #[get = "pub"] 73 | #[arg( 74 | env("CRI_CNI_DEFAULT_NETWORK"), 75 | long("cni-default-network"), 76 | value_name("NAME") 77 | )] 78 | /// The default CNI network name to choose. 79 | cni_default_network: Option, 80 | 81 | #[get = "pub"] 82 | #[arg( 83 | default_value("/etc/cni/net.d"), 84 | env("CRI_CNI_CONFIG_PATHS"), 85 | long("cni-config-paths"), 86 | value_name("PATH") 87 | )] 88 | /// The paths to the CNI configurations. 89 | cni_config_paths: Vec, 90 | 91 | #[get = "pub"] 92 | #[arg( 93 | default_value(&**DEFAULT_CNI_PLUGIN_PATHS), 94 | env("CRI_CNI_PLUGIN_PATHS"), 95 | long("cni-plugin-paths"), 96 | value_name("PATH") 97 | )] 98 | /// The paths to the CNI plugin binaries, separated by the OS typic separator. 99 | cni_plugin_paths: String, 100 | } 101 | 102 | impl Config { 103 | /// Return the default socket path depending if running as root or not. 104 | fn default_sock_path() -> PathBuf { 105 | Self::default_run_path(unistd::getuid()) 106 | .join(crate_name!()) 107 | .with_extension("sock") 108 | } 109 | 110 | /// Return the default storage path depending if running as root or not. 111 | fn default_storage_path() -> PathBuf { 112 | Self::default_run_path(unistd::getuid()).join("storage") 113 | } 114 | 115 | /// Return the default run path depending on the provided user ID. 116 | fn default_run_path(uid: Uid) -> PathBuf { 117 | if uid.is_root() { 118 | PathBuf::from("/var/run/").join(crate_name!()) 119 | } else { 120 | PathBuf::from("/var/run/user") 121 | .join(uid.to_string()) 122 | .join(crate_name!()) 123 | } 124 | } 125 | } 126 | 127 | impl Default for Config { 128 | fn default() -> Self { 129 | Self::parse() 130 | } 131 | } 132 | 133 | #[derive(AsRefStr, Clone, Copy, Debug, Deserialize, Eq, EnumString, PartialEq, Serialize)] 134 | #[strum(serialize_all = "snake_case")] 135 | /// Defines the scope of the log level 136 | pub enum LogScope { 137 | /// Logging will only happen on a library level. 138 | Lib, 139 | 140 | /// All dependent libraries will log too. 141 | Global, 142 | } 143 | 144 | #[cfg(test)] 145 | pub mod tests { 146 | use super::*; 147 | use anyhow::Result; 148 | 149 | #[test] 150 | fn default_config() { 151 | let c = Config::default(); 152 | assert_eq!(c.log_level(), "info"); 153 | assert!(c.cni_default_network().is_none()); 154 | assert_eq!(c.cni_config_paths().len(), 1); 155 | assert!(!c.cni_plugin_paths().is_empty()); 156 | } 157 | 158 | #[test] 159 | fn build_config() -> Result<()> { 160 | let c = ConfigBuilder::default() 161 | .log_level("warn") 162 | .sock_path("/some/path") 163 | .cni_default_network("default-network") 164 | .cni_config_paths(["a", "b"].iter().map(PathBuf::from).collect::>()) 165 | .cni_plugin_paths("1:2:3") 166 | .log_scope(LogScope::Global.as_ref()) 167 | .storage_path("/some/other/path") 168 | .build()?; 169 | 170 | assert_eq!(c.log_level(), "warn"); 171 | assert_eq!(&c.sock_path().display().to_string(), "/some/path"); 172 | assert_eq!(c.log_scope(), LogScope::Global.as_ref()); 173 | assert_eq!(&c.storage_path().display().to_string(), "/some/other/path"); 174 | assert_eq!(c.cni_default_network(), &Some("default-network".into())); 175 | assert_eq!(c.cni_config_paths().len(), 2); 176 | assert_eq!(c.cni_plugin_paths(), "1:2:3"); 177 | 178 | Ok(()) 179 | } 180 | 181 | #[test] 182 | fn default_run_path_root() { 183 | let uid = Uid::from_raw(0); 184 | assert!(uid.is_root()); 185 | assert!(!Config::default_run_path(uid) 186 | .display() 187 | .to_string() 188 | .contains("user")); 189 | } 190 | 191 | #[test] 192 | fn default_run_path_non_root() { 193 | let uid = Uid::from_raw(1000); 194 | assert!(!uid.is_root()); 195 | assert!(Config::default_run_path(uid) 196 | .display() 197 | .to_string() 198 | .contains(&uid.to_string())); 199 | } 200 | 201 | #[test] 202 | fn default_sock_path() { 203 | assert!(Config::default_sock_path() 204 | .display() 205 | .to_string() 206 | .contains(".sock")); 207 | } 208 | 209 | #[test] 210 | fn default_storage_path() { 211 | assert!(Config::default_storage_path() 212 | .display() 213 | .to_string() 214 | .contains("storage")); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /crates/services/src/server/mod.rs: -------------------------------------------------------------------------------- 1 | //! Container Runtime Interface server implementation 2 | use crate::cri::{ 3 | api::{image_service_server::ImageServiceServer, runtime_service_server::RuntimeServiceServer}, 4 | cri_service::CRIServiceBuilder, 5 | }; 6 | use anyhow::{bail, Context, Result}; 7 | use clap::crate_name; 8 | use common::unix_stream::UnixStream; 9 | pub use config::{Config, LogScope}; 10 | use env_logger::fmt::Color; 11 | use futures::TryFutureExt; 12 | use log::{debug, info, trace, LevelFilter}; 13 | use network::{ 14 | cni::{CNIBuilder, CNI}, 15 | Network, NetworkBuilder, 16 | }; 17 | use std::{env, io::Write, str::FromStr}; 18 | use storage::{default_key_value_storage::DefaultKeyValueStorage, KeyValueStorage}; 19 | #[cfg(unix)] 20 | use tokio::net::UnixListener; 21 | use tokio::{ 22 | fs, 23 | signal::unix::{signal, SignalKind}, 24 | }; 25 | use tonic::transport; 26 | 27 | mod config; 28 | 29 | /// Server is the main instance to run the Container Runtime Interface 30 | pub struct Server { 31 | config: Config, 32 | } 33 | 34 | impl Server { 35 | /// Create a new server instance 36 | pub fn new(config: Config) -> Self { 37 | Self { config } 38 | } 39 | 40 | /// Start a new server with its default values 41 | pub async fn start(self) -> Result<()> { 42 | self.set_logging_verbosity() 43 | .context("set logging verbosity")?; 44 | 45 | // Setup the storage and pass it to the service 46 | let storage = 47 | DefaultKeyValueStorage::open(&self.config.storage_path().join("cri-service"))?; 48 | let cri_service = CRIServiceBuilder::default() 49 | .storage(storage.clone()) 50 | .build()?; 51 | 52 | let network = self.initialize_network().await.context("init network")?; 53 | 54 | // Build a new socket from the config 55 | let uds = self.unix_domain_listener().await?; 56 | 57 | // Handle shutdown based on signals 58 | let mut shutdown_terminate = signal(SignalKind::terminate())?; 59 | let mut shutdown_interrupt = signal(SignalKind::interrupt())?; 60 | 61 | info!( 62 | "Runtime server listening on {}", 63 | self.config.sock_path().display() 64 | ); 65 | 66 | #[allow(irrefutable_let_patterns)] 67 | let incoming = async_stream::stream! { 68 | while let item = uds.accept().map_ok(|(st, _)| UnixStream(st)).await { 69 | yield item; 70 | } 71 | }; 72 | 73 | tokio::select! { 74 | res = transport::Server::builder() 75 | .add_service(RuntimeServiceServer::new(cri_service.clone())) 76 | .add_service(ImageServiceServer::new(cri_service)) 77 | .serve_with_incoming(incoming) => { 78 | res.context("run GRPC server")? 79 | } 80 | _ = shutdown_interrupt.recv() => { 81 | info!("Got interrupt signal, shutting down server"); 82 | } 83 | _ = shutdown_terminate.recv() => { 84 | info!("Got termination signal, shutting down server"); 85 | } 86 | } 87 | 88 | self.cleanup(storage, network).await 89 | } 90 | 91 | /// Create a new UnixListener from the configs socket path. 92 | async fn unix_domain_listener(&self) -> Result { 93 | let sock_path = self.config.sock_path(); 94 | if !sock_path.is_absolute() { 95 | bail!( 96 | "specified socket path {} is not absolute", 97 | sock_path.display() 98 | ) 99 | } 100 | if sock_path.exists() { 101 | fs::remove_file(sock_path) 102 | .await 103 | .with_context(|| format!("unable to remove socket file {}", sock_path.display()))?; 104 | } else { 105 | let sock_dir = sock_path 106 | .parent() 107 | .context("unable to get socket path directory")?; 108 | fs::create_dir_all(sock_dir) 109 | .await 110 | .with_context(|| format!("unable to create socket dir {}", sock_dir.display()))?; 111 | } 112 | 113 | UnixListener::bind(sock_path).context("unable to bind socket from path") 114 | } 115 | 116 | /// Initialize the logger and set the verbosity to the provided level. 117 | fn set_logging_verbosity(&self) -> Result<()> { 118 | // Set the logging verbosity via the env 119 | let level = if self.config.log_scope() == LogScope::Global.as_ref() { 120 | self.config.log_level().to_string() 121 | } else { 122 | format!("{}={}", crate_name!(), self.config.log_level()) 123 | }; 124 | env::set_var("RUST_LOG", level); 125 | 126 | // Initialize the logger with the format: 127 | // [YYYY-MM-DDTHH:MM:SS:MMMZ LEVEL crate::module file:LINE] MSG… 128 | // The file and line will be only printed when running with debug or trace level. 129 | let log_level = LevelFilter::from_str(self.config.log_level())?; 130 | env_logger::builder() 131 | .format(move |buf, r| { 132 | let mut style = buf.style(); 133 | style.set_color(Color::Black).set_intense(true); 134 | writeln!( 135 | buf, 136 | "{}{} {:<5} {}{}{} {}", 137 | style.value("["), 138 | buf.timestamp_millis(), 139 | buf.default_styled_level(r.level()), 140 | r.target(), 141 | match (log_level >= LevelFilter::Debug, r.file(), r.line()) { 142 | (true, Some(file), Some(line)) => format!(" {}:{}", file, line), 143 | _ => "".into(), 144 | }, 145 | style.value("]"), 146 | r.args() 147 | ) 148 | }) 149 | .try_init() 150 | .context("init env logger") 151 | } 152 | 153 | /// Create a new network and initialize it from the internal configuration. 154 | async fn initialize_network(&self) -> Result> { 155 | let mut cni_network = CNIBuilder::default() 156 | .default_network_name(self.config.cni_default_network().clone()) 157 | .config_paths(self.config.cni_config_paths().clone()) 158 | .plugin_paths(self.config.cni_plugin_paths()) 159 | .storage_path(self.config.storage_path().join("cni")) 160 | .build() 161 | .context("build CNI network data")?; 162 | 163 | cni_network 164 | .initialize() 165 | .await 166 | .context("initialize CNI network")?; 167 | 168 | let network = NetworkBuilder::::default() 169 | .implementation(cni_network) 170 | .build() 171 | .context("build CNI network")?; 172 | 173 | Ok(network) 174 | } 175 | 176 | /// Cleanup the server and persist any data if necessary. 177 | async fn cleanup( 178 | self, 179 | mut storage: DefaultKeyValueStorage, 180 | mut network: Network, 181 | ) -> Result<()> { 182 | debug!("Cleaning up server"); 183 | 184 | trace!("Persisting storage"); 185 | storage.persist().context("persist storage")?; 186 | 187 | trace!("Removing socket path"); 188 | std::fs::remove_file(self.config.sock_path()).with_context(|| { 189 | format!( 190 | "unable to remove socket path {}", 191 | self.config.sock_path().display() 192 | ) 193 | })?; 194 | 195 | trace!("Stopping network"); 196 | network.cleanup().await.context("clean up network")?; 197 | 198 | trace!("Server shut down"); 199 | Ok(()) 200 | } 201 | } 202 | 203 | #[cfg(test)] 204 | mod tests { 205 | use super::*; 206 | use crate::server::config::ConfigBuilder; 207 | use tempfile::{tempdir, NamedTempFile}; 208 | 209 | #[tokio::test] 210 | async fn unix_domain_listener_success() -> Result<()> { 211 | let sock_path = &tempdir()?.path().join("test.sock"); 212 | let config = ConfigBuilder::default().sock_path(sock_path).build()?; 213 | let sut = Server::new(config); 214 | 215 | assert!(!sock_path.exists()); 216 | sut.unix_domain_listener().await?; 217 | assert!(sock_path.exists()); 218 | 219 | Ok(()) 220 | } 221 | 222 | #[tokio::test] 223 | async fn unix_domain_listener_success_exists() -> Result<()> { 224 | let sock_path = NamedTempFile::new()?; 225 | let config = ConfigBuilder::default() 226 | .sock_path(sock_path.path()) 227 | .build()?; 228 | let sut = Server::new(config); 229 | 230 | assert!(sock_path.path().exists()); 231 | sut.unix_domain_listener().await?; 232 | assert!(sock_path.path().exists()); 233 | 234 | Ok(()) 235 | } 236 | 237 | #[tokio::test] 238 | async fn unix_domain_listener_fail_not_absolute() -> Result<()> { 239 | let config = ConfigBuilder::default() 240 | .sock_path("not/absolute/path") 241 | .build()?; 242 | let sut = Server::new(config); 243 | 244 | assert!(sut.unix_domain_listener().await.is_err()); 245 | 246 | Ok(()) 247 | } 248 | 249 | #[tokio::test] 250 | async fn initialize_network_success() -> Result<()> { 251 | let config = ConfigBuilder::default() 252 | .cni_config_paths(vec![tempdir()?.into_path()]) 253 | .storage_path(tempdir()?.path()) 254 | .build()?; 255 | let sut = Server::new(config); 256 | sut.initialize_network().await?; 257 | Ok(()) 258 | } 259 | 260 | #[tokio::test] 261 | async fn initialize_network_wrong_storage_path() -> Result<()> { 262 | let config = ConfigBuilder::default() 263 | .storage_path("/proc/storage") 264 | .build()?; 265 | let sut = Server::new(config); 266 | assert!(sut.initialize_network().await.is_err()); 267 | Ok(()) 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /crates/storage/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "storage" 3 | version = "0.1.0" 4 | edition = "2018" 5 | authors = [ 6 | "Furisto", 7 | "Mrunal Patel ", 8 | "Sascha Grunert ", 9 | "utam0k ", 10 | ] 11 | documentation = "https://docs.rs/containrs" 12 | homepage = "https://github.com/containers/containrs" 13 | repository = "https://github.com/containers/containrs" 14 | license = "Apache-2.0" 15 | keywords = ["runtime", "kubernetes", "cri", "container", "pod"] 16 | categories = ["network-programming", "api-bindings"] 17 | 18 | [dependencies] 19 | anyhow = "1.0.66" 20 | getset = "0.1.2" 21 | log = { version = "0.4.17", features = ["serde", "std"] } 22 | rmp-serde = "1.1.1" 23 | serde = { version = "1.0.147", features = ["derive"] } 24 | sled = "0.34.7" 25 | 26 | [dev-dependencies] 27 | tempfile = "3.3.0" 28 | -------------------------------------------------------------------------------- /crates/storage/src/default_key_value_storage.rs: -------------------------------------------------------------------------------- 1 | //! The default key value storage implementation for storing arbitrary data. 2 | 3 | use super::KeyValueStorage; 4 | use anyhow::{Context, Result}; 5 | use getset::Getters; 6 | use log::trace; 7 | use serde::{de::DeserializeOwned, Serialize}; 8 | use sled::Db; 9 | use std::{convert::AsRef, path::Path}; 10 | 11 | #[derive(Debug, Clone, Getters)] 12 | /// A default key value storage implementation 13 | pub struct DefaultKeyValueStorage { 14 | #[get] 15 | /// The internal database. 16 | db: Db, 17 | } 18 | 19 | impl KeyValueStorage for DefaultKeyValueStorage { 20 | /// Open the database, whereas the `Path` has to be a directory. 21 | fn open

(path: P) -> Result 22 | where 23 | P: AsRef, 24 | { 25 | trace!("Opening storage {}", path.as_ref().display()); 26 | Ok(Self { 27 | db: sled::open(&path).with_context(|| { 28 | format!("failed to open storage path {}", path.as_ref().display()) 29 | })?, 30 | }) 31 | } 32 | 33 | fn get(&self, key: K) -> Result> 34 | where 35 | K: AsRef<[u8]>, 36 | V: DeserializeOwned, 37 | { 38 | match self 39 | .db() 40 | .get(key) 41 | .context("failed to retrieve value for key")? 42 | { 43 | None => Ok(None), 44 | Some(value) => { 45 | trace!("Got result from storage (len = {})", value.len()); 46 | Ok(Some( 47 | rmp_serde::from_slice(&value).context("deserialize value")?, 48 | )) 49 | } 50 | } 51 | } 52 | 53 | fn insert(&mut self, key: K, value: V) -> Result<()> 54 | where 55 | K: AsRef<[u8]>, 56 | V: Serialize, 57 | { 58 | self.db() 59 | .insert( 60 | key, 61 | rmp_serde::to_vec(&value).context("failed to serialize value")?, 62 | ) 63 | .context("failed to insert key and value")?; 64 | trace!("Inserted item into storage (count = {})", self.db().len()); 65 | Ok(()) 66 | } 67 | 68 | fn remove(&mut self, key: K) -> Result<()> 69 | where 70 | K: AsRef<[u8]>, 71 | { 72 | self.db().remove(key)?.context("failed to remove value")?; 73 | trace!("Removed item from storage (count = {})", self.db().len()); 74 | Ok(()) 75 | } 76 | 77 | fn persist(&mut self) -> Result<()> { 78 | self.db().flush().context("failed to persist db")?; 79 | trace!("Persisted storage"); 80 | Ok(()) 81 | } 82 | } 83 | 84 | #[cfg(test)] 85 | mod tests { 86 | use super::*; 87 | use serde::Deserialize; 88 | use tempfile::TempDir; 89 | 90 | #[test] 91 | fn get_existing_value() -> Result<()> { 92 | let dir = TempDir::new()?; 93 | let mut db = DefaultKeyValueStorage::open(dir.path())?; 94 | 95 | let (k, v) = ("key", "value"); 96 | db.insert(k, v)?; 97 | let res: String = db.get(k)?.context("value is none")?; 98 | assert_eq!(res, v); 99 | Ok(()) 100 | } 101 | 102 | #[test] 103 | fn get_nonexisting_value() -> Result<()> { 104 | let dir = TempDir::new()?; 105 | let db = DefaultKeyValueStorage::open(dir.path())?; 106 | 107 | assert!(db.get::<_, String>("key")?.is_none()); 108 | Ok(()) 109 | } 110 | 111 | #[test] 112 | fn remove_value() -> Result<()> { 113 | let dir = TempDir::new()?; 114 | let mut db = DefaultKeyValueStorage::open(dir.path())?; 115 | 116 | let (k, v) = ("key", "value"); 117 | db.insert(k, v)?; 118 | db.remove(k)?; 119 | assert!(db.get::<_, String>(k)?.is_none()); 120 | 121 | Ok(()) 122 | } 123 | 124 | #[test] 125 | fn persist() -> Result<()> { 126 | let dir = TempDir::new()?; 127 | let mut db = DefaultKeyValueStorage::open(dir.path())?; 128 | 129 | db.insert("key", "value")?; 130 | db.persist() 131 | } 132 | 133 | #[test] 134 | fn insert_values() -> Result<()> { 135 | let dir = TempDir::new()?; 136 | let mut db = DefaultKeyValueStorage::open(dir.path())?; 137 | 138 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 139 | struct NewValue(String); 140 | 141 | let k1 = vec![1, 2, 3]; 142 | let v1 = NewValue("value".into()); 143 | 144 | let v2 = "value 2"; 145 | let k2 = vec![3, 2, 1]; 146 | 147 | db.insert(k1.clone(), v1.clone())?; 148 | assert_eq!( 149 | db.get::<_, NewValue>(k1)?.context("value for k1 is none")?, 150 | v1 151 | ); 152 | 153 | db.insert(k2.clone(), v2.clone())?; 154 | assert_eq!( 155 | db.get::<_, String>(k2)?.context("value for k2 is none")?, 156 | v2 157 | ); 158 | Ok(()) 159 | } 160 | 161 | #[test] 162 | fn open_twice() -> Result<()> { 163 | let dir = TempDir::new()?; 164 | 165 | let mut db1 = DefaultKeyValueStorage::open(dir.path())?; 166 | let db2 = db1.clone(); 167 | 168 | let (k, v) = ("key", "value"); 169 | 170 | db1.insert(k, v)?; 171 | 172 | let res1: String = db1.get(k)?.context("value 1 is none")?; 173 | assert_eq!(res1, v); 174 | 175 | let res2: String = db2.get(k)?.context("value 2 is none")?; 176 | assert_eq!(res2, v); 177 | 178 | Ok(()) 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /crates/storage/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Basic storage types 2 | 3 | pub mod default_key_value_storage; 4 | pub mod memory_key_value_storage; 5 | 6 | use anyhow::Result; 7 | use serde::{de::DeserializeOwned, Serialize}; 8 | use std::{convert::AsRef, path::Path}; 9 | 10 | /// The data storage trait which defines the methods a storage implementation should fulfill. 11 | pub trait KeyValueStorage { 12 | /// Load the storage from the provided path. 13 | fn open

(path: P) -> Result 14 | where 15 | Self: Sized, 16 | P: AsRef; 17 | 18 | /// Get an arbitrary item from the storage. 19 | fn get(&self, key: K) -> Result> 20 | where 21 | K: AsRef<[u8]>, 22 | V: DeserializeOwned; 23 | 24 | /// Insert an item into the storage. 25 | fn insert(&mut self, key: K, value: V) -> Result<()> 26 | where 27 | K: AsRef<[u8]>, 28 | V: Serialize; 29 | 30 | /// Remove an item from the storage. 31 | fn remove(&mut self, key: K) -> Result<()> 32 | where 33 | K: AsRef<[u8]>; 34 | 35 | /// Save the storage to disk so that it is safe to stop the application. 36 | fn persist(&mut self) -> Result<()>; 37 | } 38 | -------------------------------------------------------------------------------- /crates/storage/src/memory_key_value_storage.rs: -------------------------------------------------------------------------------- 1 | use crate::KeyValueStorage; 2 | use anyhow::Result; 3 | use getset::{Getters, MutGetters}; 4 | use std::collections::HashMap; 5 | 6 | #[derive(Debug, Default, Clone, Getters, MutGetters)] 7 | pub struct MemoryKeyValueStorage { 8 | #[getset(get, get_mut)] 9 | db: HashMap, Vec>, 10 | } 11 | 12 | impl KeyValueStorage for MemoryKeyValueStorage { 13 | fn open

(_: P) -> Result 14 | where 15 | Self: Sized, 16 | P: AsRef, 17 | { 18 | Ok(Self::default()) 19 | } 20 | 21 | fn get(&self, key: K) -> Result> 22 | where 23 | K: AsRef<[u8]>, 24 | V: serde::de::DeserializeOwned, 25 | { 26 | match self.db().get(key.as_ref()) { 27 | None => Ok(None), 28 | Some(value) => Ok(Some(rmp_serde::from_slice(value)?)), 29 | } 30 | } 31 | 32 | fn insert(&mut self, key: K, value: V) -> Result<()> 33 | where 34 | K: AsRef<[u8]>, 35 | V: serde::Serialize, 36 | { 37 | self.db_mut() 38 | .insert(key.as_ref().to_vec(), rmp_serde::to_vec(&value)?); 39 | Ok(()) 40 | } 41 | 42 | fn remove(&mut self, key: K) -> Result<()> 43 | where 44 | K: AsRef<[u8]>, 45 | { 46 | self.db_mut().remove(key.as_ref()); 47 | Ok(()) 48 | } 49 | 50 | fn persist(&mut self) -> Result<()> { 51 | Ok(()) 52 | } 53 | } 54 | 55 | #[cfg(test)] 56 | mod tests { 57 | use super::*; 58 | use anyhow::Context; 59 | use serde::{Deserialize, Serialize}; 60 | 61 | #[test] 62 | fn get_existing_value() -> Result<()> { 63 | let mut db = MemoryKeyValueStorage::default(); 64 | 65 | let (k, v) = ("key", "value"); 66 | db.insert(k, v)?; 67 | let res: String = db.get(k)?.context("value is none")?; 68 | assert_eq!(res, v); 69 | Ok(()) 70 | } 71 | 72 | #[test] 73 | fn get_nonexisting_value() -> Result<()> { 74 | let db = MemoryKeyValueStorage::default(); 75 | 76 | assert!(db.get::<_, String>("key")?.is_none()); 77 | Ok(()) 78 | } 79 | 80 | #[test] 81 | fn remove_value() -> Result<()> { 82 | let mut db = MemoryKeyValueStorage::default(); 83 | 84 | let (k, v) = ("key", "value"); 85 | db.insert(k, v)?; 86 | db.remove(k)?; 87 | assert!(db.get::<_, String>(k)?.is_none()); 88 | 89 | Ok(()) 90 | } 91 | 92 | #[test] 93 | fn persist() -> Result<()> { 94 | let mut db = MemoryKeyValueStorage::default(); 95 | 96 | db.insert("key", "value")?; 97 | db.persist() 98 | } 99 | 100 | #[test] 101 | fn insert_values() -> Result<()> { 102 | let mut db = MemoryKeyValueStorage::default(); 103 | 104 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 105 | struct NewValue(String); 106 | 107 | let k1 = vec![1, 2, 3]; 108 | let v1 = NewValue("value".into()); 109 | 110 | let v2 = "value 2"; 111 | let k2 = vec![3, 2, 1]; 112 | 113 | db.insert(k1.clone(), v1.clone())?; 114 | assert_eq!( 115 | db.get::<_, NewValue>(k1)?.context("value for k1 is none")?, 116 | v1 117 | ); 118 | 119 | db.insert(k2.clone(), v2.clone())?; 120 | assert_eq!( 121 | db.get::<_, String>(k2)?.context("value for k2 is none")?, 122 | v2 123 | ); 124 | Ok(()) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tests" 3 | version = "0.1.0" 4 | edition = "2018" 5 | authors = [ 6 | "Furisto", 7 | "Mrunal Patel ", 8 | "Sascha Grunert ", 9 | "utam0k ", 10 | ] 11 | documentation = "https://docs.rs/containrs" 12 | homepage = "https://github.com/containers/containrs" 13 | repository = "https://github.com/containers/containrs" 14 | license = "Apache-2.0" 15 | keywords = ["runtime", "kubernetes", "cri", "container", "pod"] 16 | categories = ["network-programming", "api-bindings"] 17 | 18 | [dev-dependencies] 19 | anyhow = "1.0.66" 20 | ctor = "0.1.26" 21 | getset = "0.1.2" 22 | env_logger = "0.9.3" 23 | ipnetwork = "0.20.0" 24 | log = { version = "0.4.17", features = ["serde", "std"] } 25 | network = { path = "../crates/network" } 26 | nix = "0.25.0" 27 | prost = "0.11.2" 28 | tempfile = "3.3.0" 29 | tokio = { version = "1.21.2", features = ["full"] } 30 | tonic = "0.8.2" 31 | tower = { version = "0.4.13", features = ["util"] } 32 | uuid = { version = "1.2.2", features = ["v4"] } 33 | which = "4.3.0" 34 | -------------------------------------------------------------------------------- /tests/cni/99-loopback.conf: -------------------------------------------------------------------------------- 1 | { 2 | "cniVersion": "0.4.0", 3 | "type": "loopback" 4 | } 5 | -------------------------------------------------------------------------------- /tests/src/common.rs: -------------------------------------------------------------------------------- 1 | use crate::criapi::{ 2 | image_service_client::ImageServiceClient, runtime_service_client::RuntimeServiceClient, 3 | }; 4 | use anyhow::{bail, Context, Result}; 5 | use getset::{Getters, MutGetters}; 6 | use log::{error, info}; 7 | use std::{ 8 | convert::TryFrom, 9 | env, 10 | fs::{self, File}, 11 | io::{BufRead, BufReader}, 12 | path::{Path, PathBuf}, 13 | process::{exit, Command, Stdio}, 14 | sync::Once, 15 | time::Instant, 16 | }; 17 | use tempfile::TempDir; 18 | use tokio::net::UnixStream; 19 | use tonic::transport::{Channel, Endpoint, Uri}; 20 | use tower::service_fn; 21 | 22 | const TIMEOUT: u64 = 10; 23 | const BINARY_PATH: &str = "../target/debug/server"; 24 | 25 | static INIT: Once = Once::new(); 26 | 27 | #[cfg(test)] 28 | #[ctor::ctor] 29 | fn init() { 30 | env::set_var("RUST_LOG", "info"); 31 | env_logger::init(); 32 | 33 | info!("Ensuring latest server binary build"); 34 | let cwd = PathBuf::from("../crates/server"); 35 | if let Err(e) = Command::new("cargo").arg("build").current_dir(cwd).status() { 36 | error!("Unable to build server binary: {}", e); 37 | exit(1); 38 | } 39 | } 40 | 41 | #[derive(Getters, MutGetters)] 42 | pub struct Sut { 43 | #[get_mut = "pub"] 44 | runtime_client: RuntimeServiceClient, 45 | 46 | #[allow(dead_code)] 47 | #[get_mut = "pub"] 48 | image_client: ImageServiceClient, 49 | 50 | #[get = "pub"] 51 | test_dir: PathBuf, 52 | 53 | pid: u32, 54 | 55 | #[get_mut = "pub"] 56 | log_file_reader: BufReader, 57 | 58 | #[get = "pub"] 59 | cni_config_path: PathBuf, 60 | } 61 | 62 | impl Sut { 63 | pub async fn start() -> Result { 64 | Self::start_with_args(vec![]).await 65 | } 66 | 67 | pub async fn start_with_args(args: Vec) -> Result { 68 | INIT.call_once(|| {}); 69 | 70 | let test_dir = TempDir::new()?.into_path(); 71 | info!("Preparing test directory: {}", test_dir.display()); 72 | 73 | let log_path = test_dir.join("test.log"); 74 | let out_file = File::create(&log_path)?; 75 | let err_file = out_file.try_clone()?; 76 | 77 | // Prepare CNI directory 78 | let cni_config_path = test_dir.join("cni"); 79 | Command::new("cp") 80 | .arg("-r") 81 | .arg("cni") 82 | .arg(&cni_config_path) 83 | .output() 84 | .with_context(|| format!("copy 'cni' test dir to {}", cni_config_path.display()))?; 85 | 86 | info!("Starting server"); 87 | let sock_path = test_dir.join("test.sock"); 88 | let child = Command::new(BINARY_PATH) 89 | .arg("--log-level=trace") 90 | .arg("--log-scope=global") 91 | .arg(format!("--sock-path={}", sock_path.display())) 92 | .arg(format!( 93 | "--storage-path={}", 94 | test_dir.join("storage").display() 95 | )) 96 | .arg(format!("--cni-config-paths={}", cni_config_path.display())) 97 | .args(args) 98 | .stderr(Stdio::from(err_file)) 99 | .stdout(Stdio::from(out_file)) 100 | .spawn() 101 | .context("unable to run server")?; 102 | 103 | info!("Waiting for server to be ready"); 104 | let mut log_file_reader = 105 | BufReader::new(File::open(log_path).context("open log file path")?); 106 | if !Self::check_file_for_output( 107 | &mut log_file_reader, 108 | &test_dir.display().to_string(), 109 | Some("Unable to run server"), 110 | )? { 111 | bail!("server did not become ready") 112 | } 113 | if !Self::wait_for_file_exists(&sock_path)? { 114 | bail!("socket path {} does not exist", sock_path.display()) 115 | } 116 | info!("Server is ready"); 117 | 118 | info!("Creating runtime and image service clients"); 119 | let channel = Endpoint::try_from("http://[::]:50051")? 120 | .connect_with_connector(service_fn(move |_: Uri| { 121 | UnixStream::connect(sock_path.clone()) 122 | })) 123 | .await?; 124 | 125 | Ok(Self { 126 | runtime_client: RuntimeServiceClient::new(channel.clone()), 127 | image_client: ImageServiceClient::new(channel), 128 | test_dir, 129 | pid: child.id(), 130 | log_file_reader, 131 | cni_config_path, 132 | }) 133 | } 134 | 135 | /// Checks if the log file contains the provided line since the last call to this method. 136 | pub fn log_file_contains_line(&mut self, content: &str) -> Result { 137 | let now = Instant::now(); 138 | info!("Checking for log line"); 139 | 140 | while now.elapsed().as_secs() < 3 { 141 | for line_result in self.log_file_reader_mut().lines() { 142 | let line = line_result.context("read log line")?; 143 | info!("Got new log line: {}", line); 144 | if line.contains(content) { 145 | return Ok(true); 146 | } 147 | } 148 | } 149 | 150 | Ok(false) 151 | } 152 | 153 | pub fn cleanup(&mut self) -> Result<()> { 154 | // Stop server 155 | info!("Killing server pid {}", self.pid); 156 | Command::new("kill").arg(self.pid.to_string()).status()?; 157 | 158 | if !Self::check_file_for_output(&mut self.log_file_reader, "Server shut down", None)? { 159 | bail!("server did not shutdown correctly") 160 | } 161 | 162 | // Remove the temp dir 163 | info!("Removing test dir {}", self.test_dir().display()); 164 | fs::remove_dir_all(self.test_dir()).context("cleanup test directory")?; 165 | 166 | Ok(()) 167 | } 168 | 169 | fn check_file_for_output( 170 | file_reader: &mut BufReader, 171 | success_pattern: &str, 172 | failure_pattern: Option<&str>, 173 | ) -> Result { 174 | let mut success = false; 175 | let now = Instant::now(); 176 | 177 | while now.elapsed().as_secs() < TIMEOUT { 178 | let mut line = String::new(); 179 | file_reader.read_line(&mut line).context("read log line")?; 180 | if !line.is_empty() { 181 | print!("{}", line); 182 | if line.contains(success_pattern) { 183 | success = true; 184 | break; 185 | } 186 | if let Some(failure_pattern) = failure_pattern { 187 | if line.contains(failure_pattern) { 188 | break; 189 | } 190 | } 191 | } 192 | } 193 | return Ok(success); 194 | } 195 | 196 | fn wait_for_file_exists(file_path: &Path) -> Result { 197 | let mut success = false; 198 | let now = Instant::now(); 199 | 200 | while now.elapsed().as_secs() < TIMEOUT { 201 | if file_path.exists() { 202 | success = true; 203 | break; 204 | } 205 | } 206 | 207 | return Ok(success); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /tests/src/criapi/mod.rs: -------------------------------------------------------------------------------- 1 | //! Kubernetes Container Runtime Interface (CRI) protobuf API 2 | #![allow(missing_docs)] 3 | #![allow(clippy::all)] 4 | 5 | include!("runtime.v1alpha2.rs"); 6 | -------------------------------------------------------------------------------- /tests/src/criapi/runtime.v1alpha2.rs: -------------------------------------------------------------------------------- 1 | ../../../crates/services/src/cri/api/runtime.v1alpha2.rs -------------------------------------------------------------------------------- /tests/src/e2e/mod.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn e2e() { 3 | assert!(true) 4 | } 5 | -------------------------------------------------------------------------------- /tests/src/integration_tests/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod network; 2 | pub mod services; 3 | -------------------------------------------------------------------------------- /tests/src/integration_tests/network/cni/config.rs: -------------------------------------------------------------------------------- 1 | use crate::common::Sut; 2 | use anyhow::Result; 3 | use std::{ 4 | fs, 5 | path::{Path, PathBuf}, 6 | }; 7 | use tempfile::TempDir; 8 | 9 | const LIST: &str = r#"{ 10 | "cniVersion": "0.4.0", 11 | "name": "list", 12 | "plugins": [ 13 | { 14 | "type": "bridge", 15 | "bridge": "cni-name0", 16 | "isGateway": true, 17 | "ipMasq": true, 18 | "hairpinMode": true, 19 | "ipam": { 20 | "type": "host-local", 21 | "routes": [{ "dst": "0.0.0.0/0" }], 22 | "ranges": [[{ "subnet": "10.88.0.0/16", "gateway": "10.88.0.1" }]] 23 | } 24 | }, 25 | { "type": "portmap", "capabilities": { "portMappings": true } }, 26 | { "type": "firewall" }, 27 | { "type": "tuning" } 28 | ] 29 | }"#; 30 | 31 | const CONFIG: &str = r#"{ 32 | "cniVersion": "0.4.0", 33 | "name": "config", 34 | "type": "bridge", 35 | "bridge": "cni0", 36 | "isGateway": true, 37 | "ipMasq": true, 38 | "hairpinMode": true, 39 | "ipam": { 40 | "type": "host-local", 41 | "routes": [ 42 | { "dst": "0.0.0.0/0" }, 43 | { "dst": "1100:200::1/24" } 44 | ], 45 | "ranges": [ 46 | [{ "subnet": "10.85.0.0/16" }], 47 | [{ "subnet": "1100:200::/24" }] 48 | ] 49 | } 50 | }"#; 51 | 52 | fn add_config(path: &Path, content: &[u8]) -> Result<()> { 53 | let mut temp_path: PathBuf = path.into(); 54 | temp_path.set_extension("bak"); 55 | 56 | fs::write(temp_path.display().to_string(), content)?; 57 | fs::rename(&temp_path, path)?; 58 | Ok(()) 59 | } 60 | 61 | #[tokio::test] 62 | async fn cni_config_lifecycle_no_default_network() -> Result<()> { 63 | let mut sut = Sut::start().await?; 64 | assert!(sut.log_file_contains_line("Currently loaded 1 network: loopback")?); 65 | 66 | // New config list added 67 | let config_list = sut.cni_config_path().join("2-list.conflist"); 68 | add_config(&config_list, LIST.as_bytes())?; 69 | assert!(sut.log_file_contains_line("Currently loaded 2 networks: list, loopback")?); 70 | 71 | // New config added 72 | let config = sut.cni_config_path().join("1-config.conf"); 73 | add_config(&config, CONFIG.as_bytes())?; 74 | assert!(sut.log_file_contains_line("Currently loaded 3 networks: config, list, loopback")?); 75 | 76 | // Remove config 77 | fs::remove_file(config)?; 78 | assert!(sut.log_file_contains_line("Using list as new default network")?); 79 | 80 | // Remove list 81 | fs::remove_file(config_list)?; 82 | assert!(sut.log_file_contains_line("Using loopback as new default network")?); 83 | 84 | // Rename loopback 85 | let loopback = sut.cni_config_path().join("loopback.json"); 86 | fs::rename(sut.cni_config_path().join("99-loopback.conf"), &loopback)?; 87 | assert!(sut.log_file_contains_line("Automatically setting default network to loopback")?); 88 | 89 | // Remove loopback 90 | fs::remove_file(loopback)?; 91 | assert!(sut.log_file_contains_line("No new default network available")?); 92 | 93 | sut.cleanup() 94 | } 95 | 96 | #[tokio::test] 97 | async fn cni_config_lifecycle_with_default_network() -> Result<()> { 98 | let mut sut = Sut::start_with_args(vec!["--cni-default-network=list".into()]).await?; 99 | assert!(sut.log_file_contains_line("Using default network name: list")?); 100 | 101 | // New config list added 102 | let config_list = sut.cni_config_path().join("2-list.conflist"); 103 | add_config(&config_list, LIST.as_bytes())?; 104 | assert!(sut.log_file_contains_line("Found user selected default network list")?); 105 | 106 | // New config added 107 | let config = sut.cni_config_path().join("1-config.conf"); 108 | add_config(&config, CONFIG.as_bytes())?; 109 | assert!(sut.log_file_contains_line("Currently loaded 3 networks: config, list, loopback")?); 110 | 111 | // Remove list 112 | fs::remove_file(config_list)?; 113 | assert!(sut.log_file_contains_line("Removed default network")?); 114 | 115 | sut.cleanup() 116 | } 117 | 118 | #[tokio::test] 119 | async fn cni_config_create_dir() -> Result<()> { 120 | let empty_temp_dir = TempDir::new()?; 121 | let mut sut = Sut::start_with_args(vec![format!( 122 | "--cni-config-paths={}", 123 | empty_temp_dir.path().join("new-dir").display() 124 | )]) 125 | .await?; 126 | assert!(sut.log_file_contains_line("Currently loaded 1 network: loopback")?); 127 | 128 | sut.cleanup()?; 129 | Ok(()) 130 | } 131 | -------------------------------------------------------------------------------- /tests/src/integration_tests/network/cni/mod.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | mod port; 3 | -------------------------------------------------------------------------------- /tests/src/integration_tests/network/cni/port.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use ipnetwork::IpNetwork; 3 | use log::info; 4 | use network::cni::port::{PortManager, PortMappingBuilder}; 5 | use nix::unistd::getuid; 6 | use std::{net::SocketAddr, path::Path}; 7 | use tempfile::TempDir; 8 | use tokio::process::Command; 9 | use uuid::Uuid; 10 | 11 | #[tokio::test] 12 | async fn port_manager_ipv4() -> Result<()> { 13 | if !getuid().is_root() { 14 | info!("skipping IPv4 port manager test because not running as root"); 15 | return Ok(()); 16 | } 17 | 18 | let storage_path = TempDir::new()?; 19 | let mut port_manager = PortManager::new(storage_path.path()).await?; 20 | let id = new_id(); 21 | 22 | // Add the port 23 | port_manager 24 | .add( 25 | &id, 26 | IpNetwork::V4("127.0.0.1/8".parse()?), 27 | &[&PortMappingBuilder::default() 28 | .host("127.0.0.1:8080".parse::()?) 29 | .container_port(8000u16) 30 | .protocol("tcp") 31 | .build()?], 32 | ) 33 | .await?; 34 | 35 | // Verify 36 | let binary = which::which("iptables")?; 37 | let lines = test_iptables_std_output(&binary, &id).await?; 38 | assert_eq!(lines.len(), 5); 39 | assert!(lines 40 | .iter() 41 | .any(|x| x.contains("-m multiport --dports 8080 -j"))); 42 | assert!(lines.get(0).context("no line 0")?.contains("-N")); 43 | assert!(lines.iter().any(|x| x.contains( 44 | "-s 127.0.0.0/8 -d 127.0.0.1/32 -p tcp -m tcp --dport 8080 -j CRI-HOSTPORT-SETMARK" 45 | ))); 46 | assert!(lines.iter().any(|x| x.contains( 47 | "-s 127.0.0.1/32 -d 127.0.0.1/32 -p tcp -m tcp --dport 8080 -j CRI-HOSTPORT-SETMARK" 48 | ))); 49 | assert!(lines.iter().any(|x| x.contains( 50 | "-d 127.0.0.1/32 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 127.0.0.1:8000" 51 | ))); 52 | 53 | // Remove the port 54 | port_manager.remove(&id).await?; 55 | 56 | // Verify 57 | let lines = test_iptables_std_output(&binary, &id).await?; 58 | assert!(lines.is_empty()); 59 | 60 | Ok(()) 61 | } 62 | 63 | #[tokio::test] 64 | async fn port_manager_ipv6() -> Result<()> { 65 | if !getuid().is_root() { 66 | info!("skipping IPv6 port manager test because not running as root"); 67 | return Ok(()); 68 | } 69 | 70 | let storage_path = TempDir::new()?; 71 | let mut port_manager = PortManager::new(storage_path.path()).await?; 72 | let id = new_id(); 73 | 74 | // Add the port 75 | port_manager 76 | .add( 77 | &id, 78 | IpNetwork::V6("::1/128".parse()?), 79 | &[&PortMappingBuilder::default() 80 | .host("[::1]:30080".parse::()?) 81 | .container_port(30090u16) 82 | .protocol("udp") 83 | .build()?], 84 | ) 85 | .await?; 86 | 87 | // Verify 88 | let binary = which::which("ip6tables")?; 89 | let lines = test_iptables_std_output(&binary, &id).await?; 90 | assert!(lines.get(0).context("no line 0")?.contains("-N")); 91 | assert_eq!(lines.len(), 4); 92 | assert!(lines 93 | .iter() 94 | .any(|x| x.contains("-m multiport --dports 30080 -j"))); 95 | assert!(lines.iter().any( 96 | |x| x.contains("-s ::1/128 -d ::1/128 -p udp -m udp --dport 30080 -j CRI-HOSTPORT-SETMARK") 97 | )); 98 | assert!(lines.iter().any(|x| x 99 | .contains("-d ::1/128 -p udp -m udp --dport 30080 -j DNAT --to-destination [::1]:30090"))); 100 | 101 | // Remove the port 102 | port_manager.remove(&id).await?; 103 | 104 | // Verify 105 | let lines = test_iptables_std_output(&binary, &id).await?; 106 | assert!(lines.is_empty()); 107 | 108 | Ok(()) 109 | } 110 | 111 | fn new_id() -> String { 112 | let mut id = Uuid::new_v4().to_string(); 113 | id.truncate(21); 114 | id 115 | } 116 | 117 | async fn test_iptables_std_output(binary: &Path, id: &str) -> Result> { 118 | let output = Command::new(binary) 119 | .args(&["--wait", "-t", "nat", "-S"]) 120 | .output() 121 | .await?; 122 | assert!(output.status.success()); 123 | let stdout = String::from_utf8(output.stdout)?; 124 | 125 | assert!(stdout.contains("-N CRI-HOSTPORT-DNAT")); 126 | assert!(stdout.contains("-N CRI-HOSTPORT-MASQ")); 127 | assert!(stdout.contains("-N CRI-HOSTPORT-SETMARK")); 128 | assert!(stdout.contains("-A CRI-HOSTPORT-MASQ -m mark --mark 0x2000/0x2000 -j MASQUERADE")); 129 | assert!(stdout.contains("-A CRI-HOSTPORT-SETMARK -m comment --comment portforward-masquerade-mark -j MARK --set-xmark 0x2000/0x2000")); 130 | 131 | Ok(stdout 132 | .lines() 133 | .filter(|line| line.contains(id)) 134 | .map(ToString::to_string) 135 | .collect::>()) 136 | } 137 | -------------------------------------------------------------------------------- /tests/src/integration_tests/network/mod.rs: -------------------------------------------------------------------------------- 1 | mod cni; 2 | -------------------------------------------------------------------------------- /tests/src/integration_tests/services/cri/mod.rs: -------------------------------------------------------------------------------- 1 | mod run_pod_sandbox; 2 | mod version; 3 | -------------------------------------------------------------------------------- /tests/src/integration_tests/services/cri/run_pod_sandbox.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | common::Sut, 3 | criapi::{ 4 | LinuxPodSandboxConfig, LinuxSandboxSecurityContext, NamespaceOption, PodSandboxConfig, 5 | PodSandboxMetadata, RunPodSandboxRequest, RunPodSandboxResponse, 6 | }, 7 | }; 8 | use anyhow::Result; 9 | use std::collections::HashMap; 10 | use tonic::Request; 11 | 12 | #[tokio::test] 13 | async fn run_pod_sandbox_success() -> Result<()> { 14 | // Given 15 | let mut sut = Sut::start().await?; 16 | let test_id = "123"; 17 | let request = Request::new(RunPodSandboxRequest { 18 | config: Some(PodSandboxConfig { 19 | metadata: Some(PodSandboxMetadata { 20 | name: "".into(), 21 | uid: test_id.into(), 22 | namespace: "".into(), 23 | attempt: 0, 24 | }), 25 | hostname: "".into(), 26 | log_directory: "".into(), 27 | dns_config: None, 28 | port_mappings: vec![], 29 | labels: HashMap::new(), 30 | annotations: HashMap::new(), 31 | linux: Some(LinuxPodSandboxConfig { 32 | cgroup_parent: String::from("/path/to/cgroup"), 33 | sysctls: HashMap::new(), 34 | security_context: Some(LinuxSandboxSecurityContext { 35 | namespace_options: Some(NamespaceOption { 36 | network: 0, 37 | pid: 1, 38 | ipc: 0, 39 | target_id: String::from("container_id"), 40 | }), 41 | selinux_options: None, 42 | run_as_user: None, 43 | run_as_group: None, 44 | readonly_rootfs: false, 45 | supplemental_groups: Vec::new(), 46 | privileged: false, 47 | seccomp_profile_path: String::from("/path/to/seccomp"), 48 | }), 49 | }), 50 | }), 51 | runtime_handler: "".into(), 52 | }); 53 | 54 | // When 55 | let response = sut 56 | .runtime_client_mut() 57 | .run_pod_sandbox(request) 58 | .await? 59 | .into_inner(); 60 | 61 | // Then 62 | assert_eq!( 63 | response, 64 | RunPodSandboxResponse { 65 | pod_sandbox_id: test_id.into() 66 | } 67 | ); 68 | 69 | sut.cleanup() 70 | } 71 | -------------------------------------------------------------------------------- /tests/src/integration_tests/services/cri/version.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | common::Sut, 3 | criapi::{VersionRequest, VersionResponse}, 4 | }; 5 | use anyhow::Result; 6 | use tonic::Request; 7 | 8 | #[tokio::test] 9 | async fn version_success() -> Result<()> { 10 | // Given 11 | let mut sut = Sut::start().await?; 12 | let request = Request::new(VersionRequest { 13 | version: "0.1.0".into(), 14 | }); 15 | 16 | // When 17 | let response = sut 18 | .runtime_client_mut() 19 | .version(request) 20 | .await? 21 | .into_inner(); 22 | 23 | // Then 24 | assert_eq!( 25 | response, 26 | VersionResponse { 27 | version: "0.1.0".into(), 28 | runtime_api_version: "v1alpha1".into(), 29 | runtime_name: "crust".into(), 30 | runtime_version: "0.0.1".into(), 31 | } 32 | ); 33 | 34 | sut.cleanup() 35 | } 36 | -------------------------------------------------------------------------------- /tests/src/integration_tests/services/mod.rs: -------------------------------------------------------------------------------- 1 | mod cri; 2 | -------------------------------------------------------------------------------- /tests/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! All integration test suites 2 | #![cfg(test)] 3 | 4 | mod common; 5 | mod criapi; 6 | mod e2e; 7 | mod integration_tests; 8 | --------------------------------------------------------------------------------