├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── publish_crates.yml ├── .gitignore ├── CODE-OF-CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── Makefile ├── README.md ├── rustfmt.toml ├── scripts ├── publish-crates.sh └── publish-list └── src ├── k8-client ├── Cargo.toml ├── Makefile ├── README.md ├── k8-fixtures │ ├── Cargo.toml │ ├── data │ │ └── topic.json │ └── src │ │ ├── lib.rs │ │ └── test_fixtures.rs ├── k8-test │ ├── Dockerfile │ ├── build.sh │ ├── hello-deploy.yaml │ ├── role-binding.yaml │ ├── role.yaml │ └── service.yaml ├── src │ ├── cert.rs │ ├── client │ │ ├── client_impl.rs │ │ ├── config_native.rs │ │ ├── config_openssl.rs │ │ ├── config_rustls.rs │ │ ├── list_stream.rs │ │ ├── log_stream.rs │ │ ├── memory.rs │ │ ├── mod.rs │ │ └── wstream.rs │ ├── fixture.rs │ ├── lib.rs │ └── uri.rs └── tests │ ├── canary.rs │ ├── error.rs │ ├── job.rs │ ├── replace.rs │ └── service.rs ├── k8-config ├── Cargo.toml ├── LICENSE-APACHE ├── README.md ├── data │ └── k8config.yaml ├── examples │ └── kubeconfig_read.rs └── src │ ├── config.rs │ ├── context.rs │ ├── error.rs │ ├── lib.rs │ └── pod.rs ├── k8-ctx-util ├── Cargo.toml └── src │ └── main.rs ├── k8-diff ├── Cargo.toml ├── LICENSE-APACHE ├── README.md ├── k8-dderive │ ├── Cargo.toml │ └── src │ │ ├── diff.rs │ │ └── lib.rs └── src │ ├── json │ ├── diff.rs │ ├── mod.rs │ └── se.rs │ └── lib.rs ├── k8-metadata-client ├── Cargo.toml ├── LICENSE-APACHE ├── README.md └── src │ ├── client.rs │ ├── diff.rs │ ├── lib.rs │ └── nothing.rs └── k8-types ├── Cargo.toml ├── LICENSE-APACHE ├── README.md └── src ├── app ├── deployment.rs ├── mod.rs └── stateful.rs ├── batch ├── job.rs └── mod.rs ├── core ├── config_map.rs ├── mod.rs ├── namespace.rs ├── node.rs ├── plugin.rs ├── pod.rs ├── secret.rs ├── service.rs └── service_account.rs ├── crd.rs ├── int_or_string.rs ├── lib.rs ├── metadata.rs ├── options.rs ├── storage ├── mod.rs └── storage_class.rs └── store.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | # Check for updates to GitHub Actions every weekday 8 | interval: "daily" 9 | 10 | # Maintain dependencies for Cargo 11 | - package-ecosystem: "cargo" 12 | directory: "/" 13 | schedule: 14 | interval: "daily" 15 | ignore: 16 | - dependency-name: "*" 17 | update-types: [ 18 | "version-update:semver-patch", 19 | ] 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | merge_group: 5 | pull_request: 6 | branches: [master] 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | K3D_VERSION: v5.4.3 11 | 12 | concurrency: 13 | group: ci-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | unit_test: 18 | name: Unit test 19 | runs-on: ${{ matrix.os }} 20 | strategy: 21 | matrix: 22 | os: [ubuntu-latest, macOS-latest] 23 | rust: [stable] 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | - name: Install ${{ matrix.rust }} 28 | uses: actions-rs/toolchain@v1 29 | with: 30 | toolchain: ${{ matrix.rust }} 31 | override: true 32 | - name: Run unit tests 33 | run: cargo test --lib --all-features 34 | 35 | unit_test_k8_client_feature_flags: 36 | name: Unit test feature flags 37 | runs-on: ${{ matrix.os }} 38 | strategy: 39 | matrix: 40 | os: [ubuntu-latest, macOS-latest] 41 | rust: [stable] 42 | features: 43 | ["openssl_tls,k8", "openssl_tls", "native_tls,k8", "native_tls", "rust_tls", "openssl_tls,memory_client"] 44 | 45 | steps: 46 | - uses: actions/checkout@v4 47 | - name: Install ${{ matrix.rust }} 48 | uses: actions-rs/toolchain@v1 49 | with: 50 | toolchain: ${{ matrix.rust }} 51 | override: true 52 | - name: Run unit tests 53 | run: cargo test --lib -p k8-client --no-default-features --features ${{ matrix.features }} 54 | 55 | check_fmt: 56 | name: check cargo fmt 57 | runs-on: ${{ matrix.os }} 58 | strategy: 59 | matrix: 60 | os: [ubuntu-latest] 61 | rust: [stable] 62 | steps: 63 | - uses: actions/checkout@v4 64 | - name: Install ${{ matrix.rust }} 65 | uses: actions-rs/toolchain@v1 66 | with: 67 | toolchain: ${{ matrix.rust }} 68 | override: true 69 | - name: fmt 70 | run: make check-fmt 71 | 72 | check_clippy: 73 | name: clippy check 74 | runs-on: ${{ matrix.os }} 75 | strategy: 76 | matrix: 77 | os: [ubuntu-latest] 78 | rust: [stable] 79 | steps: 80 | - uses: actions/checkout@v4 81 | - name: Install ${{ matrix.rust }} 82 | uses: actions-rs/toolchain@v1 83 | with: 84 | toolchain: ${{ matrix.rust }} 85 | override: true 86 | - uses: Swatinem/rust-cache@v2 87 | with: 88 | key: ${{ matrix.os }} 89 | - name: clippy 90 | run: make check-clippy 91 | 92 | k8_integration_test: 93 | name: Kubernetes integration test 94 | runs-on: ${{ matrix.os }} 95 | strategy: 96 | matrix: 97 | os: [ubuntu-latest] 98 | k8: [minikube, k3d, kind] 99 | rust: [stable] 100 | 101 | steps: 102 | - uses: actions/checkout@v4 103 | - name: Install ${{ matrix.rust }} 104 | uses: actions-rs/toolchain@v1 105 | with: 106 | toolchain: ${{ matrix.rust }} 107 | override: true 108 | - uses: Swatinem/rust-cache@v2 109 | with: 110 | key: ${{ matrix.os }}-${{ matrix.k8 }} 111 | - name: Install Minikube for Github runner 112 | if: startsWith(matrix.k8,'minikube') 113 | uses: manusa/actions-setup-minikube@v2.14.0 114 | with: 115 | minikube version: "v1.33.1" 116 | kubernetes version: "v1.30.2" 117 | github token: ${{ secrets.GITHUB_TOKEN }} 118 | driver: docker 119 | - name: Install k3d 120 | if: startsWith(matrix.k8,'k3d') 121 | run: | 122 | curl -s https://raw.githubusercontent.com/rancher/k3d/main/install.sh | TAG=${{ env.K3D_VERSION }} bash 123 | k3d cluster create fluvio-k3d --image rancher/k3s:v1.30.2-k3s2-amd64 124 | - name: Install Kind 125 | if: startsWith(matrix.k8,'kind') 126 | run: | 127 | curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.23.0/kind-linux-amd64 128 | chmod +x ./kind 129 | ./kind create cluster 130 | - name: Test K8 Installation 131 | run: | 132 | kubectl get nodes 133 | kubectl config view 134 | - name: Build test 135 | run: cargo build --tests --all-features 136 | - name: K8 client integration test native 137 | run: make k8-client-integration-test-native 138 | timeout-minutes: 2 139 | 140 | done: 141 | name: Done 142 | needs: 143 | - unit_test 144 | - check_fmt 145 | - check_clippy 146 | - k8_integration_test 147 | - unit_test_k8_client_feature_flags 148 | runs-on: ubuntu-latest 149 | steps: 150 | - name: Dump needs context 151 | env: 152 | CONTEXT: ${{ toJson(needs) }} 153 | run: | 154 | echo -e "\033[33;1;4mDump context\033[0m" 155 | echo -e "$CONTEXT\n" 156 | - name: Report failure on cancellation 157 | if: ${{ contains(needs.*.result, 'cancelled') || cancelled() }} 158 | run: exit 1 159 | - name: Failing test and build 160 | if: ${{ contains(needs.*.result, 'failure') }} 161 | run: exit 1 162 | - name: Don't allow skipped 163 | if: ${{ contains(needs.*.result, 'skipped') && github.event_name == 'merge_group' }} 164 | run: exit 1 165 | - name: Successful test and build 166 | if: ${{ !(contains(needs.*.result, 'failure')) }} 167 | run: exit 0 168 | - name: Done 169 | run: echo "Done!" 170 | -------------------------------------------------------------------------------- /.github/workflows/publish_crates.yml: -------------------------------------------------------------------------------- 1 | name: Publish crates to crates.io 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | workflow_dispatch: 8 | inputs: 9 | commit: 10 | required: false 11 | type: string 12 | description: 'Fluvio git commit override (latest `master` by default)' 13 | default: '' 14 | workflow_call: 15 | inputs: 16 | commit: 17 | required: false 18 | type: string 19 | description: 'Fluvio git commit override (latest `master` by default)' 20 | default: '' 21 | 22 | jobs: 23 | publish_crates: 24 | name: Publish crates to crates.io 25 | strategy: 26 | matrix: 27 | rust: [stable] 28 | runs-on: ubuntu-latest 29 | #permissions: write-all 30 | steps: 31 | - name: Install Rust ${{ matrix.rust }} toolchain 32 | uses: dtolnay/rust-toolchain@master 33 | with: 34 | toolchain: ${{ matrix.rust }} 35 | 36 | - uses: actions/checkout@v4 37 | with: 38 | ref: ${{ github.event.inputs.commit }} 39 | 40 | - name: Run publish script 41 | env: 42 | VERBOSE: true 43 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 44 | run: | 45 | ./scripts/publish-crates.sh 46 | 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | **/*.rs.bk 3 | .DS_Store 4 | .docker-cargo 5 | target-docker 6 | .vscode/tasks.json 7 | .vscode/settings.json 8 | .idea/ 9 | Cargo.lock 10 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 7 | 8 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 9 | 10 | ## Our Standards 11 | 12 | Examples of behavior that contributes to a positive environment for our community include: 13 | 14 | * Demonstrating empathy and kindness toward other people 15 | * Being respectful of differing opinions, viewpoints, and experiences 16 | * Giving and gracefully accepting constructive feedback 17 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 18 | * Focusing on what is best not just for us as individuals, but for the 19 | overall community 20 | 21 | Examples of unacceptable behavior include: 22 | 23 | * The use of sexualized language or imagery, and sexual attention or advances of any kind 24 | * Trolling, insulting or derogatory comments, and personal or political attacks 25 | * Public or private harassment 26 | * Publishing others' private information, such as a physical or email address, without their explicit permission 27 | * Other conduct which could reasonably be considered inappropriate in a professional setting 28 | 29 | ## Enforcement Responsibilities 30 | 31 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 32 | 33 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 34 | 35 | ## Scope 36 | 37 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address,posting via an official social media account, or acting as an appointed representative at an online or offline event. 38 | 39 | ## Enforcement 40 | 41 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at fluvio-lead@infinyon.com. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. 42 | 43 | 44 | ## Attribution 45 | 46 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 47 | 48 | For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to k8 API 2 | 3 | Thank you for contributing. No matter how large or small, contributions are always welcome. Before contributing, please read [Code of Conduct](CODE-OF-CONDUCT.md) 4 | 5 | #### Table Of Contents 6 | 7 | [Assumptions](#assumptions) 8 | 9 | [Ask a Question](#ask-a-question) 10 | 11 | [Getting Started](#getting-started) 12 | 13 | [Contributing](#contributing) 14 | 15 | ## Assumptions 16 | Familiarity with 17 | - [Rust](https://www.rust-lang.org) 18 | - [Kubernetes](https://kubernetes.io) 19 | - [Fluvio](https://www.fluvio.io/docs/) 20 | 21 | Currently, k8 API supports the following platforms: 22 | - macOS X 23 | - Linux 24 | - Windows support will be provided in future 25 | 26 | ## Ask a Question 27 | 28 | Please open an Issue on GitHub with the label `question`. 29 | 30 | ## Getting Started 31 | 32 | This is similar to [Kubernetes Go Client](https://github.com/kubernetes/client-go) 33 | 34 | Follow our [Installation Guide](https://github.com/infinyon/fluvio/blob/master/doc/INSTALL.md) to get fluvio up and running. 35 | 36 | To learn about the Fluvio Architecture, please visit the [Developer](https://github.com/infinyon/fluvio/blob/master/DEVELOPER.md) section. 37 | 38 | ## Contributing 39 | 40 | ### Report a Bug 41 | 42 | To report a bug, open an issue on GitHub with the label `bug`. Please ensure the issue has not already been reported. 43 | 44 | ### Suggest an Enhancement 45 | 46 | To suggest an enhancement, please create an issue on GitHub with the label `enhancement`. 47 | 48 | ### Creating pull request 49 | 50 | - Fork the `k8-api` repository to your GitHub Account. 51 | 52 | - Create a branch, submit a PR when your changes are tested and ready for review 53 | 54 | If you’d like to implement a new feature, please consider creating a `feature request` issue first to start a discussion about the feature. 55 | 56 | ### License 57 | 58 | This project is licensed under the [Apache license](LICENSE). Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in Fluvio by you, shall be licensed as Apache, without any additional terms or conditions. 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "src/k8-types", 4 | "src/k8-metadata-client", 5 | "src/k8-diff", 6 | "src/k8-config", 7 | "src/k8-client", 8 | "src/k8-ctx-util" 9 | ] 10 | resolver = "2" 11 | 12 | [workspace.dependencies] 13 | anyhow = "1.0" 14 | fluvio-future = "0.7.0" 15 | serde_yaml = { version = "0.9.0", default-features = false } 16 | serde_qs = "0.13.0" 17 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 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 | publish: 2 | cargo-publish-all 3 | 4 | 5 | k8-client-build: 6 | make -C src/k8-client build 7 | 8 | k8-client-integration-test-native: 9 | make -C src/k8-client run-integration-test-native 10 | 11 | 12 | k8-client-integration-test-rustls: 13 | cargo run --bin k8-ctx-util 14 | make -C src/k8-client run-integration-test-rustls 15 | 16 | check-fmt: 17 | rustup component add rustfmt 18 | cargo fmt -- --check 19 | 20 | 21 | check-clippy: 22 | rustup component add clippy 23 | cargo clippy --all-features --tests -- -D warnings -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![CI](https://github.com/infinyon/k8-api/workflows/CI/badge.svg) 2 | 3 |

Rust K8 API

4 |
5 | 6 | Rust binding for Kubernetes API 7 | 8 |
9 | 10 | ## Features 11 | 12 | - __Async:__ Use Async Rust 13 | - __Stream:__ Support Streaming 14 | - __Diff:__ Diff support 15 | - __CRD:__ Support CRD 16 | - __Generic:__ Use Rust Generics 17 | 18 | Following OS are supported: 19 | * Linux 20 | * MacOs 21 | 22 | Windows support will be coming in future 23 | 24 | ## Contributing 25 | 26 | If you'd like to contribute to the project, please read our [Contributing guide](CONTRIBUTING.md). 27 | 28 | ## License 29 | 30 | This project is licensed under the [Apache license](LICENSE-APACHE). 31 | 32 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | reorder_modules = false 2 | reorder_imports = false -------------------------------------------------------------------------------- /scripts/publish-crates.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | # Read in PUBLISH_CRATES var 6 | source $(dirname -- ${BASH_SOURCE[0]})/publish-list 7 | 8 | LOOP_AGAIN=false 9 | ATTEMPTS=1 10 | CRATES_UPLOADED=0 11 | readonly MAX_ATTEMPTS=3 12 | readonly VERBOSE=${VERBOSE:-false} 13 | 14 | readonly CARGO_OUTPUT_TMP=$(mktemp) 15 | 16 | function check_if_crate_uploaded() { 17 | 18 | local CRATE=$1; 19 | 20 | # Check for whether the crate was already uploaded to determine if we're good to move forward 21 | tail -1 "$CARGO_OUTPUT_TMP" | grep "already uploaded" > /dev/null 22 | 23 | # If exit code from `grep` is 0 24 | if [[ ${PIPESTATUS[1]} != 0 ]]; 25 | then 26 | echo "$CRATE upload check failed. Will try again" 27 | LOOP_AGAIN=true; 28 | fi 29 | } 30 | 31 | function cargo_publish_all() { 32 | # We want to loop though each of the crates and attempt to publish. 33 | 34 | if [[ $ATTEMPTS -lt $((MAX_ATTEMPTS + 1)) ]]; 35 | then 36 | for crate in "${PUBLISH_CRATES[@]}" ; do 37 | echo "$crate"; 38 | 39 | # Save the `cargo publish` in case we get a non-zero exit 40 | cargo publish -p $crate 2>&1 | tee "$CARGO_OUTPUT_TMP" 41 | result="${PIPESTATUS[0]}"; 42 | 43 | # cargo publish exit codes: 44 | if [[ "$result" != 0 ]]; 45 | then 46 | check_if_crate_uploaded "$crate"; 47 | else 48 | CRATES_UPLOADED=$((CRATES_UPLOADED+1)); 49 | fi 50 | done 51 | else 52 | echo "❌ Max number of publish attempts reached" 53 | echo "❌ Max attempts: $MAX_ATTEMPTS" 54 | exit 1 55 | fi 56 | } 57 | 58 | function main() { 59 | 60 | if [[ $VERBOSE != false ]]; 61 | then 62 | echo "VERBOSE MODE ON" 63 | set -x 64 | fi 65 | 66 | while cargo_publish_all; [[ $LOOP_AGAIN = true ]]; 67 | do 68 | # Reset the loop flag 69 | LOOP_AGAIN=false; 70 | # Increment the attempts counter 71 | ATTEMPTS=$((ATTEMPTS+1)); 72 | done 73 | 74 | echo "✅ Publish success after # Attempts: $ATTEMPTS" 75 | echo "✅ Crates uploaded: $CRATES_UPLOADED" 76 | } 77 | 78 | main; -------------------------------------------------------------------------------- /scripts/publish-list: -------------------------------------------------------------------------------- 1 | # This list is approximately sorted for dependency resolution 2 | # Used by publish crate and crate version check scripts 3 | 4 | PUBLISH_CRATES=( 5 | k8-config 6 | k8-diff 7 | k8-types 8 | k8-metadata-client 9 | k8-client 10 | ) -------------------------------------------------------------------------------- /src/k8-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "k8-client" 4 | version = "13.1.1" 5 | authors = ["Fluvio Contributors "] 6 | description = "Core Kubernetes metadata traits" 7 | repository = "https://github.com/infinyon/k8-api" 8 | license = "Apache-2.0" 9 | categories = ["api-bindings", "asynchronous", "encoding", "network-programming"] 10 | readme = "README.md" 11 | 12 | 13 | [features] 14 | default = ["openssl_tls"] 15 | k8 = [] 16 | memory_client = ["async-channel", "async-lock", "serde_yaml"] 17 | openssl_tls = ["fluvio-future/openssl_tls"] 18 | native_tls = ["fluvio-future/native_tls"] 19 | rust_tls = ["rustls", "fluvio-future/rust_tls"] 20 | 21 | [dependencies] 22 | anyhow = { workspace = true } 23 | async-channel = { version = "2.3.1", optional = true } 24 | async-lock = { version = "3.3.0", optional = true } 25 | cfg-if = "1.0" 26 | tracing = "0.1.19" 27 | bytes = "1.7.1" 28 | base64 = { version = "0.22.1" } 29 | futures-util = { version = "0.3.21", features = ["io"] } 30 | rand = { version = "0.8.3" } 31 | rustls = { version = "0.23.11", optional = true } 32 | hyper = { version = "0.14.28", features = ["client", "http1", "http2", "stream"] } 33 | http = { version = "0.2.9" } 34 | tokio = { version = "1.37.0" } 35 | pin-utils = "0.1.0" 36 | serde = { version = "1.0.136", features = ['derive'] } 37 | serde_json = "1.0.40" 38 | serde_qs = { workspace = true } 39 | serde_yaml = { workspace = true, optional = true } 40 | async-trait = "0.1.52" 41 | fluvio-future = { workspace = true, features = ["net", "task"] } 42 | k8-metadata-client = { version = "7.0.0", path = "../k8-metadata-client" } 43 | k8-diff = { version = "0.1.0", path = "../k8-diff" } 44 | k8-config = { version = "2.3.0", path = "../k8-config" } 45 | k8-types = { version = "0.8.6", path = "../k8-types", features = ["core", "batch"] } 46 | 47 | [dev-dependencies] 48 | rand = "0.8.3" 49 | once_cell = "1.19.0" 50 | async-trait = "0.1.52" 51 | 52 | fluvio-future = { workspace = true, features = ["fixture", "timer"] } 53 | -------------------------------------------------------------------------------- /src/k8-client/Makefile: -------------------------------------------------------------------------------- 1 | run-integration-test-native: 2 | cargo test test_pods --features k8,native_tls 3 | cargo test test_object_conflict --features k8,native_tls 4 | cargo test test_object_replace --features k8,native_tls 5 | cargo test test_job_created --features k8,native_tls 6 | cargo test test_service_changes --features k8,native_tls 7 | 8 | -------------------------------------------------------------------------------- /src/k8-client/README.md: -------------------------------------------------------------------------------- 1 | # Kubernetes Rust Client 2 | 3 | 4 | This is similar to Kubernetes Go Client: https://github.com/kubernetes/client-go 5 | 6 | Example of using the client: 7 | 8 | ``` 9 | use k8_client::K8Client; 10 | use k8_obj_core::pod::{PodSpec,PodStatus}; 11 | 12 | async fn main() { 13 | 14 | let client = K8Client::default().expect("cluster not initialized"); 15 | 16 | let pod_items = client.retrieve_items::("default").await.expect("pods should exist"); 17 | 18 | for pod in pod_items.items { 19 | println!("pod: {:#?}",pod); 20 | } 21 | } 22 | 23 | ``` 24 | 25 | 26 | ## License 27 | 28 | This project is licensed under the [Apache license](LICENSE-APACHE). 29 | 30 | ### Contribution 31 | 32 | Unless you explicitly state otherwise, any contribution intentionally submitted 33 | for inclusion in Fluvio by you, shall be licensed as Apache, without any additional 34 | terms or conditions. 35 | -------------------------------------------------------------------------------- /src/k8-client/k8-fixtures/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "k8-fixtures" 3 | edition = "2021" 4 | version = "0.2.2" 5 | authors = ["fluvio.io"] 6 | 7 | [dependencies] 8 | rand = "0.8.3" 9 | serde = "1.0.76" 10 | serde_derive = "1.0.76" 11 | serde_json = "1.0.27" 12 | k8-client = { path = "../../k8-client"} 13 | k8-metadata = { path = "../../k8-metadata"} 14 | 15 | -------------------------------------------------------------------------------- /src/k8-client/k8-fixtures/data/topic.json: -------------------------------------------------------------------------------- 1 | "{\"apiVersion\":\"fluvio.infinyon.com/v1\",\"items\": 2 | [{\"apiVersion\":\"fluvio.infinyon.com/v1\",\"kind\":\"Topic\",\"metadata\":{\"annotations\":{\"kubectl.kubernetes.io/last-applied-configuration\":\"{\\\"apiVersion\\\":\\\"fluvio.infinyon.com/v1\\\",\\\"kind\\\":\\\"Topic\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"name\\\":\\\"topic1\\\",\\\"namespace\\\":\\\"test\\\"},\\\"spec\\\":{\\\"partitions\\\":1,\\\"replicationFactor\\\":1}}\\n\"},\"creationTimestamp\":\"2019-12-01T07:10:50Z\",\"generation\":1,\"name\":\"topic1\",\"namespace\":\"test\",\"resourceVersion\":\"67676\",\"selfLink\":\"/apis/fluvio.infinyon.com/v1/namespaces/test/topics/topic1\",\"uid\":\"b4763b22-1409-11ea-a548-267327c1c713\"},\"spec\":{\"partitions\":1,\"replicationFactor\":1},\"status\":{\"reason\":\"need 1 more SPU\",\"replicaMap\":{},\"resolution\":\"InsufficientResources\"}}],\"kind\":\"TopicList\",\"metadata\":{\"continue\":\"\", 3 | \"resourceVersion\":\"67961\",\"selfLink\":\"/apis/fluvio.infinyon.com/v1/namespaces/test/topics\"}}\n" -------------------------------------------------------------------------------- /src/k8-client/k8-fixtures/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod test_fixtures; 2 | 3 | pub use self::test_fixtures::create_topic_stream_result; 4 | pub use self::test_fixtures::TestTopicWatch; 5 | pub use self::test_fixtures::TestTopicWatchList; 6 | -------------------------------------------------------------------------------- /src/k8-client/k8-fixtures/src/test_fixtures.rs: -------------------------------------------------------------------------------- 1 | use rand::prelude::*; 2 | use std::env; 3 | use std::ffi::OsStr; 4 | use std::fs::File; 5 | use std::io::Read; 6 | use std::path::{Path, PathBuf}; 7 | 8 | use k8_metadata_core::metadata::K8Watch; 9 | use k8_metadata::client::TokenStreamResult; 10 | use k8_metadata::topic::{TopicSpec, TopicStatus}; 11 | 12 | // 13 | // Topic Watch Fixtures 14 | // 15 | 16 | pub type TestTopicWatchList = Vec; 17 | 18 | pub struct TestTopicWatch { 19 | pub operation: String, 20 | pub name: String, 21 | pub partitions: i32, 22 | pub replication: i32, 23 | pub ignore_rack_assignment: Option, 24 | } 25 | 26 | pub fn create_topic_watch(ttw: &TestTopicWatch) -> K8Watch { 27 | let target_dir = get_target_dir(); 28 | let path = get_top_dir(&target_dir); 29 | let mut contents = String::new(); 30 | let (filename, file_has_options) = if ttw.ignore_rack_assignment.is_none() { 31 | ( 32 | String::from("k8-client/k8-fixtures/data/topic_no_options.tmpl"), 33 | false, 34 | ) 35 | } else { 36 | ( 37 | String::from("k8-client/k8-fixtures/data/topic_all.tmpl"), 38 | true, 39 | ) 40 | }; 41 | let f = File::open(path.join(filename)); 42 | f.unwrap().read_to_string(&mut contents).unwrap(); 43 | 44 | contents = contents.replace("{type}", &*ttw.operation); 45 | contents = contents.replace("{name}", &*ttw.name); 46 | contents = contents.replace("{partitions}", &*ttw.partitions.to_string()); 47 | contents = contents.replace("{replication}", &*ttw.replication.to_string()); 48 | contents = contents.replace( 49 | "{12_digit_rand}", 50 | &*format!("{:012}", thread_rng().gen_range(0, 999999)), 51 | ); 52 | if file_has_options { 53 | contents = contents.replace( 54 | "{rack_assignment}", 55 | &*ttw.ignore_rack_assignment.unwrap().to_string(), 56 | ); 57 | } 58 | serde_json::from_str(&contents).unwrap() 59 | } 60 | 61 | pub fn create_topic_stream_result( 62 | ttw_list: &TestTopicWatchList, 63 | ) -> TokenStreamResult { 64 | let mut topic_watch_list = vec![]; 65 | for ttw in ttw_list { 66 | topic_watch_list.push(Ok(create_topic_watch(&ttw))); 67 | } 68 | Ok(topic_watch_list) 69 | } 70 | 71 | // 72 | // Utility APIs 73 | // 74 | 75 | // Get absolute path to the "target" directory ("build" dir) 76 | fn get_target_dir() -> PathBuf { 77 | let bin = env::current_exe().expect("exe path"); 78 | let mut target_dir = PathBuf::from(bin.parent().expect("bin parent")); 79 | while target_dir.file_name() != Some(OsStr::new("target")) { 80 | target_dir.pop(); 81 | } 82 | target_dir 83 | } 84 | 85 | // Get absolute path to the project's top dir, given target dir 86 | fn get_top_dir<'a>(target_dir: &'a Path) -> &'a Path { 87 | target_dir.parent().expect("target parent") 88 | } 89 | -------------------------------------------------------------------------------- /src/k8-client/k8-test/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.12 2 | COPY hello hello 3 | -------------------------------------------------------------------------------- /src/k8-client/k8-test/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | if [ -n "$MINIKUBE_DOCKER_ENV" ]; then 5 | eval $(minikube -p minikube docker-env) 6 | fi 7 | 8 | tmp_dir=$(mktemp -d -t fluvio-docker-image-XXXXXX) 9 | echo "tmp_dir: ${tmp_dir}" 10 | cp ../../target/x86_64-unknown-linux-musl/$CARGO_PROFILE/hello $tmp_dir/ 11 | cp $(dirname $0)/Dockerfile $tmp_dir/Dockerfile 12 | cd $tmp_dir 13 | docker build -t k8-hello:$DOCKER_TAG . 14 | rm -rf $tmp_dir 15 | -------------------------------------------------------------------------------- /src/k8-client/k8-test/hello-deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: k8-test 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: k8-test 10 | template: 11 | metadata: 12 | labels: 13 | app: k8-test 14 | spec: 15 | serviceAccountName: hello 16 | containers: 17 | - name: hello 18 | image: k8-hello:latest 19 | imagePullPolicy: IfNotPresent 20 | env: 21 | - name: RUST_LOG 22 | value: debug 23 | command: ["/hello"] 24 | strategy: 25 | type: RollingUpdate 26 | rollingUpdate: 27 | maxUnavailable: 1 28 | maxSurge: 25% 29 | -------------------------------------------------------------------------------- /src/k8-client/k8-test/role-binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: hello 5 | subjects: 6 | - kind: ServiceAccount 7 | name: hello 8 | roleRef: 9 | kind: Role 10 | name: hello 11 | apiGroup: rbac.authorization.k8s.io -------------------------------------------------------------------------------- /src/k8-client/k8-test/role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 3 | metadata: 4 | name: hello 5 | rules: 6 | - apiGroups: [""] # "" indicates the core API group 7 | resources: 8 | - pods 9 | - services 10 | - statefulsets.apps 11 | - persistentvolumeclaims 12 | - persistentvolumes 13 | - replicasets 14 | - deployments 15 | verbs: ["*"] -------------------------------------------------------------------------------- /src/k8-client/k8-test/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: hello -------------------------------------------------------------------------------- /src/k8-client/src/cert.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use anyhow::anyhow; 4 | use anyhow::Context; 5 | use anyhow::Result; 6 | use tracing::debug; 7 | 8 | use k8_config::K8Config; 9 | use k8_config::KubeConfig; 10 | use k8_config::PodConfig; 11 | use k8_config::AuthProviderDetail; 12 | 13 | pub trait ConfigBuilder: Sized { 14 | type Client; 15 | 16 | fn new() -> Self; 17 | 18 | fn build(self) -> Result; 19 | 20 | fn load_ca_certificate(self, ca_path: impl AsRef) -> Result; 21 | 22 | // load from ca data 23 | fn load_ca_cert_with_data(self, data: Vec) -> Result; 24 | 25 | // load client certificate (crt) and private key 26 | fn load_client_certificate>( 27 | self, 28 | client_crt_path: P, 29 | client_key_path: P, 30 | ) -> Result; 31 | 32 | fn load_client_certificate_with_data( 33 | self, 34 | client_crt: Vec, 35 | client_key: Vec, 36 | ) -> Result; 37 | } 38 | 39 | /// Build Client 40 | #[derive(Debug)] 41 | pub struct ClientConfigBuilder { 42 | config: K8Config, 43 | builder: B, 44 | external_token: Option, 45 | } 46 | 47 | impl ClientConfigBuilder 48 | where 49 | B: ConfigBuilder, 50 | { 51 | pub fn new(config: K8Config) -> Result { 52 | let (builder, external_token) = Self::config(&config)?; 53 | 54 | Ok(Self { 55 | config, 56 | builder, 57 | external_token, 58 | }) 59 | } 60 | 61 | /// configure based con k8 config 62 | fn config(config: &K8Config) -> Result<(B, Option)> { 63 | let builder = B::new(); 64 | match config { 65 | K8Config::Pod(pod_config) => { 66 | Ok((Self::configure_in_cluster(builder, pod_config)?, None)) 67 | } 68 | K8Config::KubeConfig(kube_config) => { 69 | Self::configure_out_of_cluster(builder, &kube_config.config) 70 | } 71 | } 72 | } 73 | 74 | pub fn k8_config(&self) -> &K8Config { 75 | &self.config 76 | } 77 | 78 | pub fn token(&self) -> Result> { 79 | if let Some(token) = &self.external_token { 80 | Ok(Some(token.clone())) 81 | } else if let K8Config::KubeConfig(context) = &self.k8_config() { 82 | // We should be able to know if we use dynamic tokens from the User config if using `auth_provider` 83 | 84 | let kube_config = &context.config; 85 | 86 | let current_context = kube_config.current_context.clone(); 87 | 88 | if let Some(c) = &kube_config 89 | .contexts 90 | .iter() 91 | .find(|context| context.name == current_context) 92 | { 93 | let users = &kube_config.users; 94 | 95 | let token = if let Some(u) = users.iter().find(|user| user.name == c.context.user) { 96 | if let Some(auth_provider) = &u.user.auth_provider { 97 | auth_provider.token()? 98 | } else { 99 | None 100 | } 101 | } else { 102 | None 103 | }; 104 | 105 | Ok(token) 106 | } else { 107 | Ok(None) 108 | } 109 | } else { 110 | match self.k8_config() { 111 | K8Config::Pod(pod) => Ok(Some(pod.token.to_owned())), 112 | _ => Ok(None), 113 | } 114 | } 115 | } 116 | 117 | pub fn host(&self) -> String { 118 | self.k8_config().api_path().to_owned() 119 | } 120 | 121 | pub fn build(self) -> Result { 122 | self.builder.build() 123 | } 124 | 125 | fn configure_in_cluster(builder: B, pod: &PodConfig) -> Result { 126 | debug!("configure as pod in cluster"); 127 | let path = pod.ca_path(); 128 | debug!("loading ca at: {}", path); 129 | builder.load_ca_certificate(path) 130 | } 131 | 132 | fn configure_out_of_cluster( 133 | builder: B, 134 | kube_config: &KubeConfig, 135 | ) -> Result<(B, Option)> { 136 | use std::process::Command; 137 | 138 | use base64::prelude::{Engine, BASE64_STANDARD}; 139 | 140 | use k8_types::core::plugin::ExecCredentialSpec; 141 | use k8_types::K8Obj; 142 | 143 | let current_user = kube_config 144 | .current_user() 145 | .ok_or_else(|| anyhow!("config must have current user"))?; 146 | 147 | let current_cluster = kube_config 148 | .current_cluster() 149 | .ok_or_else(|| anyhow!("config must have current cluster"))?; 150 | 151 | // load CA cluster 152 | 153 | let builder = if let Some(ca_data) = ¤t_cluster.cluster.certificate_authority_data { 154 | debug!("detected in-line cluster CA certs"); 155 | let pem_bytes = BASE64_STANDARD.decode(ca_data).unwrap(); 156 | builder.load_ca_cert_with_data(pem_bytes)? 157 | } else { 158 | // let not inline, then must must ref to file 159 | if let Some(ca_certificate_path) = 160 | current_cluster.cluster.certificate_authority.as_ref() 161 | { 162 | debug!("loading cluster CA from: {:#?}", ca_certificate_path); 163 | builder.load_ca_certificate(ca_certificate_path)? 164 | } else { 165 | return Ok((builder, None)); 166 | } 167 | }; 168 | 169 | // load client certs 170 | // Note: Google Kubernetes (GKE) clusters don't have any of these set on user 171 | if let Some(exec) = ¤t_user.user.exec { 172 | debug!(exec = ?exec,"loading client CA using exec"); 173 | 174 | let token_output = Command::new(exec.command.clone()) 175 | .args(exec.args.clone()) 176 | .output()?; 177 | 178 | debug!( 179 | cmd_token = ?String::from_utf8_lossy(&token_output.stdout).to_string() 180 | ); 181 | 182 | let credential: K8Obj = 183 | serde_json::from_slice(&token_output.stdout).map_err(|err| { 184 | let cmd_token = String::from_utf8_lossy(&token_output.stdout).to_string(); 185 | anyhow!( 186 | "error parsing credential from: {} {}\nreply: {}\nerr: {}", 187 | exec.command, 188 | exec.args.join(" "), 189 | cmd_token, 190 | err 191 | ) 192 | })?; 193 | let token = credential.status.token; 194 | debug!(?token); 195 | Ok((builder, Some(token))) 196 | } else if let Some(client_cert_data) = ¤t_user.user.client_certificate_data { 197 | debug!("detected in-line cluster CA certs"); 198 | let client_cert_pem_bytes = BASE64_STANDARD 199 | .decode(client_cert_data) 200 | .context("base64 decoding err")?; 201 | 202 | let client_key_pem_bytes = BASE64_STANDARD 203 | .decode( 204 | current_user 205 | .user 206 | .client_key_data 207 | .as_ref() 208 | .ok_or_else(|| anyhow!("current user must have client key data"))?, 209 | ) 210 | .context("base64 decoding err")?; 211 | 212 | Ok(( 213 | builder.load_client_certificate_with_data( 214 | client_cert_pem_bytes, 215 | client_key_pem_bytes, 216 | )?, 217 | None, 218 | )) 219 | } else if let Some(client_crt_path) = current_user.user.client_certificate.as_ref() { 220 | let client_key_path = current_user 221 | .user 222 | .client_key 223 | .as_ref() 224 | .ok_or_else(|| anyhow!("current user must have client key"))?; 225 | 226 | debug!( 227 | "loading client crt: {} and client key: {}", 228 | client_crt_path, client_key_path 229 | ); 230 | Ok(( 231 | builder.load_client_certificate(client_crt_path, client_key_path)?, 232 | None, 233 | )) 234 | } else if let Some(user_token) = ¤t_user.user.token { 235 | Ok((builder, Some(user_token.clone()))) 236 | } else if let Some(AuthProviderDetail::Gcp(_)) = ¤t_user.user.auth_provider { 237 | Ok((builder, None)) 238 | } else { 239 | Err(anyhow!( 240 | "no client cert crt data, path or user token were found" 241 | )) 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/k8-client/src/client/config_native.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Error as IoError, ErrorKind, Result as IoResult}; 2 | use std::net::ToSocketAddrs; 3 | use std::path::Path; 4 | use std::pin::Pin; 5 | use std::sync::Arc; 6 | use std::task::{Context, Poll}; 7 | 8 | use anyhow::{anyhow, Result}; 9 | use futures_util::future::Future; 10 | use futures_util::io::{AsyncRead as StdAsyncRead, AsyncWrite as StdAsyncWrite}; 11 | use http::Uri; 12 | use hyper::client::connect::{Connected, Connection}; 13 | use hyper::rt::Executor; 14 | use hyper::service::Service; 15 | use hyper::Body; 16 | use hyper::Client; 17 | use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; 18 | use tracing::debug; 19 | 20 | use fluvio_future::native_tls::{ 21 | CertBuilder, ConnectorBuilder, DefaultClientTlsStream, IdentityBuilder, PrivateKeyBuilder, 22 | TlsConnector, X509PemBuilder, 23 | }; 24 | use fluvio_future::net::TcpStream; 25 | use fluvio_future::task::spawn; 26 | 27 | use crate::cert::{ClientConfigBuilder, ConfigBuilder}; 28 | 29 | pub type HyperClient = Client; 30 | 31 | pub type HyperConfigBuilder = ClientConfigBuilder; 32 | 33 | pub struct HyperTlsStream(DefaultClientTlsStream); 34 | 35 | impl Connection for HyperTlsStream { 36 | fn connected(&self) -> Connected { 37 | Connected::new() 38 | } 39 | } 40 | 41 | impl AsyncRead for HyperTlsStream { 42 | fn poll_read( 43 | mut self: Pin<&mut Self>, 44 | cx: &mut Context<'_>, 45 | buf: &mut ReadBuf<'_>, 46 | ) -> Poll> { 47 | match Pin::new(&mut self.0).poll_read(cx, buf.initialize_unfilled())? { 48 | Poll::Ready(bytes_read) => { 49 | buf.advance(bytes_read); 50 | Poll::Ready(Ok(())) 51 | } 52 | Poll::Pending => Poll::Pending, 53 | } 54 | } 55 | } 56 | 57 | impl AsyncWrite for HyperTlsStream { 58 | fn poll_write( 59 | mut self: Pin<&mut Self>, 60 | cx: &mut Context<'_>, 61 | buf: &[u8], 62 | ) -> Poll> { 63 | Pin::new(&mut self.0).poll_write(cx, buf) 64 | } 65 | 66 | fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 67 | Pin::new(&mut self.0).poll_flush(cx) 68 | } 69 | 70 | fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 71 | Pin::new(&mut self.0).poll_close(cx) 72 | } 73 | } 74 | 75 | struct FluvioHyperExecutor; 76 | 77 | impl Executor for FluvioHyperExecutor { 78 | fn execute(&self, fut: F) { 79 | spawn(async { drop(fut.await) }); 80 | } 81 | } 82 | 83 | /// hyper connector that uses fluvio TLS 84 | #[derive(Clone)] 85 | pub struct TlsHyperConnector(Arc); 86 | 87 | impl TlsHyperConnector { 88 | fn new(connector: TlsConnector) -> Self { 89 | Self(Arc::new(connector)) 90 | } 91 | } 92 | 93 | #[allow(clippy::type_complexity)] 94 | impl Service for TlsHyperConnector { 95 | type Response = HyperTlsStream; 96 | type Error = anyhow::Error; 97 | 98 | type Future = Pin> + Send>>; 99 | 100 | fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { 101 | Poll::Ready(Ok(())) 102 | } 103 | 104 | fn call(&mut self, uri: Uri) -> Self::Future { 105 | let connector = self.0.clone(); 106 | 107 | Box::pin(async move { 108 | let host = match uri.host() { 109 | Some(h) => h, 110 | None => return Err(anyhow!("no host")), 111 | }; 112 | 113 | match uri.scheme_str() { 114 | Some("http") => Err(anyhow!("http not supported")), 115 | Some("https") => { 116 | let socket_addr = { 117 | let host = host.to_string(); 118 | let port = uri.port_u16().unwrap_or(443); 119 | match (host.as_str(), port).to_socket_addrs()?.next() { 120 | Some(addr) => addr, 121 | None => return Err(anyhow!("host resolution: {} failed", host)), 122 | } 123 | }; 124 | debug!("socket address to: {}", socket_addr); 125 | let tcp_stream = TcpStream::connect(&socket_addr).await?; 126 | 127 | let stream = connector.connect(host, tcp_stream).await.map_err(|err| { 128 | IoError::new(ErrorKind::Other, format!("tls handshake: {}", err)) 129 | })?; 130 | Ok(HyperTlsStream(stream)) 131 | } 132 | scheme => Err(anyhow::anyhow!("{:?}", scheme)), 133 | } 134 | }) 135 | } 136 | } 137 | 138 | #[derive(Default)] 139 | pub struct HyperClientBuilder { 140 | ca_cert: Option, 141 | client_identity: Option, 142 | } 143 | 144 | impl ConfigBuilder for HyperClientBuilder { 145 | type Client = HyperClient; 146 | 147 | fn new() -> Self { 148 | Self::default() 149 | } 150 | 151 | fn build(self) -> Result { 152 | let ca_cert = self.ca_cert; 153 | 154 | let mut connector_builder = match self.client_identity { 155 | Some(builder) => ConnectorBuilder::identity(builder)?, 156 | None => ConnectorBuilder::anonymous(), 157 | }; 158 | 159 | if let Some(ca_cert) = ca_cert { 160 | connector_builder = connector_builder.add_root_certificate(ca_cert)? 161 | } 162 | 163 | let connector = connector_builder.build(); 164 | Ok(Client::builder() 165 | .executor(FluvioHyperExecutor) 166 | .build::<_, Body>(TlsHyperConnector::new(connector))) 167 | } 168 | 169 | fn load_ca_certificate(self, ca_path: impl AsRef) -> Result { 170 | let ca_builder = X509PemBuilder::from_path(ca_path)?; 171 | Ok(Self { 172 | ca_cert: Some(ca_builder), 173 | client_identity: self.client_identity, 174 | }) 175 | } 176 | 177 | fn load_ca_cert_with_data(self, ca_data: Vec) -> Result { 178 | let ca_builder = X509PemBuilder::new(ca_data); 179 | 180 | Ok(Self { 181 | ca_cert: Some(ca_builder), 182 | client_identity: self.client_identity, 183 | }) 184 | } 185 | 186 | fn load_client_certificate>( 187 | self, 188 | client_crt_path: P, 189 | client_key_path: P, 190 | ) -> Result { 191 | debug!("loading client crt from: {:#?}", client_crt_path.as_ref()); 192 | debug!("loading client key from: {:#?}", client_key_path.as_ref()); 193 | 194 | let identity = IdentityBuilder::from_x509( 195 | X509PemBuilder::from_path(client_crt_path)?, 196 | PrivateKeyBuilder::from_path(client_key_path)?, 197 | )?; 198 | 199 | Ok(Self { 200 | ca_cert: self.ca_cert, 201 | client_identity: Some(identity), 202 | }) 203 | } 204 | 205 | fn load_client_certificate_with_data( 206 | self, 207 | client_crt: Vec, 208 | client_key: Vec, 209 | ) -> Result { 210 | let identity = IdentityBuilder::from_x509( 211 | X509PemBuilder::new(client_crt), 212 | PrivateKeyBuilder::new(client_key), 213 | )?; 214 | 215 | Ok(Self { 216 | ca_cert: self.ca_cert, 217 | client_identity: Some(identity), 218 | }) 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/k8-client/src/client/config_openssl.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Error as IoError, ErrorKind, Result as IoResult}; 2 | use std::net::ToSocketAddrs; 3 | use std::path::Path; 4 | use std::pin::Pin; 5 | use std::sync::Arc; 6 | use std::task::{Context, Poll}; 7 | 8 | use anyhow::anyhow; 9 | use futures_util::future::Future; 10 | use futures_util::io::{AsyncRead as StdAsyncRead, AsyncWrite as StdAsyncWrite}; 11 | use http::Uri; 12 | use hyper::client::connect::{Connected, Connection}; 13 | use hyper::rt::Executor; 14 | use hyper::service::Service; 15 | use hyper::Body; 16 | use hyper::Client; 17 | use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; 18 | use tracing::debug; 19 | 20 | use fluvio_future::{ 21 | net::certs::CertBuilder, 22 | openssl::{ 23 | TlsConnector, DefaultClientTlsStream, 24 | certs::{X509PemBuilder, IdentityBuilder, PrivateKeyBuilder}, 25 | }, 26 | }; 27 | use fluvio_future::net::TcpStream; 28 | use fluvio_future::task::spawn; 29 | 30 | use crate::cert::{ClientConfigBuilder, ConfigBuilder}; 31 | 32 | pub type HyperClient = Client; 33 | 34 | pub type HyperConfigBuilder = ClientConfigBuilder; 35 | 36 | pub struct HyperTlsStream(DefaultClientTlsStream); 37 | 38 | impl Connection for HyperTlsStream { 39 | fn connected(&self) -> Connected { 40 | Connected::new() 41 | } 42 | } 43 | 44 | impl AsyncRead for HyperTlsStream { 45 | fn poll_read( 46 | mut self: Pin<&mut Self>, 47 | cx: &mut Context<'_>, 48 | buf: &mut ReadBuf<'_>, 49 | ) -> Poll> { 50 | match Pin::new(&mut self.0).poll_read(cx, buf.initialize_unfilled())? { 51 | Poll::Ready(bytes_read) => { 52 | buf.advance(bytes_read); 53 | Poll::Ready(Ok(())) 54 | } 55 | Poll::Pending => Poll::Pending, 56 | } 57 | } 58 | } 59 | 60 | impl AsyncWrite for HyperTlsStream { 61 | fn poll_write( 62 | mut self: Pin<&mut Self>, 63 | cx: &mut Context<'_>, 64 | buf: &[u8], 65 | ) -> Poll> { 66 | Pin::new(&mut self.0).poll_write(cx, buf) 67 | } 68 | 69 | fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 70 | Pin::new(&mut self.0).poll_flush(cx) 71 | } 72 | 73 | fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 74 | Pin::new(&mut self.0).poll_close(cx) 75 | } 76 | } 77 | 78 | struct FluvioHyperExecutor; 79 | 80 | impl Executor for FluvioHyperExecutor { 81 | fn execute(&self, fut: F) { 82 | spawn(async { drop(fut.await) }); 83 | } 84 | } 85 | 86 | /// hyper connector that uses fluvio TLS 87 | #[derive(Clone)] 88 | pub struct TlsHyperConnector(Arc); 89 | 90 | impl TlsHyperConnector { 91 | fn new(connector: TlsConnector) -> Self { 92 | Self(Arc::new(connector)) 93 | } 94 | } 95 | 96 | #[allow(clippy::type_complexity)] 97 | impl Service for TlsHyperConnector { 98 | type Response = HyperTlsStream; 99 | type Error = anyhow::Error; 100 | 101 | type Future = Pin> + Send>>; 102 | 103 | fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { 104 | Poll::Ready(Ok(())) 105 | } 106 | 107 | fn call(&mut self, uri: Uri) -> Self::Future { 108 | let connector = self.0.clone(); 109 | 110 | Box::pin(async move { 111 | let host = match uri.host() { 112 | Some(h) => h, 113 | None => return Err(anyhow!("no host")), 114 | }; 115 | 116 | match uri.scheme_str() { 117 | Some("http") => Err(anyhow!("http not supported")), 118 | Some("https") => { 119 | let socket_addr = { 120 | let host = host.to_string(); 121 | let port = uri.port_u16().unwrap_or(443); 122 | match (host.as_str(), port).to_socket_addrs()?.next() { 123 | Some(addr) => addr, 124 | None => return Err(anyhow!("host resolution: {} failed", host)), 125 | } 126 | }; 127 | debug!("socket address to: {}", socket_addr); 128 | let tcp_stream = TcpStream::connect(&socket_addr).await?; 129 | 130 | let stream = connector.connect(host, tcp_stream).await.map_err(|err| { 131 | IoError::new(ErrorKind::Other, format!("tls handshake: {}", err)) 132 | })?; 133 | Ok(HyperTlsStream(stream)) 134 | } 135 | scheme => Err(anyhow!("{:?}", scheme)), 136 | } 137 | }) 138 | } 139 | } 140 | 141 | #[derive(Default)] 142 | pub struct HyperClientBuilder { 143 | ca_cert: Option, 144 | client_identity: Option, 145 | } 146 | 147 | impl ConfigBuilder for HyperClientBuilder { 148 | type Client = HyperClient; 149 | 150 | fn new() -> Self { 151 | Self::default() 152 | } 153 | 154 | fn build(self) -> anyhow::Result { 155 | let ca_cert = match self.ca_cert { 156 | Some(cert) => cert.build().ok(), 157 | None => None, 158 | }; 159 | let mut connector_builder = TlsConnector::builder()?; 160 | 161 | if let Some(builder) = self.client_identity { 162 | connector_builder = connector_builder.with_identity(builder)?; 163 | } 164 | 165 | if let Some(ca_cert) = ca_cert { 166 | connector_builder = connector_builder.add_root_certificate(ca_cert)?; 167 | } 168 | 169 | let connector = connector_builder.build(); 170 | Ok(Client::builder() 171 | .executor(FluvioHyperExecutor) 172 | .build::<_, Body>(TlsHyperConnector::new(connector))) 173 | } 174 | 175 | fn load_ca_certificate(self, ca_path: impl AsRef) -> anyhow::Result { 176 | let ca_builder = X509PemBuilder::from_path(ca_path)?; 177 | Ok(Self { 178 | ca_cert: Some(ca_builder), 179 | client_identity: self.client_identity, 180 | }) 181 | } 182 | 183 | fn load_ca_cert_with_data(self, ca_data: Vec) -> anyhow::Result { 184 | let ca_builder = X509PemBuilder::new(ca_data); 185 | 186 | Ok(Self { 187 | ca_cert: Some(ca_builder), 188 | client_identity: self.client_identity, 189 | }) 190 | } 191 | 192 | fn load_client_certificate>( 193 | self, 194 | client_crt_path: P, 195 | client_key_path: P, 196 | ) -> anyhow::Result { 197 | debug!("loading client crt from: {:#?}", client_crt_path.as_ref()); 198 | debug!("loading client key from: {:#?}", client_key_path.as_ref()); 199 | 200 | let identity = IdentityBuilder::from_x509( 201 | X509PemBuilder::from_path(client_crt_path)?, 202 | PrivateKeyBuilder::from_path(client_key_path)?, 203 | )?; 204 | 205 | Ok(Self { 206 | ca_cert: self.ca_cert, 207 | client_identity: Some(identity), 208 | }) 209 | } 210 | 211 | fn load_client_certificate_with_data( 212 | self, 213 | client_crt: Vec, 214 | client_key: Vec, 215 | ) -> anyhow::Result { 216 | let identity = IdentityBuilder::from_x509( 217 | X509PemBuilder::new(client_crt), 218 | PrivateKeyBuilder::new(client_key), 219 | )?; 220 | 221 | Ok(Self { 222 | ca_cert: self.ca_cert, 223 | client_identity: Some(identity), 224 | }) 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/k8-client/src/client/config_rustls.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Error as IoError, ErrorKind, Result as IoResult}; 2 | use std::net::ToSocketAddrs; 3 | use std::path::Path; 4 | use std::pin::Pin; 5 | use std::sync::Arc; 6 | use std::task::{Context, Poll}; 7 | 8 | use anyhow::{anyhow, Result}; 9 | use futures_util::future::Future; 10 | use futures_util::io::{AsyncRead as StdAsyncRead, AsyncWrite as StdAsyncWrite}; 11 | use http::Uri; 12 | use tracing::debug; 13 | 14 | use hyper::client::connect::{Connected, Connection}; 15 | use hyper::service::Service; 16 | use hyper::Body; 17 | use hyper::Client; 18 | use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; 19 | use rustls::WantsVerifier; 20 | use rustls::client::WantsClientCert; 21 | 22 | use fluvio_future::net::TcpStream; 23 | use fluvio_future::rust_tls::{ 24 | ConnectorBuilder, ConnectorBuilderStage, DefaultClientTlsStream, TlsConnector, 25 | ConnectorBuilderWithConfig, 26 | }; 27 | use super::executor::FluvioHyperExecutor; 28 | use crate::cert::{ClientConfigBuilder, ConfigBuilder}; 29 | 30 | pub type HyperClient = Client; 31 | 32 | pub type HyperConfigBuilder = ClientConfigBuilder; 33 | 34 | pub struct HyperTlsStream(DefaultClientTlsStream); 35 | 36 | impl Connection for HyperTlsStream { 37 | fn connected(&self) -> Connected { 38 | Connected::new() 39 | } 40 | } 41 | 42 | impl AsyncRead for HyperTlsStream { 43 | fn poll_read( 44 | mut self: Pin<&mut Self>, 45 | cx: &mut Context<'_>, 46 | buf: &mut ReadBuf<'_>, 47 | ) -> Poll> { 48 | match Pin::new(&mut self.0).poll_read(cx, buf.initialize_unfilled())? { 49 | Poll::Ready(bytes_read) => { 50 | buf.advance(bytes_read); 51 | Poll::Ready(Ok(())) 52 | } 53 | Poll::Pending => Poll::Pending, 54 | } 55 | } 56 | } 57 | 58 | impl AsyncWrite for HyperTlsStream { 59 | fn poll_write( 60 | mut self: Pin<&mut Self>, 61 | cx: &mut Context<'_>, 62 | buf: &[u8], 63 | ) -> Poll> { 64 | Pin::new(&mut self.0).poll_write(cx, buf) 65 | } 66 | 67 | fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 68 | Pin::new(&mut self.0).poll_flush(cx) 69 | } 70 | 71 | fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 72 | Pin::new(&mut self.0).poll_close(cx) 73 | } 74 | } 75 | 76 | /// hyper connector that uses fluvio TLS 77 | #[derive(Clone)] 78 | pub struct TlsHyperConnector(Arc); 79 | 80 | impl TlsHyperConnector { 81 | fn new(connector: TlsConnector) -> Self { 82 | Self(Arc::new(connector)) 83 | } 84 | } 85 | 86 | impl Service for TlsHyperConnector { 87 | type Response = HyperTlsStream; 88 | type Error = anyhow::Error; 89 | 90 | type Future = Pin> + Send>>; 91 | 92 | fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { 93 | Poll::Ready(Ok(())) 94 | } 95 | 96 | fn call(&mut self, uri: Uri) -> Self::Future { 97 | let connector = self.0.clone(); 98 | 99 | Box::pin(async move { 100 | let host = match uri.host() { 101 | Some(h) => h.to_owned(), 102 | None => return Err(anyhow!("no host")), 103 | }; 104 | 105 | match uri.scheme_str() { 106 | Some("http") => Err(anyhow!("http not supported")), 107 | Some("https") => { 108 | let socket_addr = { 109 | let host = host.to_string(); 110 | let port = uri.port_u16().unwrap_or(443); 111 | match (host.as_str(), port).to_socket_addrs()?.next() { 112 | Some(addr) => addr, 113 | None => return Err(anyhow!("host resolution: {} failed", host)), 114 | } 115 | }; 116 | debug!("socket address to: {}", socket_addr); 117 | let tcp_stream = TcpStream::connect(&socket_addr).await?; 118 | 119 | let stream = connector 120 | .connect(host.try_into()?, tcp_stream) 121 | .await 122 | .map_err(|err| { 123 | IoError::new(ErrorKind::Other, format!("tls handshake: {}", err)) 124 | })?; 125 | Ok(HyperTlsStream(stream)) 126 | } 127 | scheme => Err(anyhow!("{:?}", scheme)), 128 | } 129 | }) 130 | } 131 | } 132 | 133 | pub enum ConnectorBuilderStages { 134 | WantsVerifier(ConnectorBuilderStage), 135 | WantsClientCert(ConnectorBuilderStage), 136 | ConnectorBuilder(ConnectorBuilderWithConfig), 137 | } 138 | 139 | impl ConnectorBuilderStages { 140 | pub fn build(self) -> Result { 141 | match self { 142 | Self::WantsVerifier(_) => Err(anyhow!("missing verifier")), 143 | Self::WantsClientCert(_) => Err(anyhow!("missing client cert")), 144 | Self::ConnectorBuilder(builder) => Ok(builder.build()), 145 | } 146 | } 147 | 148 | pub fn load_client_certs>(self, cert_path: P, key_path: P) -> Result { 149 | match self { 150 | Self::WantsVerifier(_) => Err(anyhow!("missing verifier")), 151 | Self::WantsClientCert(builder) => { 152 | Ok(builder.load_client_certs(cert_path, key_path)?.into()) 153 | } 154 | Self::ConnectorBuilder(_) => Err(anyhow!("already loaded client cert")), 155 | } 156 | } 157 | 158 | pub fn load_ca_cert>(self, path: P) -> Result { 159 | match self { 160 | Self::WantsVerifier(builder) => Ok(builder.load_ca_cert(path)?.into()), 161 | Self::WantsClientCert(_) => Err(anyhow!("already loaded ca cert")), 162 | Self::ConnectorBuilder(_) => Err(anyhow!("already loaded ca cert")), 163 | } 164 | } 165 | 166 | pub fn load_ca_cert_from_bytes(self, buffer: &[u8]) -> Result { 167 | match self { 168 | Self::WantsVerifier(builder) => Ok(builder.load_ca_cert_from_bytes(buffer)?.into()), 169 | Self::WantsClientCert(_) => Err(anyhow!("already loaded ca cert")), 170 | Self::ConnectorBuilder(_) => Err(anyhow!("already loaded ca cert")), 171 | } 172 | } 173 | 174 | pub fn load_client_certs_from_bytes( 175 | self, 176 | cert_buffer: &[u8], 177 | key_buffer: &[u8], 178 | ) -> Result { 179 | match self { 180 | Self::WantsVerifier(_) => Err(anyhow!("missing verifier")), 181 | Self::WantsClientCert(builder) => Ok(builder 182 | .load_client_certs_from_bytes(cert_buffer, key_buffer)? 183 | .into()), 184 | Self::ConnectorBuilder(_) => Err(anyhow!("already loaded client cert")), 185 | } 186 | } 187 | } 188 | 189 | impl From> for ConnectorBuilderStages { 190 | fn from(builder: ConnectorBuilderStage) -> Self { 191 | Self::WantsVerifier(builder) 192 | } 193 | } 194 | 195 | impl From> for ConnectorBuilderStages { 196 | fn from(builder: ConnectorBuilderStage) -> Self { 197 | Self::WantsClientCert(builder) 198 | } 199 | } 200 | 201 | impl From for ConnectorBuilderStages { 202 | fn from(builder: ConnectorBuilderWithConfig) -> Self { 203 | Self::ConnectorBuilder(builder) 204 | } 205 | } 206 | 207 | //#[derive(Default)] 208 | pub struct HyperClientBuilder(ConnectorBuilderStages); 209 | 210 | impl ConfigBuilder for HyperClientBuilder { 211 | type Client = HyperClient; 212 | 213 | fn new() -> Self { 214 | Self(ConnectorBuilder::with_safe_defaults().into()) 215 | } 216 | 217 | fn build(self) -> Result { 218 | let connector = self.0.build()?; 219 | 220 | Ok(Client::builder() 221 | .executor(FluvioHyperExecutor) 222 | .build::<_, Body>(TlsHyperConnector::new(connector))) 223 | } 224 | 225 | fn load_ca_certificate(self, ca_path: impl AsRef) -> Result { 226 | Ok(Self(self.0.load_ca_cert(ca_path)?)) 227 | } 228 | 229 | fn load_ca_cert_with_data(self, ca_data: Vec) -> Result { 230 | Ok(Self(self.0.load_ca_cert_from_bytes(&ca_data)?)) 231 | } 232 | 233 | fn load_client_certificate_with_data( 234 | self, 235 | client_crt: Vec, 236 | client_key: Vec, 237 | ) -> Result { 238 | Ok(Self( 239 | self.0 240 | .load_client_certs_from_bytes(&client_crt, &client_key)?, 241 | )) 242 | } 243 | 244 | fn load_client_certificate>( 245 | self, 246 | client_crt_path: P, 247 | client_key_path: P, 248 | ) -> Result { 249 | Ok(Self( 250 | self.0.load_client_certs(client_crt_path, client_key_path)?, 251 | )) 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/k8-client/src/client/list_stream.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | use std::mem::replace; 3 | use std::mem::transmute; 4 | use std::pin::Pin; 5 | use std::task::Context; 6 | use std::task::Poll; 7 | 8 | use tracing::debug; 9 | use tracing::error; 10 | use tracing::trace; 11 | 12 | use futures_util::future::Future; 13 | use futures_util::future::FutureExt; 14 | use futures_util::stream::Stream; 15 | use pin_utils::unsafe_pinned; 16 | use pin_utils::unsafe_unpinned; 17 | 18 | use k8_metadata_client::ListArg; 19 | use k8_metadata_client::NameSpace; 20 | 21 | use k8_types::{K8List, Spec}; 22 | use k8_types::options::ListOptions; 23 | use crate::K8Client; 24 | use crate::SharedK8Client; 25 | 26 | type K8ListImpl<'a, S> = 27 | Option>> + Send + 'a>>>; 28 | 29 | pub struct ListStream<'a, S> 30 | where 31 | S: Spec, 32 | { 33 | arg: Option, 34 | limit: u32, 35 | done: bool, 36 | namespace: NameSpace, 37 | client: SharedK8Client, 38 | inner: K8ListImpl<'a, S>, 39 | data1: PhantomData, 40 | } 41 | 42 | impl ListStream<'_, S> 43 | where 44 | S: Spec, 45 | { 46 | #[allow(unused)] 47 | pub fn new( 48 | namespace: NameSpace, 49 | limit: u32, 50 | arg: Option, 51 | client: SharedK8Client, 52 | ) -> Self { 53 | Self { 54 | done: false, 55 | namespace, 56 | limit, 57 | arg, 58 | client, 59 | inner: None, 60 | data1: PhantomData, 61 | } 62 | } 63 | } 64 | 65 | impl Unpin for ListStream<'_, S> where S: Spec {} 66 | 67 | impl<'a, S> ListStream<'a, S> 68 | where 69 | S: Spec, 70 | { 71 | unsafe_pinned!(inner: K8ListImpl<'a, S>); 72 | unsafe_unpinned!(client: SharedK8Client); 73 | 74 | /// given continuation, generate list option 75 | fn list_option(&self, continu: Option) -> ListOptions { 76 | let field_selector = match &self.arg { 77 | None => None, 78 | Some(arg) => arg.field_selector.clone(), 79 | }; 80 | 81 | let label_selector = match &self.arg { 82 | None => None, 83 | Some(arg) => arg.label_selector.clone(), 84 | }; 85 | 86 | ListOptions { 87 | limit: Some(self.limit), 88 | continu, 89 | field_selector, 90 | label_selector, 91 | ..Default::default() 92 | } 93 | } 94 | } 95 | 96 | impl ListStream<'_, S> 97 | where 98 | S: Spec + 'static, 99 | { 100 | #[allow(clippy::transmute_ptr_to_ptr)] 101 | fn set_inner(mut self: Pin<&mut Self>, list_option: Option) { 102 | let namespace = self.as_ref().namespace.clone(); 103 | let current_client = &self.as_ref().client; 104 | // HACK, we transmute the lifetime so that we satisfy borrow checker. should be safe.... 105 | let client: &'_ K8Client = 106 | unsafe { transmute::<&'_ K8Client, &'_ K8Client>(current_client) }; 107 | self.as_mut() 108 | .inner() 109 | .replace(client.retrieve_items_inner(namespace, list_option).boxed()); 110 | } 111 | } 112 | 113 | impl Stream for ListStream<'_, S> 114 | where 115 | S: Spec + 'static, 116 | { 117 | type Item = K8List; 118 | 119 | fn poll_next(mut self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll> { 120 | trace!( 121 | "{}: polling, done: {}, inner none: {}", 122 | S::label(), 123 | self.as_ref().done, 124 | self.as_ref().inner.is_none() 125 | ); 126 | 127 | if self.as_ref().done { 128 | trace!("{} is done, returning none", S::label()); 129 | return Poll::Ready(None); 130 | } 131 | 132 | if self.as_ref().inner.is_none() { 133 | trace!("{} no inner set.", S::label()); 134 | let list_option = self.as_ref().list_option(None); 135 | self.as_mut().set_inner(Some(list_option)); 136 | trace!("{} set inner, returning pending", S::label()); 137 | } 138 | 139 | trace!("{} polling inner", S::label()); 140 | match self.as_mut().inner().as_pin_mut() { 141 | Some(fut) => match fut.poll(ctx) { 142 | Poll::Pending => { 143 | trace!("{} inner was pending, loop continue", S::label()); 144 | Poll::Pending 145 | } 146 | Poll::Ready(val) => { 147 | match val { 148 | Ok(list) => { 149 | debug!("{} inner returned items: {}", S::label(), list.items.len()); 150 | // check if we have continue 151 | if let Some(_cont) = &list.metadata._continue { 152 | debug!("{}: we got continue: {}", S::label(), _cont); 153 | let list_option = self.as_ref().list_option(Some(_cont.clone())); 154 | self.set_inner(Some(list_option)); 155 | trace!("{}: ready and set inner, returning ready", S::label()); 156 | } else { 157 | debug!("{} no more continue, marking as done", S::label()); 158 | // we are done 159 | let _ = replace(&mut self.as_mut().done, true); 160 | } 161 | Poll::Ready(Some(list)) 162 | } 163 | Err(err) => { 164 | error!("{}: error in list stream: {}", S::label(), err); 165 | let _ = replace(&mut self.as_mut().done, true); 166 | Poll::Ready(None) 167 | } 168 | } 169 | } 170 | }, 171 | None => panic!("{} inner should be always set", S::label()), 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/k8-client/src/client/log_stream.rs: -------------------------------------------------------------------------------- 1 | use std::{pin::Pin, task::Poll}; 2 | 3 | use bytes::{Bytes, BufMut}; 4 | use futures_util::{AsyncRead, Stream, StreamExt}; 5 | 6 | pub struct LogStream(pub Pin + Send + Sync + 'static>>); 7 | 8 | impl AsyncRead for LogStream { 9 | fn poll_read( 10 | mut self: Pin<&mut Self>, 11 | cx: &mut std::task::Context<'_>, 12 | mut buf: &mut [u8], 13 | ) -> std::task::Poll> { 14 | match self.0.poll_next_unpin(cx) { 15 | Poll::Ready(Some(chunk)) => { 16 | buf.put_slice(&chunk); 17 | buf.put_u8(0x0A); 18 | Poll::Ready(std::io::Result::Ok(chunk.len() + 1)) 19 | } 20 | Poll::Ready(None) => Poll::Ready(std::io::Result::Ok(0)), 21 | Poll::Pending => Poll::Pending, 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/k8-client/src/client/mod.rs: -------------------------------------------------------------------------------- 1 | mod client_impl; 2 | mod log_stream; 3 | #[cfg(feature = "memory_client")] 4 | pub mod memory; 5 | 6 | mod list_stream; 7 | mod wstream; 8 | 9 | pub use client_impl::K8Client; 10 | pub use log_stream::LogStream; 11 | 12 | cfg_if::cfg_if! { 13 | if #[cfg(feature = "openssl_tls")] { 14 | mod config_openssl; 15 | use config_openssl::*; 16 | } else if #[cfg(feature = "native_tls")] { 17 | mod config_native; 18 | use config_native::*; 19 | } else if #[cfg(feature = "rust_tls")] { 20 | mod config_rustls; 21 | use config_rustls::*; 22 | } 23 | } 24 | 25 | use list_stream::*; 26 | 27 | pub mod http { 28 | pub use ::http::header; 29 | pub use ::http::status; 30 | pub use ::http::Error; 31 | pub use ::http::uri::InvalidUri; 32 | pub use hyper::Uri; 33 | } 34 | 35 | pub mod prelude { 36 | pub use hyper::Body; 37 | pub use hyper::Request; 38 | } 39 | 40 | mod executor { 41 | 42 | use futures_util::future::Future; 43 | use hyper::rt::Executor; 44 | 45 | use fluvio_future::task::spawn; 46 | 47 | #[allow(dead_code)] 48 | pub(crate) struct FluvioHyperExecutor; 49 | 50 | impl Executor for FluvioHyperExecutor { 51 | fn execute(&self, fut: F) { 52 | spawn(async { drop(fut.await) }); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/k8-client/src/client/wstream.rs: -------------------------------------------------------------------------------- 1 | use std::marker::Unpin; 2 | use std::mem; 3 | use std::pin::Pin; 4 | use std::task::Context; 5 | use std::task::Poll; 6 | 7 | use bytes::BytesMut; 8 | use futures_util::stream::Stream; 9 | use hyper::body::Bytes; 10 | use hyper::Error; 11 | use pin_utils::unsafe_pinned; 12 | use pin_utils::unsafe_unpinned; 13 | use tracing::error; 14 | use tracing::trace; 15 | 16 | /// Watch Stream suitable for parsing Kubernetes HTTP stream 17 | /// It relies on inner stream which returns streams of bytes 18 | pub struct WatchStream 19 | where 20 | S: Stream, 21 | { 22 | stream: S, 23 | done: bool, 24 | buffer: BytesMut, 25 | } 26 | 27 | impl Unpin for WatchStream where S: Stream {} 28 | 29 | impl WatchStream 30 | where 31 | S: Stream>, 32 | { 33 | unsafe_pinned!(stream: S); 34 | unsafe_unpinned!(buffer: BytesMut); 35 | unsafe_unpinned!(done: bool); 36 | 37 | pub fn new(stream: S) -> Self { 38 | let buffer = BytesMut::new(); 39 | WatchStream { 40 | stream, 41 | done: false, 42 | buffer, 43 | } 44 | } 45 | } 46 | 47 | const SEPARATOR: u8 = b'\n'; 48 | 49 | impl Stream for WatchStream 50 | where 51 | S: Stream>, 52 | { 53 | type Item = Bytes; 54 | 55 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { 56 | let mut done = self.as_ref().done; 57 | let mut last_buffer = mem::replace(&mut self.as_mut().buffer, BytesMut::new()); 58 | 59 | trace!( 60 | "entering poll next with buffer: {}, done: {}", 61 | last_buffer.len(), 62 | done 63 | ); 64 | 65 | // if not done, we accumulate buffer from inner until they are exhausted 66 | if !done { 67 | loop { 68 | trace!("not done. polling inner"); 69 | match self.as_mut().stream().poll_next(cx) { 70 | Poll::Pending => break, 71 | Poll::Ready(chunk_item) => { 72 | match chunk_item { 73 | Some(chunk_result) => { 74 | match chunk_result { 75 | Ok(chunk) => { 76 | trace!("got inner stream len: {}", chunk.len()); 77 | // trace!("chunk: {}", String::from_utf8_lossy(&chunk).to_string()); 78 | last_buffer.extend_from_slice(chunk.as_ref()); 79 | } 80 | Err(err) => { 81 | error!("error getting chunk: {}", err); 82 | *self.as_mut().done() = true; 83 | return Poll::Ready(None); 84 | } 85 | } 86 | } 87 | None => { 88 | done = true; 89 | break; 90 | } 91 | } 92 | } 93 | } 94 | } 95 | } 96 | 97 | *(self.as_mut().done()) = done; 98 | 99 | if !last_buffer.is_empty() { 100 | trace!("no more inner, buffer len: {}", last_buffer.len()); 101 | // trace!("chunk: {:#}",String::from_utf8_lossy(&last_buffer).to_string()); 102 | 103 | if let Some(i) = last_buffer.iter().position(|&c| c == SEPARATOR) { 104 | trace!("found separator at: {}", i); 105 | let remainder = last_buffer.split_off(i + 1); 106 | // need to truncate last one since it contains remainder 107 | last_buffer.truncate(last_buffer.len() - 1); 108 | *(self.as_mut().buffer()) = remainder; 109 | return Poll::Ready(Some(last_buffer.freeze())); 110 | } else { 111 | trace!("no separator"); 112 | if done { 113 | trace!("since we are done, returning last buffer"); 114 | return Poll::Ready(Some(last_buffer.freeze())); 115 | } 116 | *(self.as_mut().buffer()) = last_buffer; 117 | } 118 | } else { 119 | trace!("no buffer, swapping pending"); 120 | *(self.as_mut().buffer()) = last_buffer; 121 | } 122 | 123 | if done { 124 | trace!("done, returning none"); 125 | Poll::Ready(None) 126 | } else { 127 | trace!("not done, returning pending"); 128 | Poll::Pending 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/k8-client/src/fixture.rs: -------------------------------------------------------------------------------- 1 | // common test fixtures 2 | 3 | pub const TEST_NS: &str = "test"; 4 | -------------------------------------------------------------------------------- /src/k8-client/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod cert; 2 | mod uri; 3 | 4 | mod client; 5 | pub use self::client::*; 6 | 7 | pub use k8_config::K8Config; 8 | 9 | #[cfg(feature = "k8")] 10 | pub mod fixture; 11 | 12 | pub use k8_metadata_client as meta_client; 13 | 14 | pub use shared::load_and_share; 15 | pub use shared::new_shared; 16 | pub use shared::SharedK8Client; 17 | 18 | mod shared { 19 | 20 | use anyhow::Result; 21 | 22 | use super::K8Client; 23 | use super::K8Config; 24 | use std::sync::Arc; 25 | 26 | pub type SharedK8Client = Arc; 27 | 28 | /// create new shared k8 client based on k8 config 29 | pub fn new_shared(config: K8Config) -> Result { 30 | let client = K8Client::new(config)?; 31 | Ok(Arc::new(client)) 32 | } 33 | 34 | /// load k8 config and create shared k8 client 35 | pub fn load_and_share() -> Result { 36 | let config = K8Config::load()?; 37 | new_shared(config) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/k8-client/src/uri.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | use k8_types::{Crd, Spec}; 4 | use k8_types::options::ListOptions; 5 | 6 | use crate::http::Uri; 7 | use crate::meta_client::NameSpace; 8 | 9 | /// items uri 10 | pub fn item_uri( 11 | host: &str, 12 | name: &str, 13 | namespace: &str, 14 | sub_resource: Option<&str>, 15 | query: Option<&str>, 16 | ) -> Result 17 | where 18 | S: Spec, 19 | { 20 | let ns = if S::NAME_SPACED { 21 | NameSpace::Named(namespace.to_owned()) 22 | } else { 23 | NameSpace::All 24 | }; 25 | 26 | let crd = S::metadata(); 27 | let prefix = prefix_uri(crd, host, ns, None); 28 | let sub_resource = sub_resource.unwrap_or(""); 29 | let query = query.map(|q| format!("?{}", q)).unwrap_or_default(); 30 | let uri_value = format!("{prefix}/{name}{sub_resource}{query}"); 31 | let uri: Uri = uri_value.parse()?; 32 | 33 | Ok(uri) 34 | } 35 | 36 | /// items uri 37 | pub fn items_uri(host: &str, namespace: NameSpace, list_options: Option) -> Uri 38 | where 39 | S: Spec, 40 | { 41 | let ns = if S::NAME_SPACED { 42 | namespace 43 | } else { 44 | NameSpace::All 45 | }; 46 | let crd = S::metadata(); 47 | let uri_value = prefix_uri(crd, host, ns, list_options); 48 | let uri: Uri = uri_value.parse().unwrap(); 49 | uri 50 | } 51 | 52 | /// related to query parameters and uri 53 | /// 54 | /// 55 | /// 56 | /// generate prefix for given crd 57 | /// if crd group is core then /api is used otherwise /apis + group 58 | pub fn prefix_uri(crd: &Crd, host: &str, ns: N, options: Option) -> String 59 | where 60 | N: Into, 61 | { 62 | let namespace = ns.into(); 63 | let version = crd.version; 64 | let plural = crd.names.plural; 65 | let group = crd.group; 66 | let api_prefix = match group { 67 | "core" => "api".to_owned(), 68 | _ => format!("apis/{}", group), 69 | }; 70 | 71 | let query = if let Some(opt) = options { 72 | let mut query = "?".to_owned(); 73 | let qs = serde_qs::to_string(&opt).unwrap(); 74 | query.push_str(&qs); 75 | query 76 | } else { 77 | "".to_owned() 78 | }; 79 | 80 | if namespace.is_all() { 81 | format!("{}/{}/{}/{}{}", host, api_prefix, version, plural, query) 82 | } else { 83 | format!( 84 | "{}/{}/{}/namespaces/{}/{}{}", 85 | host, 86 | api_prefix, 87 | version, 88 | namespace.named(), 89 | plural, 90 | query 91 | ) 92 | } 93 | } 94 | 95 | #[cfg(test)] 96 | mod test { 97 | use k8_metadata_client::ApplyOptions; 98 | use k8_types::core::pod::PodSpec; 99 | use k8_types::{Crd, CrdNames, DEFAULT_NS}; 100 | 101 | use super::{prefix_uri, item_uri}; 102 | use super::ListOptions; 103 | 104 | const G1: Crd = Crd { 105 | group: "test.com", 106 | version: "v1", 107 | names: CrdNames { 108 | kind: "Item", 109 | plural: "items", 110 | singular: "item", 111 | }, 112 | }; 113 | 114 | const C1: Crd = Crd { 115 | group: "core", 116 | version: "v1", 117 | names: CrdNames { 118 | kind: "Item", 119 | plural: "items", 120 | singular: "item", 121 | }, 122 | }; 123 | 124 | #[test] 125 | fn test_api_prefix_group() { 126 | let uri = prefix_uri(&G1, "https://localhost", DEFAULT_NS, None); 127 | assert_eq!( 128 | uri, 129 | "https://localhost/apis/test.com/v1/namespaces/default/items" 130 | ); 131 | } 132 | 133 | #[test] 134 | fn test_api_prefix_core() { 135 | let uri = prefix_uri(&C1, "https://localhost", DEFAULT_NS, None); 136 | assert_eq!(uri, "https://localhost/api/v1/namespaces/default/items"); 137 | } 138 | 139 | #[test] 140 | fn test_api_prefix_watch() { 141 | let opt = ListOptions { 142 | watch: Some(true), 143 | ..Default::default() 144 | }; 145 | let uri = prefix_uri(&C1, "https://localhost", DEFAULT_NS, Some(opt)); 146 | assert_eq!( 147 | uri, 148 | "https://localhost/api/v1/namespaces/default/items?watch=true" 149 | ); 150 | } 151 | 152 | #[test] 153 | fn test_list_query() { 154 | let opt = ListOptions { 155 | pretty: Some(true), 156 | watch: Some(true), 157 | ..Default::default() 158 | }; 159 | 160 | let qs = serde_qs::to_string(&opt).unwrap(); 161 | assert_eq!(qs, "pretty=true&watch=true") 162 | } 163 | 164 | #[test] 165 | fn support_item_uri_params() { 166 | let patch_params = ApplyOptions { 167 | force: true, 168 | field_manager: Some(String::from("fluvio")), 169 | }; 170 | let params = serde_qs::to_string(&patch_params).unwrap(); 171 | let uri = item_uri::( 172 | "http://localhost:8001", 173 | "test", 174 | DEFAULT_NS, 175 | Some("/status"), 176 | Some(¶ms), 177 | ); 178 | assert_eq!( 179 | uri.unwrap().to_string(), 180 | "http://localhost:8001/api/v1/namespaces/default/pods/test/status?force=true&fieldManager=fluvio" 181 | ); 182 | } 183 | } 184 | 185 | /* 186 | #[cfg(test)] 187 | mod test { 188 | 189 | use k8_obj_metadata::item_uri; 190 | use k8_obj_metadata::items_uri; 191 | use k8_obj_metadata::DEFAULT_NS; 192 | use crate::pod::PodSpec; 193 | 194 | #[test] 195 | fn test_pod_item_uri() { 196 | let uri = item_uri::("https://localhost", "test", DEFAULT_NS, None); 197 | assert_eq!( 198 | uri, 199 | "https://localhost/api/v1/namespaces/default/pods/test" 200 | ); 201 | } 202 | 203 | #[test] 204 | fn test_pod_items_uri() { 205 | let uri = items_uri::("https://localhost", DEFAULT_NS, None); 206 | assert_eq!( 207 | uri, 208 | "https://localhost/api/v1/namespaces/default/pods" 209 | ); 210 | } 211 | 212 | 213 | } 214 | 215 | */ 216 | -------------------------------------------------------------------------------- /src/k8-client/tests/canary.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "k8")] 2 | mod canary_test { 3 | 4 | use anyhow::Result; 5 | use tracing::debug; 6 | use tracing::info; 7 | 8 | use fluvio_future::test_async; 9 | use k8_client::K8Client; 10 | use k8_metadata_client::MetadataClient; 11 | use k8_metadata_client::NameSpace; 12 | use k8_types::core::service::ServiceSpec; 13 | use k8_types::K8Obj; 14 | 15 | // get services to find kubernetes api 16 | #[test_async] 17 | async fn test_client_server_version() -> Result<()> { 18 | let client = K8Client::try_default().expect("cluster could not be configured"); 19 | let version = client.server_version().await?; 20 | info!( 21 | "server version reported: {}.{}", 22 | version.major, version.minor, 23 | ); 24 | Ok(()) 25 | } 26 | 27 | // get services to find kubernetes api 28 | #[test_async] 29 | async fn test_client_get_services() -> Result<()> { 30 | let client = K8Client::try_default().expect("cluster could not be configured"); 31 | let services = client.retrieve_items::("default").await?; 32 | debug!("service: {} has been retrieved", services.items.len()); 33 | 34 | let kubernetes_service = services 35 | .items 36 | .iter() 37 | .find(|i| i.metadata.name == "kubernetes"); 38 | assert!(kubernetes_service.is_some()); 39 | Ok(()) 40 | } 41 | 42 | use k8_types::core::secret::SecretSpec; 43 | 44 | #[test_async] 45 | async fn test_client_secrets() -> Result<()> { 46 | let client = K8Client::try_default().expect("cluster could not be configured"); 47 | let secrets = client 48 | .retrieve_items::(NameSpace::All) 49 | .await 50 | .expect("item retrieve"); 51 | 52 | let system_secrets: Vec> = secrets 53 | .items 54 | .into_iter() 55 | .filter(|s| s.metadata.namespace == "kube-system") 56 | .collect(); 57 | 58 | info!( 59 | "system secrets: {} has been retrieved", 60 | system_secrets.len() 61 | ); 62 | 63 | assert!(system_secrets.len() > 20); 64 | 65 | Ok(()) 66 | } 67 | 68 | #[test_async] 69 | async fn test_pods() -> Result<()> { 70 | use k8_types::core::pod::PodSpec; 71 | 72 | let client = K8Client::try_default().expect("cluster could not be configured"); 73 | let pod_items = client 74 | .retrieve_items::("default") 75 | .await 76 | .expect("pods should exist"); 77 | 78 | for pod in pod_items.items { 79 | println!("pod: {:#?}", pod); 80 | } 81 | 82 | Ok(()) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/k8-client/tests/error.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "k8")] 2 | mod integration_tests { 3 | 4 | use std::collections::HashMap; 5 | use std::time::Duration; 6 | 7 | use anyhow::Result; 8 | use rand::distributions::Alphanumeric; 9 | use rand::{thread_rng, Rng}; 10 | use tracing::debug; 11 | 12 | use fluvio_future::test_async; 13 | use fluvio_future::timer::sleep; 14 | use k8_client::http::status::StatusCode; 15 | use k8_client::K8Client; 16 | use k8_metadata_client::MetadataClient; 17 | use k8_types::core::service::{LoadBalancerType, ServicePort}; 18 | use k8_types::core::service::{LoadBalancerIngress, ServiceSpec}; 19 | use k8_types::{InputK8Obj, InputObjectMeta, Spec, MetaStatus}; 20 | 21 | const SPU_DEFAULT_NAME: &str = "spu"; 22 | const DELAY: Duration = Duration::from_millis(100); 23 | 24 | fn create_client() -> K8Client { 25 | K8Client::try_default().expect("cluster not initialized") 26 | } 27 | 28 | fn new_service() -> InputK8Obj { 29 | let rng = thread_rng(); 30 | let rname: String = rng 31 | .sample_iter(&Alphanumeric) 32 | .map(char::from) 33 | .take(5) 34 | .collect(); 35 | let name = format!("test{}", rname); 36 | 37 | let mut labels = HashMap::new(); 38 | labels.insert("app".to_owned(), SPU_DEFAULT_NAME.to_owned()); 39 | let mut selector = HashMap::new(); 40 | selector.insert("app".to_owned(), SPU_DEFAULT_NAME.to_owned()); 41 | 42 | let service_spec = ServiceSpec { 43 | ports: vec![ServicePort { 44 | port: 9092, 45 | ..Default::default() 46 | }], 47 | selector: Some(selector), 48 | r#type: Some(LoadBalancerType::LoadBalancer), 49 | ..Default::default() 50 | }; 51 | 52 | let new_item: InputK8Obj = InputK8Obj { 53 | api_version: ServiceSpec::api_version(), 54 | kind: ServiceSpec::kind(), 55 | metadata: InputObjectMeta { 56 | name: name.to_lowercase(), 57 | labels, 58 | namespace: "default".to_owned(), 59 | ..Default::default() 60 | }, 61 | spec: service_spec, 62 | ..Default::default() 63 | }; 64 | 65 | new_item 66 | } 67 | 68 | #[test_async] 69 | async fn test_object_conflict() -> Result<()> { 70 | let new_item = new_service(); 71 | debug!("creating new service: {:#?}", &new_item); 72 | let client = create_client(); 73 | let created_item = client 74 | .create_item::(new_item) 75 | .await 76 | .expect("service should be created"); 77 | 78 | sleep(DELAY).await; 79 | 80 | let item = client 81 | .retrieve_item::(&created_item.metadata) 82 | .await 83 | .expect("retrieval") 84 | .expect("service should exist"); 85 | 86 | let initial_version = item.metadata.resource_version.clone(); 87 | debug!("client resource_version: {}", initial_version); 88 | 89 | // update status 90 | let mut new_status = item.status.clone(); 91 | let ingress = LoadBalancerIngress { 92 | ip: Some("0.0.0.0".to_string()), 93 | ip_mode: Some("VIP".to_string()), 94 | ..Default::default() 95 | }; 96 | new_status.load_balancer.ingress.push(ingress); 97 | 98 | let mut new_status2 = item.status.clone(); 99 | let ingress = LoadBalancerIngress { 100 | ip: Some("1.1.1.1".to_string()), 101 | ip_mode: Some("Proxy".to_string()), 102 | ..Default::default() 103 | }; 104 | new_status2.load_balancer.ingress.push(ingress); 105 | 106 | // manually set ip external ip 107 | 108 | let status_update1 = item.as_status_update(new_status); 109 | let status_update2 = item.as_status_update(new_status2); 110 | 111 | let updated_item = client.update_status(&status_update1).await.expect("update"); 112 | 113 | let updated_version = updated_item.metadata.resource_version.clone(); 114 | 115 | debug!("updated resource_version: {}", updated_version); 116 | 117 | assert_ne!(updated_version, initial_version); 118 | 119 | // do another update status which leads to conflict. 120 | let err = client 121 | .update_status(&status_update2) 122 | .await 123 | .expect_err("update"); 124 | if let Some(status) = err.downcast_ref::() { 125 | assert_eq!(status.code, Some(StatusCode::CONFLICT.as_u16())) 126 | } else { 127 | panic!("expecting conflict error"); 128 | } 129 | 130 | // clean up 131 | let input_metadata: InputObjectMeta = updated_item.metadata.into(); 132 | client 133 | .delete_item::(&input_metadata) 134 | .await 135 | .expect("delete should work"); 136 | 137 | Ok(()) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/k8-client/tests/job.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "k8")] 2 | mod integration_tests { 3 | 4 | use fluvio_future::test_async; 5 | 6 | use anyhow::Result; 7 | use k8_client::K8Client; 8 | use k8_metadata_client::MetadataClient; 9 | use k8_types::{ 10 | InputK8Obj, InputObjectMeta, TemplateSpec, 11 | batch::job::JobSpec, 12 | core::pod::{ContainerSpec, PodSpec, PodRestartPolicy}, 13 | TemplateMeta, 14 | }; 15 | 16 | const NS: &str = "default"; 17 | const JOB_NAME: &str = "job-test-name"; 18 | 19 | fn create_client() -> K8Client { 20 | K8Client::try_default().expect("cluster not initialized") 21 | } 22 | 23 | async fn create_job(client: &K8Client) { 24 | let input_meta = InputObjectMeta { 25 | name: JOB_NAME.to_string(), 26 | namespace: NS.to_string(), 27 | ..Default::default() 28 | }; 29 | 30 | let job = JobSpec { 31 | template: TemplateSpec { 32 | spec: PodSpec { 33 | containers: vec![ContainerSpec { 34 | name: JOB_NAME.to_string(), 35 | image: Some("busybox".to_string()), 36 | command: vec![ 37 | "sh".to_string(), 38 | "-c".to_string(), 39 | "echo \"Hello, Kubernetes!\"".to_string(), 40 | ], 41 | ..Default::default() 42 | }], 43 | restart_policy: Some(PodRestartPolicy::Never), 44 | ..Default::default() 45 | }, 46 | metadata: Some(TemplateMeta { 47 | name: Some("busybox-pod-name".to_string()), 48 | annotations: vec![("some-key".to_string(), "some-value".to_string())] 49 | .into_iter() 50 | .collect(), 51 | ..Default::default() 52 | }), 53 | }, 54 | active_deadline_seconds: Some(60), 55 | backoff_limit: Some(1), 56 | ..Default::default() 57 | }; 58 | 59 | let input = InputK8Obj::new(job, input_meta); 60 | 61 | client.apply(input).await.expect("failed creating job"); 62 | } 63 | 64 | async fn check_job(client: &K8Client) { 65 | let job_items = client 66 | .retrieve_items::(NS) 67 | .await 68 | .expect("jobs should exist"); 69 | 70 | assert_eq!(job_items.items.len(), 1); 71 | for job in job_items.items { 72 | assert_eq!(job.metadata.name, JOB_NAME); 73 | assert_eq!( 74 | job.spec 75 | .template 76 | .metadata 77 | .expect("expected meta") 78 | .annotations, 79 | vec![("some-key".to_string(), "some-value".to_string())] 80 | .into_iter() 81 | .collect(), 82 | ); 83 | } 84 | } 85 | #[test_async] 86 | async fn test_job_created() -> Result<()> { 87 | let client = create_client(); 88 | 89 | create_job(&client).await; 90 | check_job(&client).await; 91 | 92 | Ok(()) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/k8-client/tests/replace.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "k8")] 2 | mod integration_tests { 3 | 4 | use std::collections::HashMap; 5 | 6 | use anyhow::Result; 7 | use rand::distributions::Alphanumeric; 8 | use rand::{thread_rng, Rng}; 9 | use tracing::debug; 10 | 11 | use fluvio_future::test_async; 12 | use k8_client::K8Client; 13 | use k8_metadata_client::MetadataClient; 14 | use k8_types::core::service::ServicePort; 15 | use k8_types::core::service::ServiceSpec; 16 | use k8_types::{InputK8Obj, InputObjectMeta, Spec}; 17 | 18 | const SPU_DEFAULT_NAME: &str = "spu"; 19 | 20 | fn create_client() -> K8Client { 21 | K8Client::try_default().expect("cluster not initialized") 22 | } 23 | 24 | fn new_service() -> InputK8Obj { 25 | let rng = thread_rng(); 26 | let rname: String = rng 27 | .sample_iter(&Alphanumeric) 28 | .map(char::from) 29 | .take(5) 30 | .collect(); 31 | let name = format!("test{}", rname); 32 | 33 | let mut labels = HashMap::new(); 34 | labels.insert("app".to_owned(), SPU_DEFAULT_NAME.to_owned()); 35 | let mut selector = HashMap::new(); 36 | selector.insert("app".to_owned(), SPU_DEFAULT_NAME.to_owned()); 37 | 38 | let service_spec = ServiceSpec { 39 | cluster_ip: "None".to_owned(), 40 | ports: vec![ServicePort { 41 | port: 9092, 42 | ..Default::default() 43 | }], 44 | selector: Some(selector), 45 | ..Default::default() 46 | }; 47 | 48 | let new_item: InputK8Obj = InputK8Obj { 49 | api_version: ServiceSpec::api_version(), 50 | kind: ServiceSpec::kind(), 51 | metadata: InputObjectMeta { 52 | name: name.to_lowercase(), 53 | labels, 54 | namespace: "default".to_owned(), 55 | ..Default::default() 56 | }, 57 | spec: service_spec, 58 | ..Default::default() 59 | }; 60 | 61 | new_item 62 | } 63 | 64 | #[test_async] 65 | async fn test_object_replace() -> Result<()> { 66 | let new_item = new_service(); 67 | debug!("creating new service: {:#?}", &new_item); 68 | let client = create_client(); 69 | let created_item = client 70 | .create_item::(new_item) 71 | .await 72 | .expect("service should be created"); 73 | 74 | let initial_version = created_item.metadata.resource_version.clone(); 75 | debug!("client resource_version: {}", initial_version); 76 | 77 | assert!(created_item.metadata.labels.contains_key("app")); 78 | 79 | let mut update_item = created_item.as_update(); 80 | update_item.metadata.labels.clear(); 81 | 82 | client 83 | .replace_item(update_item.clone()) 84 | .await 85 | .expect("replace"); 86 | 87 | let updated_service = client 88 | .retrieve_item::(&update_item.metadata) 89 | .await 90 | .expect("retrieval") 91 | .expect("not found"); 92 | 93 | assert!(updated_service.metadata.labels.is_empty()); 94 | 95 | // clean up 96 | let input_metadata: InputObjectMeta = created_item.metadata.into(); 97 | client 98 | .delete_item::(&input_metadata) 99 | .await 100 | .expect("delete should work"); 101 | 102 | Ok(()) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/k8-client/tests/service.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "k8")] 2 | mod integration_tests { 3 | 4 | use std::collections::HashMap; 5 | use std::time::Duration; 6 | 7 | use anyhow::Result; 8 | use futures_util::future::join; 9 | use futures_util::StreamExt; 10 | use once_cell::sync::Lazy; 11 | use rand::distributions::Alphanumeric; 12 | use rand::{thread_rng, Rng}; 13 | use tracing::debug; 14 | use tracing::trace; 15 | 16 | use fluvio_future::test_async; 17 | use fluvio_future::timer::sleep; 18 | 19 | use k8_client::K8Client; 20 | use k8_metadata_client::{ApplyResult, MetadataClient}; 21 | use k8_types::core::service::{ServicePort, ServiceSpec}; 22 | use k8_types::{InputK8Obj, InputObjectMeta, K8Watch, Spec}; 23 | 24 | const SPU_DEFAULT_NAME: &str = "spu"; 25 | const PORT: u16 = 9002; 26 | const ITER: u16 = 10; 27 | const NS: &str = "default"; 28 | 29 | const DELAY: Duration = Duration::from_millis(100); 30 | 31 | fn create_client() -> K8Client { 32 | K8Client::try_default().expect("cluster not initialized") 33 | } 34 | 35 | static PREFIX: Lazy = Lazy::new(|| { 36 | let rng = thread_rng(); 37 | rng.sample_iter(&Alphanumeric) 38 | .map(char::from) 39 | .take(5) 40 | .collect() 41 | }); 42 | 43 | fn new_service(item_id: u16) -> InputK8Obj { 44 | let name = format!("testservice{}{}", item_id, *PREFIX); 45 | new_service_with_name(name) 46 | } 47 | 48 | /// create new service item 49 | fn new_service_with_name(name: String) -> InputK8Obj { 50 | let mut labels = HashMap::new(); 51 | labels.insert("app".to_owned(), SPU_DEFAULT_NAME.to_owned()); 52 | let mut selector = HashMap::new(); 53 | selector.insert("app".to_owned(), SPU_DEFAULT_NAME.to_owned()); 54 | 55 | let service_spec = ServiceSpec { 56 | cluster_ip: "None".to_owned(), 57 | ports: vec![ServicePort { 58 | port: PORT, 59 | ..Default::default() 60 | }], 61 | selector: Some(selector), 62 | ..Default::default() 63 | }; 64 | 65 | InputK8Obj { 66 | api_version: ServiceSpec::api_version(), 67 | kind: ServiceSpec::kind(), 68 | metadata: InputObjectMeta { 69 | name: name.to_lowercase(), 70 | labels, 71 | namespace: "default".to_owned(), 72 | ..Default::default() 73 | }, 74 | spec: service_spec, 75 | ..Default::default() 76 | } 77 | } 78 | 79 | /// create, update and delete random services 80 | async fn generate_services_data(client: &K8Client) { 81 | // wait to allow test to retrieve first in order to version 82 | sleep(DELAY).await; 83 | 84 | // go thru create, update spec and delete 85 | for i in 0..ITER { 86 | let new_item = new_service(i); 87 | 88 | debug!("creating service: {}", i); 89 | let created_item = client 90 | .create_item::(new_item) 91 | .await 92 | .expect("service should be created"); 93 | 94 | sleep(DELAY).await; 95 | 96 | let mut update_item = created_item.as_input(); 97 | update_item.spec = ServiceSpec { 98 | cluster_ip: "None".to_owned(), 99 | ports: vec![ServicePort { 100 | port: PORT, 101 | name: Some("t".to_owned()), 102 | ..Default::default() 103 | }], 104 | ..Default::default() 105 | }; 106 | 107 | // apply new changes 108 | debug!("updating service: {}", i); 109 | let updates = client.apply(update_item).await.expect("apply"); 110 | 111 | let updated_item = match updates { 112 | ApplyResult::Patched(item) => item, 113 | _ => { 114 | panic!("apply does not result in patch"); 115 | } 116 | }; 117 | trace!("updated item: {:#?}", updated_item); 118 | 119 | sleep(DELAY).await; 120 | 121 | // update only metadata 122 | let mut update_item_2 = updated_item.as_input(); 123 | update_item_2.metadata.annotations.insert( 124 | "test-annotations".to_owned(), 125 | "test-annotations-value".to_owned(), 126 | ); 127 | // apply new changes 128 | debug!("updating service meta: {}", i); 129 | let updates_2 = client.apply(update_item_2).await.expect("apply"); 130 | trace!("updated item meta: {:#?}", updates_2); 131 | 132 | let updated_item_2 = match updates_2 { 133 | ApplyResult::Patched(item) => item, 134 | _ => { 135 | panic!("apply does not result in patch"); 136 | } 137 | }; 138 | 139 | debug!("deleting service: {}", i); 140 | client 141 | .delete_item::(&updated_item_2.metadata) 142 | .await 143 | .expect("delete should work"); 144 | 145 | sleep(DELAY).await; 146 | } 147 | } 148 | 149 | // verify client 150 | async fn verify_client(client: &K8Client) { 151 | // there should be only 1 service (kubernetes) 152 | let services = client 153 | .retrieve_items::(NS) 154 | .await 155 | .expect("services"); 156 | // assert_eq!(services.items.len(), 1); 157 | 158 | let version = services.metadata.resource_version.clone(); 159 | debug!("using version: {} ", version); 160 | 161 | let mut service_streams = client.watch_stream_since::(NS, Some(version)); 162 | 163 | for i in 0..ITER { 164 | debug!("checking service: {}", i); 165 | let mut add_events = service_streams 166 | .next() 167 | .await 168 | .expect("events") 169 | .expect("events"); 170 | assert_eq!(add_events.len(), 1); 171 | let add_event = add_events.pop().unwrap(); 172 | // debug!("events:{:#?}",events); 173 | assert!(matches!(add_event.expect("ok"), K8Watch::ADDED(_))); 174 | 175 | let mut update_events = service_streams 176 | .next() 177 | .await 178 | .expect("events") 179 | .expect("events"); 180 | trace!("update_events {:?}", update_events); 181 | assert_eq!(update_events.len(), 1); 182 | let update_event = update_events.pop().unwrap(); 183 | assert!(matches!(update_event.expect("ok"), K8Watch::MODIFIED(_))); 184 | 185 | let mut update_2_events = service_streams 186 | .next() 187 | .await 188 | .expect("events") 189 | .expect("events"); 190 | trace!("update_events {:?}", update_2_events); 191 | assert_eq!(update_2_events.len(), 1); 192 | let update_2_event = update_2_events.pop().unwrap(); 193 | assert!(matches!(update_2_event.expect("ok"), K8Watch::MODIFIED(_))); 194 | 195 | let mut delete_events = service_streams 196 | .next() 197 | .await 198 | .expect("events") 199 | .expect("events"); 200 | 201 | trace!("delete_events {:?}", delete_events); 202 | assert_eq!(delete_events.len(), 1); 203 | let delete_event = delete_events.pop().unwrap(); 204 | assert!(matches!(delete_event.expect("ok"), K8Watch::DELETED(_))); 205 | } 206 | } 207 | 208 | #[test_async] 209 | async fn test_service_changes() -> Result<()> { 210 | let client = create_client(); 211 | 212 | join(generate_services_data(&client), verify_client(&client)).await; 213 | 214 | Ok(()) 215 | } 216 | 217 | /* 218 | TODO: fix this test 219 | 220 | #[test_async] 221 | async fn test_service_delete_with_option() -> Result<()> { 222 | use k8_obj_core::metadata::options::{ DeleteOptions, PropogationPolicy }; 223 | 224 | let client = create_client(); 225 | 226 | let new_item = new_service_with_name("testservice_delete".to_owned()); 227 | // new_item.metadata.finalizers = vec!["my-finalizer.example.com".to_owned()]; 228 | 229 | 230 | let created_item = client 231 | .create_item::(new_item) 232 | .await 233 | .expect("service should be created"); 234 | 235 | 236 | 237 | client 238 | .delete_item_with_option::(&created_item.metadata,Some(DeleteOptions { 239 | propagation_policy: Some(PropogationPolicy::Foreground), 240 | ..Default::default() 241 | })) 242 | .await 243 | .expect("delete should work"); 244 | 245 | 246 | 247 | assert!(true); 248 | 249 | Ok(()) 250 | 251 | } 252 | */ 253 | } 254 | -------------------------------------------------------------------------------- /src/k8-config/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "k8-config" 3 | version = "2.3.0" 4 | authors = ["Fluvio Contributors "] 5 | edition = "2021" 6 | description = "Read Kubernetes config" 7 | repository = "https://github.com/infinyon/k8-api" 8 | license = "Apache-2.0" 9 | 10 | [features] 11 | context = ["tera"] 12 | 13 | [dependencies] 14 | tracing = "0.1.19" 15 | dirs = "5.0.1" 16 | serde = { version ="1.0.136", features = ['derive'] } 17 | serde_yaml = { workspace = true } 18 | serde_json = "1.0.57" 19 | tera = { version = "1.19.1", optional = true } 20 | hostfile = "0.3.0" 21 | thiserror = "1.0.20" 22 | 23 | [dev-dependencies] 24 | fluvio-future = { workspace = true, features = ["subscriber"]} 25 | 26 | 27 | [[example]] 28 | name = "kubeconfig_read" 29 | -------------------------------------------------------------------------------- /src/k8-config/LICENSE-APACHE: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /src/k8-config/README.md: -------------------------------------------------------------------------------- 1 | # Kubernetes Configuration Utility 2 | 3 | This crate is used to read K8 Configuration. 4 | 5 | ## License 6 | 7 | This project is licensed under the [Apache license](LICENSE-APACHE). 8 | 9 | ### Contribution 10 | 11 | Unless you explicitly state otherwise, any contribution intentionally submitted 12 | for inclusion in Fluvio by you, shall be licensed as Apache, without any additional 13 | terms or conditions. 14 | -------------------------------------------------------------------------------- /src/k8-config/data/k8config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | clusters: 3 | - cluster: 4 | certificate-authority: /Users/test/.minikube/ca.crt 5 | server: https://192.168.0.0:8443 6 | name: minikube 7 | contexts: 8 | - context: 9 | cluster: minikube 10 | namespace: flv 11 | user: minikube 12 | name: flv 13 | - context: 14 | cluster: minikube 15 | user: minikube 16 | name: minikube 17 | current-context: flv 18 | kind: Config 19 | preferences: {} 20 | users: 21 | - name: minikube 22 | user: 23 | client-certificate: /Users/test/.minikube/client.crt 24 | client-key: /Users/test/.minikube/client.key -------------------------------------------------------------------------------- /src/k8-config/examples/kubeconfig_read.rs: -------------------------------------------------------------------------------- 1 | use k8_config::KubeConfig; 2 | 3 | const KUBECONFIG: &str = "KUBECONFIG"; 4 | 5 | fn main() { 6 | // Read the KUBECONFIG env var for a path, or attempt to open $HOME/.kube/config 7 | // Parse then print config 8 | 9 | fluvio_future::subscriber::init_tracer(None); 10 | let config = std::env::var(KUBECONFIG) 11 | .map_or(KubeConfig::from_home(), KubeConfig::from_file) 12 | .expect("Load failed"); 13 | 14 | println!("{config:#?}") 15 | } 16 | -------------------------------------------------------------------------------- /src/k8-config/src/error.rs: -------------------------------------------------------------------------------- 1 | use serde_yaml::Error as SerdeYamlError; 2 | use std::io::Error as IoError; 3 | use thiserror::Error; 4 | 5 | #[derive(Error, Debug)] 6 | pub enum ConfigError { 7 | #[error("IO error: {0}")] 8 | IoError(#[from] IoError), 9 | #[error("Yaml error: {0}")] 10 | SerdeError(#[from] SerdeYamlError), 11 | #[error("No active Kubernetes context")] 12 | NoCurrentContext, 13 | #[error("Unknown error: {0}")] 14 | Other(String), 15 | } 16 | -------------------------------------------------------------------------------- /src/k8-config/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | mod error; 3 | mod pod; 4 | 5 | #[cfg(feature = "context")] 6 | pub mod context; 7 | 8 | pub use config::{KubeConfig, ClusterDetail, Context, Cluster, ContextDetail, User, UserDetail}; 9 | pub use error::ConfigError; 10 | pub use pod::PodConfig; 11 | 12 | pub use config::{AuthProviderDetail, GcpAuthProviderConfig}; 13 | 14 | use tracing::debug; 15 | 16 | const KUBECONFIG: &str = "KUBECONFIG"; 17 | 18 | #[derive(Debug)] 19 | pub struct KubeContext { 20 | pub namespace: String, 21 | pub api_path: String, 22 | pub config: KubeConfig, 23 | } 24 | 25 | #[derive(Debug)] 26 | pub enum K8Config { 27 | Pod(PodConfig), 28 | KubeConfig(KubeContext), 29 | } 30 | 31 | impl Default for K8Config { 32 | fn default() -> Self { 33 | Self::Pod(PodConfig::default()) 34 | } 35 | } 36 | 37 | impl K8Config { 38 | pub fn load() -> Result { 39 | if let Some(pod_config) = PodConfig::load() { 40 | debug!("found pod config: {:#?}", pod_config); 41 | Ok(K8Config::Pod(pod_config)) 42 | } else { 43 | debug!("no pod config is found. trying to read kubeconfig"); 44 | let config = 45 | std::env::var(KUBECONFIG).map_or(KubeConfig::from_home(), KubeConfig::from_file)?; 46 | debug!("kube config: {:#?}", config); 47 | // check if we have current cluster 48 | 49 | if let Some(current_cluster) = config.current_cluster() { 50 | let ctx = config 51 | .current_context() 52 | .expect("current context should exists"); 53 | Ok(K8Config::KubeConfig(KubeContext { 54 | namespace: ctx.context.namespace().to_owned(), 55 | api_path: current_cluster.cluster.server.clone(), 56 | config, 57 | })) 58 | } else { 59 | Err(ConfigError::NoCurrentContext) 60 | } 61 | } 62 | } 63 | 64 | pub fn api_path(&self) -> &str { 65 | match self { 66 | Self::Pod(pod) => pod.api_path(), 67 | Self::KubeConfig(config) => &config.api_path, 68 | } 69 | } 70 | 71 | pub fn namespace(&self) -> &str { 72 | match self { 73 | Self::Pod(pod) => &pod.namespace, 74 | Self::KubeConfig(config) => &config.namespace, 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/k8-config/src/pod.rs: -------------------------------------------------------------------------------- 1 | use std::fs::read_to_string; 2 | use std::path::Path; 3 | 4 | use tracing::debug; 5 | use tracing::error; 6 | use tracing::trace; 7 | 8 | const BASE_DIR: &str = "/var/run/secrets/kubernetes.io/serviceaccount"; 9 | const API_SERVER: &str = "https://kubernetes.default.svc"; 10 | 11 | /// Configuration as Pod 12 | #[derive(Debug, Default, Clone)] 13 | pub struct PodConfig { 14 | pub namespace: String, 15 | pub token: String, 16 | } 17 | 18 | impl PodConfig { 19 | pub fn load() -> Option { 20 | // first try to see if this base dir account exists, otherwise return non 21 | let path = Path::new(BASE_DIR); 22 | if !path.exists() { 23 | debug!( 24 | "pod config dir: {} is not found, skipping pod config", 25 | BASE_DIR 26 | ); 27 | return None; 28 | } 29 | 30 | let namespace = read_file("namespace")?; 31 | let token = read_file("token")?; 32 | 33 | Some(Self { namespace, token }) 34 | } 35 | 36 | pub fn api_path(&self) -> &'static str { 37 | API_SERVER 38 | } 39 | 40 | /// path to CA certificate 41 | pub fn ca_path(&self) -> String { 42 | format!("{}/{}", BASE_DIR, "ca.crt") 43 | } 44 | } 45 | 46 | // read file 47 | fn read_file(name: &str) -> Option { 48 | let full_path = format!("{}/{}", BASE_DIR, name); 49 | match read_to_string(&full_path) { 50 | Ok(value) => Some(value), 51 | Err(err) => { 52 | error!("no {} found as pod in {}", name, full_path); 53 | trace!("unable to read pod: {} value: {}", name, err); 54 | None 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/k8-ctx-util/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "k8-ctx-util" 3 | edition = "2021" 4 | version = "0.1.0" 5 | authors = ["Fluvio Contributors "] 6 | description = "utility to create dns based kube config context" 7 | repository = "https://github.com/infinyon/k8-api" 8 | license = "Apache-2.0" 9 | 10 | [dependencies] 11 | k8-config = { path = "../k8-config", features = ["context"]} -------------------------------------------------------------------------------- /src/k8-ctx-util/src/main.rs: -------------------------------------------------------------------------------- 1 | use k8_config::context::MinikubeContext; 2 | use k8_config::ConfigError; 3 | 4 | /// Performs following 5 | /// add minikube IP address to /etc/host 6 | /// create new kubectl cluster and context which uses minikube name 7 | fn main() { 8 | if let Err(e) = run() { 9 | println!("{}", e); 10 | } 11 | } 12 | 13 | fn run() -> Result<(), ConfigError> { 14 | let context = MinikubeContext::try_from_system()?; 15 | context.save()?; 16 | Ok(()) 17 | } 18 | -------------------------------------------------------------------------------- /src/k8-diff/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "k8-diff" 3 | edition = "2021" 4 | version = "0.1.2" 5 | authors = ["Fluvio Contributors "] 6 | description = "Used for computing diff between Kubernetes objects" 7 | repository = "https://github.com/infinyon/k8-api" 8 | license = "Apache-2.0" 9 | 10 | [dependencies] 11 | serde = "1.0.136" 12 | serde_json = "1.0.60" 13 | 14 | [dev-dependencies] 15 | serde = { version ="1.0.136", features = ['derive'] } -------------------------------------------------------------------------------- /src/k8-diff/README.md: -------------------------------------------------------------------------------- 1 | # kf-diff 2 | 3 | Used for computing diff between Kubernetes objects 4 | 5 | ## License 6 | 7 | This project is licensed under the [Apache license](LICENSE-APACHE). 8 | 9 | ### Contribution 10 | 11 | Unless you explicitly state otherwise, any contribution intentionally submitted 12 | for inclusion in Fluvio by you, shall be licensed as Apache, without any additional 13 | terms or conditions. 14 | -------------------------------------------------------------------------------- /src/k8-diff/k8-dderive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "k8-dderive" 3 | version = "0.2.1" 4 | edition = "2021" 5 | authors = ["fluvio.io"] 6 | 7 | [lib] 8 | proc-macro = true 9 | 10 | [dependencies] 11 | proc-macro2 = "0.4.24" 12 | quote = "0.6.10" 13 | syn = "0.15.21" 14 | log = "0.4.8" 15 | -------------------------------------------------------------------------------- /src/k8-diff/k8-dderive/src/diff.rs: -------------------------------------------------------------------------------- 1 | use quote::quote; 2 | use proc_macro2::TokenStream; 3 | use syn::Data; 4 | use syn::DeriveInput; 5 | use syn::Fields; 6 | 7 | 8 | 9 | pub fn geneate_diff_trait(input: &DeriveInput) -> TokenStream { 10 | let name = &input.ident; 11 | let decoded_field_tokens = decode_fields(&input.data); 12 | 13 | quote! { 14 | 15 | impl <'a>k8_diff::Changes<'a> for #name { 16 | 17 | fn diff(&self, new: &'a Self) -> k8_diff::Diff { 18 | 19 | let mut s_diff = k8_diff::DiffStruct::new(); 20 | 21 | #decoded_field_tokens 22 | 23 | if s_diff.no_change() { 24 | return k8_diff::Diff::None 25 | } 26 | 27 | k8_diff::Diff::Change(k8_diff::DiffValue::Struct(s_diff)) 28 | } 29 | } 30 | 31 | } 32 | } 33 | 34 | fn decode_fields(data: &Data) -> TokenStream { 35 | match *data { 36 | Data::Struct(ref data) => { 37 | match data.fields { 38 | Fields::Named(ref fields) => { 39 | let recurse = fields.named.iter().map(|f| { 40 | let fname = &f.ident; 41 | 42 | quote! { 43 | // s_diff.insert("replicas".to_owned(), self.replicas.diff(&new.replicas)); 44 | s_diff.insert(stringify!(#fname).to_owned(), self.#fname.diff(&new.#fname)); 45 | 46 | } 47 | 48 | }); 49 | 50 | quote! { 51 | #(#recurse)* 52 | } 53 | } 54 | _ => unimplemented!(), 55 | } 56 | } 57 | _ => unimplemented!(), 58 | } 59 | } 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/k8-diff/k8-dderive/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | mod diff; 4 | 5 | use proc_macro::TokenStream as TokenStream1; 6 | use syn::DeriveInput; 7 | 8 | 9 | #[proc_macro_derive(Difference)] 10 | pub fn diff(input: TokenStream1) -> TokenStream1 { 11 | 12 | // Parse the string representation 13 | let ast: DeriveInput = syn::parse(input).unwrap(); 14 | 15 | let expanded = diff::geneate_diff_trait(&ast); 16 | expanded.into() 17 | } 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/k8-diff/src/json/diff.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | 3 | use super::JsonDiff; 4 | use super::PatchObject; 5 | use crate::Changes; 6 | use crate::Diff; 7 | use crate::DiffError; 8 | 9 | impl Changes for Value { 10 | type Replace = Value; 11 | type Patch = PatchObject; 12 | 13 | fn diff(&self, new: &Self) -> Result { 14 | if *self == *new { 15 | return Ok(Diff::None); 16 | } 17 | match self { 18 | Value::Null => Ok(Diff::Replace(new.clone())), 19 | _ => { 20 | match new { 21 | Value::Null => Ok(Diff::Replace(Value::Null)), 22 | Value::Bool(ref _val) => Ok(Diff::Replace(new.clone())), // for now, we only support replace 23 | Value::Number(ref _val) => Ok(Diff::Replace(new.clone())), 24 | Value::String(ref _val) => Ok(Diff::Replace(new.clone())), 25 | Value::Array(ref _val) => Ok(Diff::Replace(new.clone())), 26 | Value::Object(ref new_val) => match self { 27 | Value::Object(ref old_val) => { 28 | let patch = PatchObject::diff(old_val, new_val)?; 29 | Ok(Diff::Patch(patch)) 30 | } 31 | _ => Err(DiffError::DiffValue), 32 | }, 33 | } 34 | } 35 | } 36 | } 37 | } 38 | 39 | #[cfg(test)] 40 | mod test { 41 | 42 | use serde_json::json; 43 | use serde_json::Value; 44 | 45 | use super::Changes; 46 | 47 | #[test] 48 | fn test_null_comparision() { 49 | let n1 = Value::Null; 50 | let str1 = Value::String("test".to_owned()); 51 | let str2 = Value::String("test".to_owned()); 52 | 53 | assert!(n1.diff(&str1).expect("diff").is_replace()); 54 | assert!(str1.diff(&str2).expect("diff").is_none()); 55 | } 56 | 57 | #[test] 58 | fn test_object_comparision() { 59 | let old_spec = json!({ 60 | "replicas": 2, 61 | "apple": 5 62 | }); 63 | let new_spec = json!({ 64 | "replicas": 3, 65 | "apple": 5 66 | }); 67 | 68 | let diff = old_spec.diff(&new_spec).expect("diff"); 69 | assert!(diff.is_patch()); 70 | let patch = diff.as_patch_ref().get_inner_ref(); 71 | assert_eq!(patch.len(), 1); 72 | let diff_replicas = patch.get("replicas").unwrap(); 73 | assert!(diff_replicas.is_replace()); 74 | assert_eq!(*diff_replicas.as_replace_ref(), 3); 75 | } 76 | 77 | #[test] 78 | #[allow(clippy::assertions_on_constants)] 79 | fn test_replace_some_with_none() { 80 | use serde::Serialize; 81 | use serde_json::to_value; 82 | 83 | use crate::Diff; 84 | 85 | #[derive(Serialize)] 86 | struct Test { 87 | choice: Option, 88 | value: u16, 89 | } 90 | 91 | let old_spec = to_value(Test { 92 | choice: Some(true), 93 | value: 5, 94 | }) 95 | .expect("json"); 96 | let new_spec = to_value(Test { 97 | choice: None, 98 | value: 5, 99 | }) 100 | .expect("json"); 101 | 102 | let diff = old_spec.diff(&new_spec).expect("diff"); 103 | 104 | assert!(diff.is_patch()); 105 | 106 | match diff { 107 | Diff::Patch(p) => { 108 | let json_diff = serde_json::to_value(p).expect("json"); 109 | println!("json diff: {:#?}", json_diff); 110 | assert_eq!(json_diff, json!({ "choice": null })); 111 | } 112 | _ => assert!(false), 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/k8-diff/src/json/mod.rs: -------------------------------------------------------------------------------- 1 | mod diff; 2 | mod se; 3 | 4 | use serde_json::Map; 5 | use serde_json::Value; 6 | use std::collections::HashMap; 7 | 8 | use crate::Changes; 9 | use crate::Diff; 10 | use crate::DiffError; 11 | 12 | type SerdeObj = Map; 13 | pub type JsonDiff = Diff; 14 | 15 | #[derive(Debug)] 16 | pub struct PatchObject(HashMap); 17 | 18 | impl PatchObject { 19 | // diff { "a": 1,"b": 2}, { "a": 3, "b": 2} => { "a": 1} 20 | fn diff(old: &SerdeObj, new: &SerdeObj) -> Result { 21 | let mut map: HashMap = HashMap::new(); 22 | 23 | for (key, new_val) in new.iter() { 24 | match old.get(key) { 25 | Some(old_val) => { 26 | if old_val != new_val { 27 | let diff_value = old_val.diff(new_val)?; 28 | map.insert(key.clone(), diff_value); 29 | } 30 | } 31 | _ => { 32 | map.insert(key.clone(), Diff::Replace(new_val.clone())); // just replace with new if key doesn't match 33 | } 34 | } 35 | } 36 | 37 | Ok(PatchObject(map)) 38 | } 39 | 40 | fn get_inner_ref(&self) -> &HashMap { 41 | &self.0 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/k8-diff/src/json/se.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | use serde_json::Value; 3 | 4 | use super::PatchObject; 5 | use crate::Diff; 6 | 7 | impl Serialize for PatchObject { 8 | fn serialize(&self, serializer: S) -> Result 9 | where 10 | S: ::serde::Serializer, 11 | { 12 | let diff_maps = self.get_inner_ref(); 13 | use serde::ser::SerializeMap; 14 | let mut map = serializer.serialize_map(Some(diff_maps.len()))?; 15 | for (key, val) in diff_maps { 16 | match val { 17 | Diff::None => {} 18 | Diff::Delete => {} 19 | Diff::Patch(ref v) => map.serialize_entry(key, v)?, 20 | Diff::Replace(ref v) => { 21 | map.serialize_entry(key, v)?; 22 | } 23 | Diff::Merge(ref v) => { 24 | map.serialize_entry(key, v)?; 25 | } 26 | } 27 | } 28 | 29 | map.end() 30 | } 31 | } 32 | 33 | impl Serialize for Diff { 34 | fn serialize(&self, serializer: S) -> Result 35 | where 36 | S: ::serde::Serializer, 37 | { 38 | match self { 39 | Diff::None => serializer.serialize_unit(), 40 | Diff::Delete => serializer.serialize_unit(), 41 | Diff::Patch(ref p) => p.serialize(serializer), 42 | Diff::Replace(ref v) => v.serialize(serializer), 43 | Diff::Merge(ref v) => v.serialize(serializer), 44 | } 45 | } 46 | } 47 | 48 | #[cfg(test)] 49 | mod test { 50 | 51 | use serde_json::json; 52 | 53 | use crate::Changes; 54 | 55 | #[test] 56 | fn test_patch_to_simple() { 57 | let old_spec = json!({ 58 | "replicas": 2, 59 | "apple": 5 60 | }); 61 | let new_spec = json!({ 62 | "replicas": 3, 63 | "apple": 5 64 | }); 65 | 66 | let diff = old_spec.diff(&new_spec).expect("diff"); 67 | assert!(diff.is_patch()); 68 | 69 | let expected = json!({ 70 | "replicas": 3 71 | }); 72 | let json_diff = serde_json::to_value(diff).unwrap(); 73 | assert_eq!(json_diff, expected); 74 | } 75 | 76 | #[test] 77 | fn test_patch_to_hierarchy() { 78 | let old_spec = json!({ 79 | "spec": { 80 | "replicas": 2, 81 | "apple": 5 82 | } 83 | }); 84 | let new_spec = json!({ 85 | "spec": { 86 | "replicas": 3, 87 | "apple": 5 88 | } 89 | }); 90 | 91 | let diff = old_spec.diff(&new_spec).expect("diff"); 92 | assert!(diff.is_patch()); 93 | println!("final diff: {:#?}", diff); 94 | let expected = json!({ 95 | "spec": { 96 | "replicas": 3 97 | }}); 98 | let json_diff = serde_json::to_value(diff).unwrap(); 99 | assert_eq!(json_diff, expected); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/k8-diff/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod json; 2 | 3 | use std::fmt; 4 | 5 | pub trait Changes { 6 | type Replace; 7 | type Patch; 8 | 9 | fn diff(&self, new: &Self) -> Result, DiffError>; 10 | } 11 | 12 | #[derive(Debug)] 13 | pub enum DiffError { 14 | DiffValue, // json values are different 15 | } 16 | 17 | impl std::fmt::Display for DiffError { 18 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 19 | write!(f, "JSON value types are different") 20 | } 21 | } 22 | 23 | impl std::error::Error for DiffError {} 24 | 25 | // use Option as inspiration 26 | #[derive(Debug)] 27 | pub enum Diff { 28 | None, 29 | Delete, 30 | Patch(P), // for non primitive type 31 | Replace(R), // can be used for map and list (with our without tag), works on ordered list 32 | Merge(R), // need tag, works on unorderd list 33 | } 34 | 35 | impl Diff { 36 | pub fn is_none(&self) -> bool { 37 | matches!(self, Diff::None) 38 | } 39 | 40 | pub fn is_delete(&self) -> bool { 41 | matches!(self, Diff::Delete) 42 | } 43 | 44 | pub fn is_replace(&self) -> bool { 45 | matches!(self, Diff::Replace(_)) 46 | } 47 | 48 | pub fn is_patch(&self) -> bool { 49 | matches!(self, Diff::Patch(_)) 50 | } 51 | 52 | pub fn is_merge(&self) -> bool { 53 | matches!(self, Diff::Merge(_)) 54 | } 55 | 56 | pub fn as_replace_ref(&self) -> &R { 57 | match self { 58 | Diff::Replace(ref val) => val, 59 | _ => panic!("no change value"), 60 | } 61 | } 62 | 63 | pub fn as_patch_ref(&self) -> &P { 64 | match self { 65 | Diff::Patch(ref val) => val, 66 | _ => panic!("no change value"), 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/k8-metadata-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "k8-metadata-client" 4 | version = "7.1.0" 5 | authors = ["Fluvio Contributors "] 6 | description = "Trait for interfacing kubernetes metadata service" 7 | repository = "https://github.com/infinyon/k8-api" 8 | license = "Apache-2.0" 9 | 10 | [dependencies] 11 | anyhow = { workspace = true } 12 | tracing = "0.1.19" 13 | futures-util = { version = "0.3.21"} 14 | pin-utils = "0.1.0-alpha.4" 15 | serde = { version ="1.0.136", features = ['derive'] } 16 | serde_json = "1.0.40" 17 | serde_qs = { workspace = true } 18 | async-trait = "0.1.52" 19 | k8-diff = { version = "0.1.0", path = "../k8-diff"} 20 | k8-types = { version = "0.8.0", path = "../k8-types" } 21 | -------------------------------------------------------------------------------- /src/k8-metadata-client/README.md: -------------------------------------------------------------------------------- 1 | # kf-metadata-client 2 | 3 | Trait for interfacing kubernetes metadata service 4 | 5 | ## License 6 | 7 | This project is licensed under the [Apache license](LICENSE-APACHE). 8 | 9 | ### Contribution 10 | 11 | Unless you explicitly state otherwise, any contribution intentionally submitted 12 | for inclusion in Fluvio by you, shall be licensed as Apache, without any additional 13 | terms or conditions. 14 | -------------------------------------------------------------------------------- /src/k8-metadata-client/src/client.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | use std::sync::Arc; 3 | 4 | use anyhow::{anyhow, Result}; 5 | use async_trait::async_trait; 6 | use futures_util::future::ready; 7 | use futures_util::future::FutureExt; 8 | use futures_util::stream::once; 9 | use futures_util::stream::BoxStream; 10 | use futures_util::stream::StreamExt; 11 | use serde::de::DeserializeOwned; 12 | use serde::Serialize; 13 | use serde_json::Value; 14 | use tracing::debug; 15 | use tracing::trace; 16 | 17 | use k8_diff::{Changes, Diff}; 18 | use k8_types::{InputK8Obj, K8List, K8Meta, K8Obj, DeleteStatus, K8Watch, Spec, UpdateK8ObjStatus}; 19 | use k8_types::options::DeleteOptions; 20 | use crate::diff::PatchMergeType; 21 | use crate::{ApplyResult, DiffableK8Obj}; 22 | 23 | #[derive(Clone)] 24 | pub enum NameSpace { 25 | All, 26 | Named(String), 27 | } 28 | 29 | #[derive(Debug)] 30 | pub struct ObjectKeyNotFound { 31 | key: String, 32 | } 33 | 34 | impl ObjectKeyNotFound { 35 | pub fn new(key: String) -> Self { 36 | Self { key } 37 | } 38 | } 39 | 40 | impl std::error::Error for ObjectKeyNotFound {} 41 | 42 | impl Display for ObjectKeyNotFound { 43 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 44 | write!(f, "'{}' not found", self.key) 45 | } 46 | } 47 | 48 | impl NameSpace { 49 | pub fn is_all(&self) -> bool { 50 | matches!(self, Self::All) 51 | } 52 | 53 | pub fn named(&self) -> &str { 54 | match self { 55 | Self::All => "all", 56 | Self::Named(name) => name, 57 | } 58 | } 59 | } 60 | 61 | impl From for NameSpace { 62 | fn from(namespace: String) -> Self { 63 | NameSpace::Named(namespace) 64 | } 65 | } 66 | 67 | impl From<&str> for NameSpace { 68 | fn from(namespace: &str) -> Self { 69 | NameSpace::Named(namespace.to_owned()) 70 | } 71 | } 72 | 73 | #[derive(Default, Clone)] 74 | pub struct ListArg { 75 | pub field_selector: Option, 76 | pub include_uninitialized: Option, 77 | pub label_selector: Option, 78 | } 79 | 80 | // For error mapping: see: https://doc.rust-lang.org/nightly/core/convert/trait.From.html 81 | 82 | pub type TokenStreamResult = Result>>>; 83 | 84 | #[allow(clippy::redundant_closure)] 85 | pub fn as_token_stream_result(events: Vec>) -> TokenStreamResult 86 | where 87 | S: Spec, 88 | S::Status: Serialize + DeserializeOwned, 89 | S::Header: Serialize + DeserializeOwned, 90 | { 91 | Ok(events.into_iter().map(|event| Ok(event)).collect()) 92 | } 93 | 94 | #[async_trait] 95 | pub trait MetadataClient: Send + Sync { 96 | /// retrieval a single item 97 | async fn retrieve_item(&self, metadata: &M) -> Result>> 98 | where 99 | S: Spec, 100 | M: K8Meta + Send + Sync; 101 | 102 | /// retrieve all items a single chunk 103 | /// this may cause client to hang if there are too many items 104 | async fn retrieve_items(&self, namespace: N) -> Result> 105 | where 106 | S: Spec, 107 | N: Into + Send + Sync, 108 | { 109 | self.retrieve_items_with_option(namespace, None).await 110 | } 111 | 112 | async fn retrieve_items_with_option( 113 | &self, 114 | namespace: N, 115 | option: Option, 116 | ) -> Result> 117 | where 118 | S: Spec, 119 | N: Into + Send + Sync; 120 | 121 | /// returns stream of items in chunks 122 | fn retrieve_items_in_chunks<'a, S, N>( 123 | self: Arc, 124 | namespace: N, 125 | limit: u32, 126 | option: Option, 127 | ) -> BoxStream<'a, K8List> 128 | where 129 | S: Spec + 'static, 130 | N: Into + Send + Sync + 'static; 131 | 132 | async fn delete_item_with_option( 133 | &self, 134 | metadata: &M, 135 | option: Option, 136 | ) -> Result> 137 | where 138 | S: Spec, 139 | M: K8Meta + Send + Sync; 140 | 141 | async fn delete_item(&self, metadata: &M) -> Result> 142 | where 143 | S: Spec, 144 | M: K8Meta + Send + Sync, 145 | { 146 | self.delete_item_with_option::(metadata, None).await 147 | } 148 | 149 | /// create new object 150 | async fn create_item(&self, value: InputK8Obj) -> Result> 151 | where 152 | S: Spec; 153 | 154 | /// apply object, this is similar to ```kubectl apply``` 155 | /// for now, this doesn't do any optimization 156 | /// if object doesn't exist, it will be created 157 | /// if object exist, it will be patched by using strategic merge diff 158 | async fn apply(&self, value: InputK8Obj) -> Result> 159 | where 160 | S: Spec, 161 | { 162 | debug!("{}: applying '{}' changes", S::label(), value.metadata.name); 163 | trace!("{}: applying {:#?}", S::label(), value); 164 | match self.retrieve_item(&value.metadata).await { 165 | Ok(Some(old_item)) => { 166 | let mut old_spec: S = old_item.spec; 167 | old_spec.make_same(&value.spec); 168 | // we don't care about status 169 | let new_obj = serde_json::to_value(DiffableK8Obj::new( 170 | value.metadata.clone(), 171 | value.spec.clone(), 172 | value.header.clone(), 173 | ))?; 174 | let old_obj = serde_json::to_value(DiffableK8Obj::new( 175 | old_item.metadata, 176 | old_spec, 177 | old_item.header, 178 | ))?; 179 | let diff = old_obj.diff(&new_obj)?; 180 | match diff { 181 | Diff::None => { 182 | debug!("{}: no diff detected, doing nothing", S::label()); 183 | Ok(ApplyResult::None) 184 | } 185 | Diff::Patch(p) => { 186 | let json_diff = serde_json::to_value(p)?; 187 | debug!("{}: detected diff: old vs. new obj", S::label()); 188 | trace!("{}: new obj: {:#?}", S::label(), &new_obj); 189 | trace!("{}: old obj: {:#?}", S::label(), &old_obj); 190 | trace!("{}: new/old diff: {:#?}", S::label(), json_diff); 191 | let patch_result = self.patch_obj(&value.metadata, &json_diff).await?; 192 | Ok(ApplyResult::Patched(patch_result)) 193 | } 194 | _ => Err(anyhow!("unsupported diff type")), 195 | } 196 | } 197 | Ok(None) => { 198 | debug!( 199 | "{}: item '{}' not found, creating ...", 200 | S::label(), 201 | value.metadata.name 202 | ); 203 | let created_item = self.create_item(value).await?; 204 | Ok(ApplyResult::Created(created_item)) 205 | } 206 | Err(err) => Err(err), 207 | } 208 | } 209 | 210 | /// update status 211 | async fn update_status(&self, value: &UpdateK8ObjStatus) -> Result> 212 | where 213 | S: Spec; 214 | 215 | /// patch existing obj 216 | async fn patch_obj(&self, metadata: &M, patch: &Value) -> Result> 217 | where 218 | S: Spec, 219 | M: K8Meta + Display + Send + Sync, 220 | { 221 | self.patch(metadata, patch, PatchMergeType::for_spec(S::metadata())) 222 | .await 223 | } 224 | 225 | /// patch object with arbitrary patch 226 | async fn patch( 227 | &self, 228 | metadata: &M, 229 | patch: &Value, 230 | merge_type: PatchMergeType, 231 | ) -> Result> 232 | where 233 | S: Spec, 234 | M: K8Meta + Display + Send + Sync; 235 | 236 | /// patch status 237 | async fn patch_status( 238 | &self, 239 | metadata: &M, 240 | patch: &Value, 241 | merge_type: PatchMergeType, 242 | ) -> Result> 243 | where 244 | S: Spec, 245 | M: K8Meta + Display + Send + Sync; 246 | 247 | async fn patch_subresource( 248 | &self, 249 | metadata: &M, 250 | subresource: String, 251 | patch: &Value, 252 | merge_type: PatchMergeType, 253 | ) -> Result> 254 | where 255 | S: Spec, 256 | M: K8Meta + Display + Send + Sync; 257 | 258 | /// stream items since resource versions 259 | fn watch_stream_since( 260 | &self, 261 | namespace: N, 262 | resource_version: Option, 263 | ) -> BoxStream<'_, TokenStreamResult> 264 | where 265 | S: Spec + 'static, 266 | N: Into; 267 | 268 | fn watch_stream_now(&self, ns: String) -> BoxStream<'_, TokenStreamResult> 269 | where 270 | S: Spec + 'static, 271 | { 272 | let ft_stream = async move { 273 | let namespace = ns.as_ref(); 274 | match self.retrieve_items_with_option(namespace, None).await { 275 | Ok(item_now_list) => { 276 | let resource_version = item_now_list.metadata.resource_version; 277 | 278 | let items_watch_stream = 279 | self.watch_stream_since(namespace, Some(resource_version)); 280 | 281 | let items_list = item_now_list 282 | .items 283 | .into_iter() 284 | .map(|item| Ok(K8Watch::ADDED(item))) 285 | .collect(); 286 | let list_stream = once(ready(Ok(items_list))); 287 | 288 | list_stream.chain(items_watch_stream).left_stream() 289 | // list_stream 290 | } 291 | Err(err) => once(ready(Err(err))).right_stream(), 292 | } 293 | }; 294 | 295 | ft_stream.flatten_stream().boxed() 296 | } 297 | 298 | /// Check if the object exists, return true or false. 299 | async fn exists(&self, metadata: &M) -> Result 300 | where 301 | S: Spec, 302 | M: K8Meta + Display + Send + Sync, 303 | { 304 | debug!("check if '{}' exists", metadata); 305 | match self.retrieve_item::(metadata).await { 306 | Ok(Some(_)) => Ok(true), 307 | Ok(None) => Ok(false), 308 | Err(err) => Err(err), 309 | } 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /src/k8-metadata-client/src/diff.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | 3 | use k8_types::{Crd, K8Obj, Spec}; 4 | 5 | #[derive(Debug)] 6 | pub enum ApplyResult 7 | where 8 | S: Spec, 9 | { 10 | None, 11 | Created(K8Obj), 12 | Patched(K8Obj), 13 | } 14 | 15 | #[derive(Debug, Serialize)] 16 | #[serde(rename_all = "camelCase")] 17 | pub struct ApplyOptions { 18 | pub force: bool, 19 | pub field_manager: Option, 20 | } 21 | 22 | #[allow(dead_code)] 23 | pub enum PatchMergeType { 24 | Json, 25 | JsonMerge, 26 | StrategicMerge, // for aggegration API 27 | Apply(ApplyOptions), 28 | } 29 | 30 | impl PatchMergeType { 31 | pub fn for_spec(crd: &Crd) -> Self { 32 | match crd.group { 33 | "core" => PatchMergeType::StrategicMerge, 34 | "apps" => PatchMergeType::StrategicMerge, 35 | _ => PatchMergeType::JsonMerge, 36 | } 37 | } 38 | 39 | pub fn content_type(&self) -> &'static str { 40 | match self { 41 | PatchMergeType::Json => "application/json-patch+json", 42 | PatchMergeType::JsonMerge => "application/merge-patch+json", 43 | PatchMergeType::StrategicMerge => "application/strategic-merge-patch+json", 44 | PatchMergeType::Apply { .. } => "application/apply-patch+yaml", 45 | } 46 | } 47 | } 48 | 49 | /// used for comparing k8 objects, 50 | #[derive(Serialize, Debug, Clone)] 51 | pub struct DiffableK8Obj { 52 | metadata: M, 53 | spec: S, 54 | #[serde(flatten)] 55 | header: H, 56 | } 57 | 58 | impl DiffableK8Obj 59 | where 60 | M: Serialize, 61 | S: Serialize, 62 | H: Serialize, 63 | { 64 | pub fn new(metadata: M, spec: S, header: H) -> Self { 65 | Self { 66 | metadata, 67 | spec, 68 | header, 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/k8-metadata-client/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod client; 2 | mod diff; 3 | mod nothing; 4 | pub use diff::*; 5 | 6 | pub use client::as_token_stream_result; 7 | pub use client::ListArg; 8 | pub use client::MetadataClient; 9 | pub use client::NameSpace; 10 | pub use client::ObjectKeyNotFound; 11 | pub use client::TokenStreamResult; 12 | pub use nothing::DoNothingClient; 13 | 14 | pub type SharedClient = std::sync::Arc; 15 | -------------------------------------------------------------------------------- /src/k8-metadata-client/src/nothing.rs: -------------------------------------------------------------------------------- 1 | // implementation of metadata client do nothing 2 | // it is used for testing where to satisfy metadata contract 3 | use std::fmt::Debug; 4 | use std::fmt::Display; 5 | use std::sync::Arc; 6 | 7 | use anyhow::Result; 8 | use async_trait::async_trait; 9 | use futures_util::stream::BoxStream; 10 | use futures_util::stream::StreamExt; 11 | use serde::de::DeserializeOwned; 12 | use serde::Serialize; 13 | use serde_json::Value; 14 | 15 | use k8_types::{InputK8Obj, K8List, K8Meta, K8Obj, DeleteStatus, K8Watch, Spec, UpdateK8ObjStatus}; 16 | use k8_types::options::DeleteOptions; 17 | use crate::client::ObjectKeyNotFound; 18 | use crate::diff::PatchMergeType; 19 | 20 | use crate::{ListArg, MetadataClient, NameSpace, TokenStreamResult}; 21 | 22 | pub struct DoNothingClient(); 23 | 24 | #[async_trait] 25 | impl MetadataClient for DoNothingClient { 26 | async fn retrieve_item(&self, _metadata: &M) -> Result>> 27 | where 28 | K8Obj: DeserializeOwned, 29 | S: Spec, 30 | M: K8Meta + Send + Sync, 31 | { 32 | Ok(None) 33 | } 34 | 35 | async fn retrieve_items_with_option( 36 | &self, 37 | _namespace: N, 38 | _option: Option, 39 | ) -> Result> 40 | where 41 | S: Spec, 42 | N: Into + Send + Sync, 43 | { 44 | Ok(K8List::default()) 45 | } 46 | 47 | fn retrieve_items_in_chunks<'a, S, N>( 48 | self: Arc, 49 | _namespace: N, 50 | _limit: u32, 51 | _option: Option, 52 | ) -> BoxStream<'a, K8List> 53 | where 54 | S: Spec + 'static, 55 | N: Into + Send + Sync + 'static, 56 | { 57 | futures_util::stream::empty().boxed() 58 | } 59 | 60 | async fn delete_item_with_option( 61 | &self, 62 | metadata: &M, 63 | _options: Option, 64 | ) -> Result> 65 | where 66 | S: Spec, 67 | M: K8Meta + Send + Sync, 68 | { 69 | Err(ObjectKeyNotFound::new(metadata.name().into()).into()) 70 | } 71 | 72 | async fn create_item(&self, _value: InputK8Obj) -> Result> 73 | where 74 | InputK8Obj: Serialize + Debug, 75 | K8Obj: DeserializeOwned, 76 | S: Spec + Send, 77 | { 78 | Err(ObjectKeyNotFound::new(_value.metadata.name().into()).into()) 79 | } 80 | 81 | async fn update_status(&self, _value: &UpdateK8ObjStatus) -> Result> 82 | where 83 | UpdateK8ObjStatus: Serialize + Debug, 84 | K8Obj: DeserializeOwned, 85 | S: Spec + Send + Sync, 86 | S::Status: Send + Sync, 87 | { 88 | Err(ObjectKeyNotFound::new(_value.metadata.name().into()).into()) 89 | } 90 | 91 | async fn patch( 92 | &self, 93 | metadata: &M, 94 | _patch: &Value, 95 | _merge_type: PatchMergeType, 96 | ) -> Result> 97 | where 98 | S: Spec, 99 | M: K8Meta + Display + Send + Sync, 100 | { 101 | Err(ObjectKeyNotFound::new(metadata.name().into()).into()) 102 | } 103 | 104 | async fn patch_status( 105 | &self, 106 | metadata: &M, 107 | _patch: &Value, 108 | _merge_type: PatchMergeType, 109 | ) -> Result> 110 | where 111 | S: Spec, 112 | M: K8Meta + Display + Send + Sync, 113 | { 114 | Err(ObjectKeyNotFound::new(metadata.name().into()).into()) 115 | } 116 | 117 | async fn patch_subresource( 118 | &self, 119 | metadata: &M, 120 | _subresource: String, 121 | _patch: &Value, 122 | _merge_type: PatchMergeType, 123 | ) -> Result> 124 | where 125 | S: Spec, 126 | M: K8Meta + Display + Send + Sync, 127 | { 128 | Err(ObjectKeyNotFound::new(metadata.name().into()).into()) 129 | } 130 | 131 | fn watch_stream_since( 132 | &self, 133 | _namespace: N, 134 | _resource_version: Option, 135 | ) -> BoxStream<'_, TokenStreamResult> 136 | where 137 | K8Watch: DeserializeOwned, 138 | S: Spec + Send + 'static, 139 | S::Header: Send + 'static, 140 | S::Status: Send + 'static, 141 | N: Into, 142 | { 143 | futures_util::stream::empty().boxed() 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/k8-types/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "k8-types" 4 | version = "0.8.8" 5 | authors = ["Fluvio Contributors "] 6 | description = "Kubernetes Object Types" 7 | repository = "https://github.com/infinyon/k8-api" 8 | license = "Apache-2.0" 9 | categories = ["encoding"] 10 | 11 | [features] 12 | core = [] 13 | app = ["core"] 14 | storage = [] 15 | batch = ["core"] 16 | 17 | [dependencies] 18 | serde = { version ="1.0.136", features = ['derive'] } 19 | serde_json = "1.0.60" 20 | 21 | [dev-dependencies] 22 | serde_qs = { workspace = true } 23 | -------------------------------------------------------------------------------- /src/k8-types/README.md: -------------------------------------------------------------------------------- 1 | # kf-metadata-core 2 | 3 | Core Kubernetes metadata traits 4 | 5 | ## License 6 | 7 | This project is licensed under the [Apache license](LICENSE-APACHE). 8 | 9 | ### Contribution 10 | 11 | Unless you explicitly state otherwise, any contribution intentionally submitted 12 | for inclusion in Fluvio by you, shall be licensed as Apache, without any additional 13 | terms or conditions. 14 | -------------------------------------------------------------------------------- /src/k8-types/src/app/deployment.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use serde::Serialize; 3 | 4 | use crate::core::pod::PodSpec; 5 | use crate::{Crd, CrdNames, DefaultHeader, Int32OrString, LabelSelector, Spec, Status, TemplateSpec}; 6 | const DEPLOYMENT_API: Crd = Crd { 7 | group: "apps", 8 | version: "v1", 9 | names: CrdNames { 10 | kind: "Deployment", 11 | plural: "deployments", 12 | singular: "deployment", 13 | }, 14 | }; 15 | 16 | #[derive(Deserialize, Serialize, Debug, Default, Clone, Eq, PartialEq)] 17 | #[serde(rename_all = "camelCase", default)] 18 | pub struct DeploymentSpec { 19 | pub min_ready_seconds: Option, 20 | pub paused: Option, 21 | pub progress_deadline_seconds: Option, 22 | pub replicas: Option, 23 | pub revision_history_limit: Option, 24 | pub selector: LabelSelector, 25 | pub strategy: Option, 26 | pub template: TemplateSpec, 27 | } 28 | 29 | #[derive(Deserialize, Serialize, Debug, Default, Clone, Eq, PartialEq)] 30 | #[serde(rename_all = "camelCase", default)] 31 | pub struct DeploymentStrategy { 32 | pub rolling_update: Option, 33 | #[serde(rename = "type")] 34 | pub type_: Option, 35 | } 36 | 37 | #[derive(Deserialize, Serialize, Debug, Default, Clone, Eq, PartialEq)] 38 | #[serde(rename_all = "camelCase", default)] 39 | pub struct RollingUpdateDeployment { 40 | pub max_surge: Option, 41 | pub max_unavailable: Option, 42 | } 43 | 44 | impl Spec for DeploymentSpec { 45 | type Status = DeploymentStatus; 46 | type Header = DefaultHeader; 47 | 48 | fn metadata() -> &'static Crd { 49 | &DEPLOYMENT_API 50 | } 51 | } 52 | 53 | #[derive(Deserialize, Serialize, Default, Debug, Clone, Eq, PartialEq)] 54 | #[serde(rename_all = "camelCase")] 55 | pub struct DeploymentStatus { 56 | pub available_replicas: Option, 57 | pub collision_count: Option, 58 | #[serde(default = "Vec::new")] 59 | pub conditions: Vec, 60 | pub observed_generation: Option, 61 | pub ready_replicas: Option, 62 | pub replicas: Option, 63 | pub unavailable_replicas: Option, 64 | pub updated_replicas: Option, 65 | } 66 | 67 | #[derive(Deserialize, Serialize, Default, Debug, Clone, Eq, PartialEq)] 68 | #[serde(rename_all = "camelCase")] 69 | pub struct DeploymentCondition { 70 | pub last_transition_time: Option, 71 | pub last_update_time: Option, 72 | pub message: Option, 73 | pub reason: Option, 74 | pub status: String, 75 | #[serde(rename = "type")] 76 | pub type_: String, 77 | } 78 | 79 | impl Status for DeploymentStatus {} 80 | -------------------------------------------------------------------------------- /src/k8-types/src/app/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod stateful; 2 | pub mod deployment; 3 | -------------------------------------------------------------------------------- /src/k8-types/src/app/stateful.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use serde::Serialize; 3 | 4 | use crate::core::pod::PodSpec; 5 | use crate::{Crd, CrdNames, DefaultHeader, LabelSelector, Spec, Status, TemplateSpec}; 6 | 7 | const STATEFUL_API: Crd = Crd { 8 | group: "apps", 9 | version: "v1", 10 | names: CrdNames { 11 | kind: "StatefulSet", 12 | plural: "statefulsets", 13 | singular: "statefulset", 14 | }, 15 | }; 16 | 17 | #[derive(Deserialize, Serialize, Debug, Default, Clone, Eq, PartialEq)] 18 | #[serde(rename_all = "camelCase", default)] 19 | pub struct StatefulSetSpec { 20 | pub pod_management_policy: Option, 21 | pub replicas: Option, 22 | pub revision_history_limit: Option, 23 | pub selector: LabelSelector, 24 | pub service_name: String, 25 | pub template: TemplateSpec, 26 | pub volume_claim_templates: Vec>, 27 | pub update_strategy: Option, 28 | } 29 | 30 | impl Spec for StatefulSetSpec { 31 | type Status = StatefulSetStatus; 32 | type Header = DefaultHeader; 33 | 34 | fn metadata() -> &'static Crd { 35 | &STATEFUL_API 36 | } 37 | 38 | // statefulset doesnt' like to change volume claim template 39 | fn make_same(&mut self, other: &Self) { 40 | self.volume_claim_templates 41 | .clone_from(&other.volume_claim_templates) 42 | } 43 | } 44 | 45 | #[derive(Deserialize, Serialize, Debug, Default, Clone, Eq, PartialEq)] 46 | #[serde(rename_all = "camelCase")] 47 | pub struct StatefulSetUpdateStrategy { 48 | pub _type: String, 49 | pub rolling_ipdate: Option, 50 | } 51 | 52 | #[derive(Deserialize, Serialize, Debug, Default, Clone, Eq, PartialEq)] 53 | #[serde(rename_all = "camelCase")] 54 | pub struct RollingUpdateStatefulSetStrategy { 55 | partition: u32, 56 | } 57 | 58 | #[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Clone)] 59 | pub enum PodMangementPolicy { 60 | OrderedReady, 61 | Parallel, 62 | } 63 | 64 | #[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq)] 65 | #[serde(rename_all = "camelCase")] 66 | pub struct PersistentVolumeClaim { 67 | pub access_modes: Vec, 68 | pub storage_class_name: Option, 69 | pub resources: ResourceRequirements, 70 | } 71 | 72 | #[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Clone)] 73 | pub enum VolumeAccessMode { 74 | ReadWriteOnce, 75 | ReadWrite, 76 | ReadOnlyMany, 77 | } 78 | 79 | #[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq)] 80 | pub struct ResourceRequirements { 81 | pub requests: VolumeRequest, 82 | } 83 | 84 | #[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq)] 85 | pub struct VolumeRequest { 86 | pub storage: String, 87 | } 88 | 89 | #[derive(Deserialize, Serialize, Default, Debug, Clone, Eq, PartialEq)] 90 | #[serde(rename_all = "camelCase")] 91 | pub struct StatefulSetStatus { 92 | pub replicas: u16, 93 | pub collision_count: Option, 94 | #[serde(default)] 95 | pub conditions: Vec, 96 | pub current_replicas: Option, 97 | pub current_revision: Option, 98 | pub observed_generation: Option, 99 | pub ready_replicas: Option, 100 | pub update_revision: Option, 101 | pub updated_replicas: Option, 102 | } 103 | 104 | impl Status for StatefulSetStatus {} 105 | 106 | #[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Clone)] 107 | pub enum StatusEnum { 108 | True, 109 | False, 110 | Unknown, 111 | } 112 | 113 | #[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq)] 114 | #[serde(rename_all = "camelCase")] 115 | pub struct StatefulSetCondition { 116 | pub message: String, 117 | pub reason: StatusEnum, 118 | pub status: String, 119 | #[serde(rename = "type")] 120 | pub _type: String, 121 | } 122 | 123 | /* 124 | #[cfg(test)] 125 | mod test { 126 | 127 | use serde_json; 128 | use serde_json::json; 129 | 130 | use super::LabelSelector; 131 | use super::StatefulSetSpec; 132 | use k8_diff::Changes; 133 | use k8_metadata::cluster::ClusterSpec; 134 | use k8_metadata::cluster::Configuration; 135 | use k8_metadata::cluster::Cluster; 136 | use k8_metadata::cluster::ClusterEndpoint; 137 | 138 | #[test] 139 | fn test_label_selector() { 140 | let selector = LabelSelector::new_labels(vec![("app".to_owned(), "test".to_owned())]); 141 | 142 | let maps = selector.match_labels; 143 | assert_eq!(maps.len(), 1); 144 | assert_eq!(maps.get("app").unwrap(), "test"); 145 | } 146 | 147 | #[test] 148 | fn test_cluster_to_stateful() { 149 | let cluster = ClusterSpec { 150 | cluster: Cluster { 151 | replicas: Some(3), 152 | rack: Some("rack1".to_string()), 153 | public_endpoint: Some(ClusterEndpoint::new(9005)), 154 | private_endpoint: Some(ClusterEndpoint::new(9006)), 155 | controller_endpoint: Some(ClusterEndpoint::new(9004)), 156 | }, 157 | configuration: Some(Configuration::default()), 158 | env: None, 159 | }; 160 | 161 | let stateful: StatefulSetSpec = (&cluster).into(); 162 | assert_eq!(stateful.replicas, Some(3)); 163 | let mut stateful2 = stateful.clone(); 164 | stateful2.replicas = Some(2); 165 | 166 | let state1_json = serde_json::to_value(stateful).expect("json"); 167 | let state2_json = serde_json::to_value(stateful2).expect("json"); 168 | let diff = state1_json.diff(&state2_json).expect("diff"); 169 | let json_diff = serde_json::to_value(diff).unwrap(); 170 | assert_eq!( 171 | json_diff, 172 | json!({ 173 | "replicas": 2 174 | }) 175 | ); 176 | } 177 | 178 | 179 | /* 180 | * TODO: make this as utility 181 | use std::io::Read; 182 | use std::fs::File; 183 | use k8_metadata_core::metadata::ObjectMeta; 184 | use k8_metadata_core::metadata::K8Obj; 185 | use super::StatefulSetStatus; 186 | use super::TemplateSpec; 187 | use super::PodSpec; 188 | use super::ContainerSpec; 189 | use super::ContainerPortSpec; 190 | 191 | #[test] 192 | fn test_decode_statefulset() { 193 | let file_name = "/private/tmp/f1.json"; 194 | 195 | let mut f = File::open(file_name).expect("open failed"); 196 | let mut contents = String::new(); 197 | f.read_to_string(&mut contents).expect("read file"); 198 | // let st: StatefulSetSpec = serde_json::from_slice(&buffer).expect("error"); 199 | let st: K8Obj = serde_json::from_str(&contents).expect("error"); 200 | println!("st: {:#?}",st); 201 | assert!(true); 202 | } 203 | */ 204 | 205 | } 206 | */ 207 | -------------------------------------------------------------------------------- /src/k8-types/src/batch/job.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use serde::Serialize; 3 | 4 | use crate::core::pod::PodSpec; 5 | use crate::{Crd, CrdNames, DefaultHeader, LabelSelector, Spec, Status, TemplateSpec}; 6 | 7 | #[derive(Deserialize, Serialize, Default, Debug, Clone)] 8 | #[serde(rename_all = "camelCase", default)] 9 | pub struct JobSpec { 10 | pub template: TemplateSpec, 11 | pub backoff_limit: Option, 12 | pub active_deadline_seconds: Option, 13 | pub paralellism: Option, 14 | pub completions: Option, 15 | pub completion_mode: Option, 16 | pub suspend: Option, 17 | pub selector: Option, 18 | pub manual_selector: Option, 19 | pub ttl_seconds_after_finished: Option, 20 | } 21 | 22 | #[derive(Deserialize, Serialize, Debug, Clone)] 23 | pub enum CompletionMode { 24 | Indexed, 25 | NonIndexed, 26 | } 27 | 28 | impl Spec for JobSpec { 29 | type Status = JobStatus; 30 | type Header = DefaultHeader; 31 | 32 | fn metadata() -> &'static Crd { 33 | &API 34 | } 35 | } 36 | 37 | const API: Crd = Crd { 38 | group: "batch", 39 | version: "v1", 40 | names: CrdNames { 41 | kind: "Job", 42 | plural: "jobs", 43 | singular: "job", 44 | }, 45 | }; 46 | 47 | #[derive(Deserialize, Serialize, Default, Debug, Clone)] 48 | #[serde(rename_all = "camelCase", default)] 49 | pub struct JobStatus { 50 | active: usize, 51 | completed_indices: Option, 52 | completion_time: Option, 53 | failed: usize, 54 | job_condition: Vec, 55 | start_time: Option, 56 | succeeded: usize, 57 | } 58 | 59 | #[derive(Deserialize, Serialize, Debug, Clone)] 60 | #[serde(rename_all = "camelCase")] 61 | pub struct JobCondition { 62 | last_probe_time: String, 63 | last_transition_time: String, 64 | message: String, 65 | reason: String, 66 | status: JobConditionStatus, 67 | #[serde(rename = "type")] 68 | _type: JobType, 69 | } 70 | 71 | #[derive(Deserialize, Serialize, Debug, Clone)] 72 | pub enum JobConditionStatus { 73 | True, 74 | False, 75 | Unknown, 76 | } 77 | #[derive(Deserialize, Serialize, Debug, Clone)] 78 | 79 | pub enum JobType { 80 | Completed, 81 | Failed, 82 | } 83 | 84 | impl Status for JobStatus {} 85 | -------------------------------------------------------------------------------- /src/k8-types/src/batch/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod job; 2 | -------------------------------------------------------------------------------- /src/k8-types/src/core/config_map.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use serde::Deserialize; 4 | use serde::Serialize; 5 | 6 | use crate::Crd; 7 | use crate::CrdNames; 8 | use crate::Header; 9 | use crate::Spec; 10 | use crate::Status; 11 | 12 | // 13 | // ConfigMap Object 14 | const CONFIG_MAP_API: Crd = Crd { 15 | group: "core", 16 | version: "v1", 17 | names: CrdNames { 18 | kind: "ConfigMap", 19 | plural: "configmaps", 20 | singular: "configmap", 21 | }, 22 | }; 23 | 24 | impl Spec for ConfigMapSpec { 25 | type Status = ConfigMapStatus; 26 | type Header = ConfigMapHeader; 27 | 28 | fn metadata() -> &'static Crd { 29 | &CONFIG_MAP_API 30 | } 31 | } 32 | 33 | #[derive(Deserialize, Serialize, Debug, Default, Clone)] 34 | #[serde(rename_all = "camelCase")] 35 | pub struct ConfigMapSpec {} 36 | 37 | #[derive(Deserialize, Serialize, Default, Debug, Clone)] 38 | #[serde(rename_all = "camelCase")] 39 | pub struct ConfigMapStatus {} 40 | 41 | impl Status for ConfigMapStatus {} 42 | 43 | #[derive(Deserialize, Serialize, Debug, Default, Clone)] 44 | #[serde(rename_all = "camelCase")] 45 | pub struct ConfigMapHeader { 46 | #[serde(default)] 47 | pub data: BTreeMap, 48 | } 49 | 50 | impl Header for ConfigMapHeader {} 51 | -------------------------------------------------------------------------------- /src/k8-types/src/core/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod config_map; 2 | pub mod namespace; 3 | pub mod plugin; 4 | pub mod pod; 5 | pub mod secret; 6 | pub mod service; 7 | pub mod service_account; 8 | pub mod node; 9 | -------------------------------------------------------------------------------- /src/k8-types/src/core/namespace.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use serde::Serialize; 3 | 4 | use crate::default_store_spec; 5 | use crate::Crd; 6 | use crate::CrdNames; 7 | use crate::DefaultHeader; 8 | use crate::Spec; 9 | use crate::Status; 10 | 11 | const API: Crd = Crd { 12 | group: "core", 13 | version: "v1", 14 | names: CrdNames { 15 | kind: "Namespace", 16 | plural: "namespaces", 17 | singular: "namespace", 18 | }, 19 | }; 20 | 21 | #[derive(Deserialize, Serialize, Debug, Default, Clone)] 22 | #[serde(rename_all = "camelCase")] 23 | pub struct NamespaceSpec {} 24 | 25 | impl Spec for NamespaceSpec { 26 | type Status = NamespaceStatus; 27 | type Header = DefaultHeader; 28 | const NAME_SPACED: bool = false; 29 | 30 | fn metadata() -> &'static Crd { 31 | &API 32 | } 33 | } 34 | 35 | default_store_spec!(NamespaceSpec, NamespaceStatus, "Namespace"); 36 | 37 | #[derive(Deserialize, Serialize, Eq, PartialEq, Debug, Default, Clone)] 38 | #[serde(rename_all = "camelCase", default)] 39 | pub struct NamespaceStatus { 40 | pub phase: String, 41 | } 42 | 43 | impl Status for NamespaceStatus {} 44 | -------------------------------------------------------------------------------- /src/k8-types/src/core/node.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use serde::Serialize; 3 | 4 | use crate::Crd; 5 | use crate::CrdNames; 6 | use crate::DefaultHeader; 7 | use crate::Spec; 8 | use crate::Status; 9 | 10 | const NODE_API: Crd = Crd { 11 | group: "core", 12 | version: "v1", 13 | names: CrdNames { 14 | kind: "Node", 15 | plural: "nodes", 16 | singular: "node", 17 | }, 18 | }; 19 | 20 | impl Spec for NodeSpec { 21 | type Status = NodeStatus; 22 | type Header = DefaultHeader; 23 | const NAME_SPACED: bool = false; 24 | 25 | fn metadata() -> &'static Crd { 26 | &NODE_API 27 | } 28 | } 29 | 30 | #[derive(Deserialize, Serialize, Debug, Default, Clone, Eq, PartialEq)] 31 | #[serde(rename_all = "camelCase", default)] 32 | pub struct NodeSpec { 33 | #[serde(rename = "providerID")] 34 | pub provider_id: String, 35 | } 36 | 37 | #[derive(Deserialize, Serialize, Debug, Default, Clone, Eq, PartialEq)] 38 | #[serde(rename_all = "camelCase", default)] 39 | pub struct NodeStatus { 40 | pub addresses: Vec, 41 | //phase: String, 42 | //node_info: String, 43 | //volumes_attached: Vec, 44 | //volumesAttached: AttachedVolume 45 | } 46 | 47 | impl Status for NodeStatus {} 48 | 49 | #[derive(Deserialize, Serialize, Debug, Default, Clone, Eq, PartialEq)] 50 | #[serde(rename_all = "camelCase", default)] 51 | pub struct NodeList {} 52 | 53 | #[derive(Deserialize, Serialize, Debug, Default, Clone, Eq, PartialEq)] 54 | #[serde(rename_all = "camelCase", default)] 55 | pub struct NodeAddress { 56 | pub address: String, 57 | pub r#type: String, 58 | } 59 | 60 | //#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq)] 61 | //#[serde(rename_all = "camelCase", default)] 62 | //pub enum NodeAddressType { 63 | // Hostname, 64 | // #[serde(rename = "InternalIP")] 65 | // InternalIp, 66 | // #[serde(rename = "ExternalIP")] 67 | // ExternalIp 68 | //} 69 | -------------------------------------------------------------------------------- /src/k8-types/src/core/plugin.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use serde::Serialize; 3 | 4 | use crate::Crd; 5 | use crate::CrdNames; 6 | use crate::DefaultHeader; 7 | use crate::Spec; 8 | use crate::Status; 9 | 10 | const CREDENTIAL_API: Crd = Crd { 11 | group: "client.authentication.k8s.io", 12 | version: "v1", 13 | names: CrdNames { 14 | kind: "ExecCrendetial", 15 | plural: "credentials", 16 | singular: "credential", 17 | }, 18 | }; 19 | 20 | #[derive(Deserialize, Serialize, Debug, Default, Clone)] 21 | #[serde(rename_all = "camelCase")] 22 | pub struct ExecCredentialSpec {} 23 | 24 | impl Spec for ExecCredentialSpec { 25 | type Status = ExecCredentialStatus; 26 | type Header = DefaultHeader; 27 | 28 | fn metadata() -> &'static Crd { 29 | &CREDENTIAL_API 30 | } 31 | } 32 | 33 | #[derive(Deserialize, Serialize, Debug, Default, Clone)] 34 | #[serde(rename_all = "camelCase")] 35 | pub struct ExecCredentialStatus { 36 | pub expiration_timestamp: String, 37 | pub token: String, 38 | } 39 | 40 | impl Status for ExecCredentialStatus {} 41 | -------------------------------------------------------------------------------- /src/k8-types/src/core/pod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use serde::Deserialize; 4 | use serde::Serialize; 5 | use serde_json::Value as DynamicObject; 6 | 7 | use crate::Crd; 8 | use crate::CrdNames; 9 | use crate::DefaultHeader; 10 | use crate::Env; 11 | use crate::Spec; 12 | use crate::Status; 13 | 14 | const POD_API: Crd = Crd { 15 | group: "core", 16 | version: "v1", 17 | names: CrdNames { 18 | kind: "Pod", 19 | plural: "pods", 20 | singular: "pod", 21 | }, 22 | }; 23 | 24 | impl Spec for PodSpec { 25 | type Status = PodStatus; 26 | type Header = DefaultHeader; 27 | 28 | fn metadata() -> &'static Crd { 29 | &POD_API 30 | } 31 | } 32 | 33 | #[derive(Deserialize, Serialize, Debug, Default, Clone, Eq, PartialEq)] 34 | #[serde(rename_all = "camelCase", default)] 35 | pub struct PodSpec { 36 | pub volumes: Vec, 37 | pub containers: Vec, 38 | pub restart_policy: Option, 39 | pub service_account_name: Option, 40 | pub service_account: Option, 41 | pub node_name: Option, 42 | pub termination_grace_period_seconds: Option, 43 | pub dns_policy: Option, 44 | pub security_context: Option, 45 | pub scheduler_name: Option, 46 | pub node_selector: Option>, 47 | pub priority: Option, 48 | pub priority_class_name: Option, 49 | } 50 | 51 | #[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq)] 52 | pub enum PodRestartPolicy { 53 | Always, 54 | Never, 55 | OnFailure, 56 | } 57 | impl Default for PodRestartPolicy { 58 | fn default() -> Self { 59 | Self::Always // https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#restart-policy 60 | } 61 | } 62 | 63 | #[derive(Deserialize, Serialize, Debug, Default, Clone, Eq, PartialEq)] 64 | #[serde(rename_all = "camelCase", default)] 65 | pub struct PodSecurityContext { 66 | pub fs_group: Option, 67 | pub run_as_group: Option, 68 | pub run_as_non_root: Option, 69 | pub run_as_user: Option, 70 | pub sysctls: Vec, 71 | } 72 | 73 | #[derive(Deserialize, Serialize, Debug, Default, Clone, Eq, PartialEq)] 74 | #[serde(rename_all = "camelCase")] 75 | pub struct Sysctl { 76 | pub name: String, 77 | pub value: String, 78 | } 79 | 80 | #[derive(Deserialize, Serialize, Default, Debug, Clone, Eq, PartialEq)] 81 | #[serde(rename_all = "camelCase", default)] 82 | pub struct ContainerSpec { 83 | pub name: String, 84 | pub args: Vec, 85 | pub command: Vec, 86 | pub ports: Vec, 87 | pub image: Option, 88 | pub image_pull_policy: Option, // TODO: should be enum 89 | pub volume_mounts: Vec, 90 | pub env: Vec, 91 | pub resources: Option, 92 | pub termination_mssage_path: Option, 93 | pub termination_message_policy: Option, 94 | pub tty: Option, 95 | pub liveness_probe: Option, 96 | } 97 | 98 | #[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq)] 99 | pub enum ImagePullPolicy { 100 | Always, 101 | Never, 102 | IfNotPresent, 103 | } 104 | 105 | impl Default for ImagePullPolicy { 106 | fn default() -> Self { 107 | Self::Always // https://kubernetes.io/docs/concepts/containers/images/#updating-images 108 | } 109 | } 110 | 111 | #[derive(Deserialize, Serialize, Default, Debug, Clone, Eq, PartialEq)] 112 | #[serde(rename_all = "camelCase", default)] 113 | pub struct Probe { 114 | pub exec: Option, 115 | pub failure_threshold: Option, 116 | pub initial_delay_seconds: Option, 117 | pub period_seconds: Option, 118 | pub success_threshold: Option, 119 | pub tcp_socket: Option, 120 | pub timeout_seconds: Option, 121 | } 122 | 123 | #[derive(Deserialize, Serialize, Default, Debug, Clone, Eq, PartialEq)] 124 | #[serde(rename_all = "camelCase", default)] 125 | pub struct ExecAction { 126 | pub command: Vec, 127 | } 128 | 129 | #[derive(Deserialize, Serialize, Default, Debug, Clone, Eq, PartialEq)] 130 | #[serde(rename_all = "camelCase", default)] 131 | pub struct TcpSocketAction { 132 | pub host: String, 133 | pub port: u16, 134 | } 135 | 136 | #[derive(Deserialize, Serialize, Default, Debug, Clone, Eq, PartialEq)] 137 | #[serde(rename_all = "camelCase", default)] 138 | pub struct ResourceRequirements { 139 | pub limits: DynamicObject, 140 | pub requests: DynamicObject, 141 | } 142 | 143 | #[derive(Deserialize, Serialize, Default, Debug, Clone, Eq, PartialEq)] 144 | #[serde(rename_all = "camelCase")] 145 | pub struct ContainerPortSpec { 146 | pub container_port: u16, 147 | pub name: Option, 148 | pub protocol: Option, // TODO: This should be enum 149 | } 150 | 151 | impl ContainerPortSpec { 152 | pub fn new>(container_port: u16, name: T) -> Self { 153 | ContainerPortSpec { 154 | container_port, 155 | name: Some(name.into()), 156 | protocol: None, 157 | } 158 | } 159 | } 160 | 161 | #[derive(Deserialize, Serialize, Default, Debug, Clone, Eq, PartialEq)] 162 | #[serde(rename_all = "camelCase")] 163 | pub struct VolumeSpec { 164 | pub name: String, 165 | #[serde(skip_serializing_if = "Option::is_none")] 166 | pub secret: Option, 167 | #[serde(skip_serializing_if = "Option::is_none")] 168 | pub config_map: Option, 169 | #[serde(skip_serializing_if = "Option::is_none")] 170 | pub persistent_volume_claim: Option, 171 | #[serde(skip_serializing_if = "Option::is_none")] 172 | pub empty_dir: Option, 173 | #[serde(skip_serializing_if = "Option::is_none")] 174 | pub csi: Option, 175 | } 176 | 177 | #[derive(Deserialize, Serialize, Default, Debug, Clone, Eq, PartialEq)] 178 | #[serde(rename_all = "camelCase")] 179 | pub struct VolumeMount { 180 | pub mount_path: String, 181 | pub mount_propagation: Option, 182 | pub name: String, 183 | pub read_only: Option, 184 | pub sub_path: Option, 185 | } 186 | 187 | #[derive(Deserialize, Serialize, Default, Debug, Clone, Eq, PartialEq)] 188 | #[serde(rename_all = "camelCase")] 189 | pub struct SecretVolumeSpec { 190 | pub default_mode: u16, 191 | pub secret_name: String, 192 | pub optional: Option, 193 | } 194 | 195 | #[derive(Deserialize, Serialize, Default, Debug, Clone, Eq, PartialEq)] 196 | #[serde(rename_all = "camelCase")] 197 | pub struct ConfigMapVolumeSource { 198 | pub default_mode: Option, 199 | pub items: Option>, 200 | pub name: Option, 201 | pub optional: Option, 202 | } 203 | #[derive(Deserialize, Serialize, Default, Debug, Clone, Eq, PartialEq)] 204 | #[serde(rename_all = "camelCase")] 205 | pub struct KeyToPath { 206 | pub key: String, 207 | pub mode: Option, 208 | pub path: String, 209 | } 210 | 211 | #[derive(Deserialize, Serialize, Default, Debug, Clone, Eq, PartialEq)] 212 | #[serde(rename_all = "camelCase")] 213 | pub struct PersistentVolumeClaimVolumeSource { 214 | claim_name: String, 215 | read_only: Option, 216 | } 217 | 218 | #[derive(Deserialize, Serialize, Default, Debug, Clone)] 219 | #[serde(rename_all = "camelCase")] 220 | pub struct PodStatus { 221 | pub phase: String, 222 | #[serde(rename = "hostIP")] 223 | pub host_ip: String, 224 | #[serde(rename = "podIP")] 225 | pub pod_ip: Option, 226 | pub start_time: String, 227 | pub container_statuses: Vec, 228 | } 229 | 230 | #[derive(Deserialize, Serialize, Default, Debug, Clone, Eq, PartialEq)] 231 | #[serde(rename_all = "camelCase")] 232 | pub struct EmptyDirVolumeSource { 233 | #[serde(skip_serializing_if = "Option::is_none")] 234 | pub medium: Option, 235 | #[serde(skip_serializing_if = "Option::is_none")] 236 | pub size_limit: Option, 237 | } 238 | 239 | // ref https://kubernetes.io/docs/concepts/storage/volumes/#csi 240 | #[derive(Deserialize, Serialize, Default, Debug, Clone, Eq, PartialEq)] 241 | #[serde(rename_all = "camelCase")] 242 | pub struct CsiVolumeSource { 243 | #[serde(skip_serializing_if = "Option::is_none")] 244 | pub driver: Option, 245 | #[serde(skip_serializing_if = "Option::is_none")] 246 | pub fs_type: Option, 247 | pub read_only: bool, 248 | #[serde(skip_serializing_if = "Option::is_none")] 249 | pub volume_attributes: Option, 250 | #[serde(skip_serializing_if = "Option::is_none")] 251 | pub volume_handle: Option, 252 | } 253 | 254 | #[derive(Deserialize, Serialize, Default, Debug, Clone, Eq, PartialEq)] 255 | #[serde(rename_all = "camelCase")] 256 | pub struct CsiVolumeAttributes { 257 | #[serde(skip_serializing_if = "Option::is_none")] 258 | pub secret_provider_class: Option, 259 | } 260 | 261 | impl Status for PodStatus {} 262 | 263 | #[derive(Deserialize, Serialize, Debug, Clone)] 264 | #[serde(rename_all = "camelCase")] 265 | pub struct ContainerStatus { 266 | pub name: String, 267 | pub state: ContainerState, 268 | pub ready: bool, 269 | pub restart_count: i32, 270 | pub image: String, 271 | #[serde(rename = "imageID")] 272 | pub image_id: String, 273 | #[serde(rename = "containerID")] 274 | pub container_id: Option, 275 | } 276 | 277 | #[derive(Deserialize, Serialize, Debug, Clone)] 278 | #[serde(rename_all = "camelCase")] 279 | pub struct ContainerState { 280 | pub running: Option, 281 | } 282 | 283 | #[derive(Deserialize, Serialize, Debug, Clone)] 284 | #[serde(rename_all = "camelCase")] 285 | pub struct ContainerStateRunning { 286 | pub started_at: String, 287 | } 288 | -------------------------------------------------------------------------------- /src/k8-types/src/core/secret.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use serde::Deserialize; 4 | use serde::Serialize; 5 | 6 | use crate::Crd; 7 | use crate::CrdNames; 8 | use crate::Header; 9 | use crate::Spec; 10 | use crate::Status; 11 | 12 | // 13 | // Secret Object 14 | const SECRET_API: Crd = Crd { 15 | group: "core", 16 | version: "v1", 17 | names: CrdNames { 18 | kind: "Secret", 19 | plural: "secrets", 20 | singular: "secret", 21 | }, 22 | }; 23 | 24 | impl Spec for SecretSpec { 25 | type Status = SecretStatus; 26 | type Header = SecretHeader; 27 | 28 | fn metadata() -> &'static Crd { 29 | &SECRET_API 30 | } 31 | } 32 | 33 | #[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Default, Clone)] 34 | #[serde(rename_all = "camelCase")] 35 | pub struct SecretSpec {} 36 | 37 | #[derive(Deserialize, Serialize, Default, Eq, PartialEq, Debug, Clone)] 38 | #[serde(rename_all = "camelCase")] 39 | pub struct SecretStatus {} 40 | 41 | impl Status for SecretStatus {} 42 | 43 | #[derive(Deserialize, Serialize, Debug, Default, Clone)] 44 | #[serde(rename_all = "camelCase")] 45 | pub struct SecretHeader { 46 | #[serde(default)] 47 | pub data: BTreeMap, 48 | #[serde(rename = "type")] 49 | pub ty: String, 50 | } 51 | 52 | impl Header for SecretHeader {} 53 | -------------------------------------------------------------------------------- /src/k8-types/src/core/service.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use serde::Serialize; 3 | use std::collections::HashMap; 4 | 5 | use crate::default_store_spec; 6 | use crate::Crd; 7 | use crate::CrdNames; 8 | use crate::DefaultHeader; 9 | use crate::Spec; 10 | use crate::Status; 11 | 12 | const SERVICE_API: Crd = Crd { 13 | group: "core", 14 | version: "v1", 15 | names: CrdNames { 16 | kind: "Service", 17 | plural: "services", 18 | singular: "service", 19 | }, 20 | }; 21 | 22 | #[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Default, Clone)] 23 | #[serde(rename_all = "camelCase", default)] 24 | pub struct ServiceSpec { 25 | #[serde(rename = "clusterIP")] 26 | pub cluster_ip: String, 27 | #[serde(rename = "externalIPs")] 28 | pub external_ips: Vec, 29 | #[serde(rename = "loadBalancerIP")] 30 | pub load_balancer_ip: Option, 31 | pub r#type: Option, 32 | pub external_name: Option, 33 | pub external_traffic_policy: Option, 34 | pub ports: Vec, 35 | pub selector: Option>, 36 | } 37 | 38 | impl Spec for ServiceSpec { 39 | type Status = ServiceStatus; 40 | type Header = DefaultHeader; 41 | 42 | fn metadata() -> &'static Crd { 43 | &SERVICE_API 44 | } 45 | 46 | fn make_same(&mut self, other: &Self) { 47 | if other.cluster_ip.is_empty() { 48 | "".clone_into(&mut self.cluster_ip); 49 | } 50 | } 51 | } 52 | 53 | default_store_spec!(ServiceSpec, ServiceStatus, "Service"); 54 | 55 | #[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Default, Clone)] 56 | #[serde(rename_all = "camelCase")] 57 | pub struct ServicePort { 58 | pub name: Option, 59 | pub node_port: Option, 60 | pub port: u16, 61 | pub target_port: Option, 62 | } 63 | 64 | #[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Clone)] 65 | #[serde(untagged)] 66 | pub enum TargetPort { 67 | Number(u16), 68 | Name(String), 69 | } 70 | 71 | impl std::fmt::Display for TargetPort { 72 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 73 | match self { 74 | Self::Number(value) => write!(f, "{}", value), 75 | Self::Name(value) => write!(f, "{}", value), 76 | } 77 | } 78 | } 79 | 80 | #[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Default, Clone)] 81 | #[serde(rename_all = "camelCase", default)] 82 | pub struct ServiceStatus { 83 | pub load_balancer: LoadBalancerStatus, 84 | } 85 | 86 | impl Status for ServiceStatus {} 87 | 88 | #[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Clone)] 89 | pub enum ExternalTrafficPolicy { 90 | Local, 91 | Cluster, 92 | } 93 | 94 | #[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Clone)] 95 | pub enum LoadBalancerType { 96 | ExternalName, 97 | #[allow(clippy::upper_case_acronyms)] 98 | ClusterIP, 99 | NodePort, 100 | LoadBalancer, 101 | } 102 | 103 | #[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Default, Clone)] 104 | #[serde(rename_all = "camelCase", default)] 105 | pub struct LoadBalancerStatus { 106 | pub ingress: Vec, 107 | } 108 | 109 | impl LoadBalancerStatus { 110 | /// find any ip or host 111 | pub fn find_any_ip_or_host(&self) -> Option<&str> { 112 | self.ingress.iter().find_map(|ingress| ingress.host_or_ip()) 113 | } 114 | } 115 | 116 | #[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Default, Clone)] 117 | #[serde(rename_all = "camelCase")] 118 | pub struct LoadBalancerIngress { 119 | pub hostname: Option, 120 | pub ip: Option, 121 | pub ip_mode: Option, 122 | } 123 | 124 | impl LoadBalancerIngress { 125 | /// return either host or ip 126 | pub fn host_or_ip(&self) -> Option<&str> { 127 | if let Some(host) = &self.hostname { 128 | Some(host) 129 | } else if let Some(ip) = &self.ip { 130 | Some(ip) 131 | } else { 132 | None 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/k8-types/src/core/service_account.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use serde::Serialize; 3 | 4 | use crate::default_store_spec; 5 | use crate::Crd; 6 | use crate::CrdNames; 7 | use crate::DefaultHeader; 8 | use crate::Spec; 9 | use crate::Status; 10 | 11 | const API: Crd = Crd { 12 | group: "core", 13 | version: "v1", 14 | names: CrdNames { 15 | kind: "ServiceAccount", 16 | plural: "serviceaccounts", 17 | singular: "serviceaccount", 18 | }, 19 | }; 20 | 21 | #[derive(Deserialize, Serialize, Debug, Default, Clone)] 22 | #[serde(rename_all = "camelCase")] 23 | pub struct ServiceAccountSpec {} 24 | 25 | impl Spec for ServiceAccountSpec { 26 | type Status = ServiceAccountStatus; 27 | type Header = DefaultHeader; 28 | fn metadata() -> &'static Crd { 29 | &API 30 | } 31 | } 32 | 33 | default_store_spec!(ServiceAccountSpec, ServiceAccountStatus, "ServiceAccount"); 34 | 35 | #[derive(Deserialize, Serialize, Eq, PartialEq, Debug, Default, Clone)] 36 | #[serde(rename_all = "camelCase", default)] 37 | pub struct ServiceAccountStatus {} 38 | 39 | impl Status for ServiceAccountStatus {} 40 | -------------------------------------------------------------------------------- /src/k8-types/src/crd.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! # CRD Definition 3 | //! 4 | //! Interface to the CRD header definition in K8 key value store 5 | //! 6 | #[derive(Debug)] 7 | pub struct Crd { 8 | pub group: &'static str, 9 | pub version: &'static str, 10 | pub names: CrdNames, 11 | } 12 | 13 | #[derive(Debug)] 14 | pub struct CrdNames { 15 | pub kind: &'static str, 16 | pub plural: &'static str, 17 | pub singular: &'static str, 18 | } 19 | 20 | pub const GROUP: &str = "fluvio.infinyon.com"; 21 | pub const V1: &str = "v1"; 22 | -------------------------------------------------------------------------------- /src/k8-types/src/int_or_string.rs: -------------------------------------------------------------------------------- 1 | use std::{str::FromStr, convert::Infallible}; 2 | 3 | /// See: https://github.com/kubernetes/apimachinery/blob/master/pkg/util/intstr/intstr.go 4 | /// Int32OrString is a type that can hold an int32 or a string. 5 | /// When used in JSON or YAML marshalling and unmarshalling, it produces or consumes the inner type. 6 | /// This allows you to have, for example, a JSON field that can accept a name or number. 7 | #[derive(Clone, Debug, Eq, PartialEq)] 8 | pub enum Int32OrString { 9 | Int(i32), 10 | String(String), 11 | } 12 | 13 | impl Default for Int32OrString { 14 | fn default() -> Self { 15 | Int32OrString::Int(0) 16 | } 17 | } 18 | impl FromStr for Int32OrString { 19 | type Err = Infallible; 20 | fn from_str(s: &str) -> Result { 21 | match s.parse::() { 22 | Ok(i) => Ok(Int32OrString::Int(i)), 23 | Err(_) => Ok(Int32OrString::String(s.to_string())), 24 | } 25 | } 26 | } 27 | 28 | impl From for Int32OrString { 29 | fn from(f: i32) -> Self { 30 | Int32OrString::Int(f) 31 | } 32 | } 33 | 34 | impl<'de> serde::Deserialize<'de> for Int32OrString { 35 | fn deserialize(deserializer: D) -> Result 36 | where 37 | D: serde::Deserializer<'de>, 38 | { 39 | struct Visitor; 40 | 41 | impl serde::de::Visitor<'_> for Visitor { 42 | type Value = Int32OrString; 43 | 44 | fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 45 | write!(formatter, "enum Int32OrString") 46 | } 47 | 48 | fn visit_i32(self, v: i32) -> Result 49 | where 50 | E: serde::de::Error, 51 | { 52 | Ok(Int32OrString::Int(v)) 53 | } 54 | 55 | fn visit_i64(self, v: i64) -> Result 56 | where 57 | E: serde::de::Error, 58 | { 59 | if v < i64::from(i32::MIN) || v > i64::from(i32::MAX) { 60 | return Err(serde::de::Error::invalid_value( 61 | serde::de::Unexpected::Signed(v), 62 | &"a 32-bit integer", 63 | )); 64 | } 65 | 66 | #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] 67 | Ok(Int32OrString::Int(v as i32)) 68 | } 69 | 70 | fn visit_u64(self, v: u64) -> Result 71 | where 72 | E: serde::de::Error, 73 | { 74 | #[allow(clippy::cast_sign_loss)] 75 | { 76 | if v > i32::MAX as u64 { 77 | return Err(serde::de::Error::invalid_value( 78 | serde::de::Unexpected::Unsigned(v), 79 | &"a 32-bit integer", 80 | )); 81 | } 82 | } 83 | 84 | #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] 85 | Ok(Int32OrString::Int(v as i32)) 86 | } 87 | 88 | fn visit_str(self, v: &str) -> Result 89 | where 90 | E: serde::de::Error, 91 | { 92 | self.visit_string(v.to_string()) 93 | } 94 | 95 | fn visit_string(self, v: String) -> Result 96 | where 97 | E: serde::de::Error, 98 | { 99 | Ok(Int32OrString::String(v)) 100 | } 101 | } 102 | 103 | deserializer.deserialize_any(Visitor) 104 | } 105 | } 106 | 107 | impl serde::Serialize for Int32OrString { 108 | fn serialize(&self, serializer: S) -> Result 109 | where 110 | S: serde::Serializer, 111 | { 112 | match self { 113 | Int32OrString::Int(i) => i.serialize(serializer), 114 | Int32OrString::String(s) => s.serialize(serializer), 115 | } 116 | } 117 | } 118 | 119 | #[cfg(test)] 120 | mod test { 121 | 122 | use serde_json::json; 123 | 124 | use crate::Int32OrString; 125 | 126 | #[test] 127 | fn test_int_serde() { 128 | let int_value = json!(100); 129 | 130 | let int_or_string: Int32OrString = 131 | serde_json::from_value(int_value.clone()).expect("failed deserialization"); 132 | assert_eq!(int_or_string, Int32OrString::Int(100)); 133 | let serialization = serde_json::to_value(&int_or_string).expect("failed serialization"); 134 | assert_eq!(int_value, serialization); 135 | } 136 | 137 | #[test] 138 | fn test_invalid_float_serde() { 139 | let int_value = json!(2.5); 140 | 141 | let _error = serde_json::from_value::(int_value) 142 | .expect_err("float should not be deserialized"); 143 | } 144 | 145 | #[test] 146 | fn test_str_serde() { 147 | let str_value = json!("25%"); 148 | 149 | let int_or_string: Int32OrString = 150 | serde_json::from_value(str_value.clone()).expect("failed deserialization"); 151 | assert_eq!(int_or_string, Int32OrString::String("25%".into())); 152 | 153 | let serialization = serde_json::to_value(&int_or_string).expect("failed serialization"); 154 | assert_eq!(str_value, serialization); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/k8-types/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod crd; 2 | mod int_or_string; 3 | mod metadata; 4 | pub mod options; 5 | pub mod store; 6 | #[cfg(feature = "core")] 7 | pub mod core; 8 | #[cfg(feature = "app")] 9 | pub mod app; 10 | #[cfg(feature = "storage")] 11 | pub mod storage; 12 | #[cfg(feature = "batch")] 13 | pub mod batch; 14 | 15 | pub use self::crd::*; 16 | pub use self::metadata::*; 17 | pub use self::spec_def::*; 18 | 19 | mod spec_def { 20 | 21 | use std::fmt::Debug; 22 | 23 | use serde::de::DeserializeOwned; 24 | use serde::Deserialize; 25 | use serde::Serialize; 26 | 27 | use super::Crd; 28 | 29 | pub trait Status: 30 | Sized + Debug + Clone + Default + Serialize + DeserializeOwned + Send + Sync 31 | { 32 | } 33 | 34 | pub trait Header: 35 | Sized + Debug + Clone + Default + Serialize + DeserializeOwned + Send + Sync 36 | { 37 | } 38 | 39 | /// Kubernetes Spec 40 | pub trait Spec: 41 | Sized + Debug + Clone + Default + Serialize + DeserializeOwned + Send + Sync 42 | { 43 | type Status: Status; 44 | 45 | type Header: Header; 46 | 47 | /// if true, spec is namespaced 48 | const NAME_SPACED: bool = true; 49 | 50 | /// return uri for single instance 51 | fn metadata() -> &'static Crd; 52 | 53 | fn label() -> &'static str { 54 | Self::metadata().names.kind 55 | } 56 | 57 | fn api_version() -> String { 58 | let metadata = Self::metadata(); 59 | if metadata.group == "core" { 60 | return metadata.version.to_owned(); 61 | } 62 | format!("{}/{}", metadata.group, metadata.version) 63 | } 64 | 65 | fn kind() -> String { 66 | Self::metadata().names.kind.to_owned() 67 | } 68 | 69 | /// in case of applying, we have some fields that are generated 70 | /// or override. So need to special logic to reset them so we can do proper comparison 71 | fn make_same(&mut self, _other: &Self) {} 72 | } 73 | 74 | #[derive(Deserialize, Serialize, Debug, Default, Clone)] 75 | pub struct DefaultHeader {} 76 | 77 | impl Header for DefaultHeader {} 78 | } 79 | 80 | pub use int_or_string::Int32OrString; 81 | -------------------------------------------------------------------------------- /src/k8-types/src/options.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | 3 | /// goes as query parameter 4 | #[derive(Serialize, Default, Debug)] 5 | #[serde(rename_all = "camelCase")] 6 | pub struct ListOptions { 7 | pub pretty: Option, 8 | #[serde(rename = "continue")] 9 | pub continu: Option, 10 | pub field_selector: Option, 11 | pub include_uninitialized: Option, 12 | pub label_selector: Option, 13 | pub limit: Option, 14 | pub resource_version: Option, 15 | pub timeout_seconds: Option, 16 | pub watch: Option, 17 | } 18 | 19 | #[derive(Serialize, Debug)] 20 | #[serde(rename_all = "camelCase")] 21 | pub struct DeleteOptions { 22 | pub kind: &'static str, 23 | pub api_version: &'static str, 24 | #[serde(skip_serializing_if = "Option::is_none")] 25 | pub pretty: Option, 26 | #[serde(skip_serializing_if = "Option::is_none")] 27 | pub dry_run: Option, 28 | #[serde(skip_serializing_if = "Option::is_none")] 29 | pub grace_period_seconds: Option, 30 | #[serde(skip_serializing_if = "Option::is_none")] 31 | pub propagation_policy: Option, 32 | } 33 | 34 | impl Default for DeleteOptions { 35 | fn default() -> Self { 36 | Self { 37 | kind: "DeleteOptions", 38 | api_version: "v1", 39 | pretty: None, 40 | dry_run: None, 41 | grace_period_seconds: None, 42 | propagation_policy: None, 43 | } 44 | } 45 | } 46 | #[derive(Serialize, Debug)] 47 | pub enum PropogationPolicy { 48 | Orphan, 49 | Background, 50 | Foreground, 51 | } 52 | 53 | #[derive(Serialize, Default)] 54 | #[serde(rename_all = "camelCase")] 55 | pub struct Precondition { 56 | pub uid: String, 57 | } 58 | 59 | #[cfg(test)] 60 | mod test { 61 | 62 | use super::ListOptions; 63 | 64 | #[test] 65 | fn test_list_query() { 66 | let opt = ListOptions { 67 | pretty: Some(true), 68 | watch: Some(true), 69 | ..Default::default() 70 | }; 71 | 72 | let qs = serde_qs::to_string(&opt).unwrap(); 73 | assert_eq!(qs, "pretty=true&watch=true") 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/k8-types/src/storage/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod storage_class; 2 | -------------------------------------------------------------------------------- /src/k8-types/src/storage/storage_class.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use serde::Serialize; 3 | 4 | use crate::{Crd, CrdNames, Header, Spec, Status}; 5 | 6 | const STORAGE_API: Crd = Crd { 7 | group: "storage.k8s.io", 8 | version: "v1", 9 | names: CrdNames { 10 | kind: "StorageClass", 11 | plural: "storageclasses", 12 | singular: "storageclass", 13 | }, 14 | }; 15 | 16 | #[derive(Deserialize, Serialize, Debug, Default, Clone)] 17 | #[serde(rename_all = "camelCase")] 18 | pub struct StorageClassSpec {} 19 | 20 | impl Spec for StorageClassSpec { 21 | type Status = StorageClassStatus; 22 | type Header = StorageClassHeader; 23 | const NAME_SPACED: bool = false; 24 | 25 | fn metadata() -> &'static Crd { 26 | &STORAGE_API 27 | } 28 | } 29 | 30 | #[derive(Deserialize, Serialize, Debug, Default, Clone)] 31 | #[serde(rename_all = "camelCase")] 32 | pub struct StorageClassHeader { 33 | pub allow_volume_expansion: Option, 34 | pub provisioner: String, 35 | pub reclaim_policy: String, 36 | pub volume_binding_mode: String, 37 | } 38 | 39 | impl Header for StorageClassHeader {} 40 | 41 | #[derive(Deserialize, Serialize, Default, Debug, Clone)] 42 | #[serde(rename_all = "camelCase")] 43 | pub struct StorageClassStatus {} 44 | 45 | impl Status for StorageClassStatus {} 46 | -------------------------------------------------------------------------------- /src/k8-types/src/store.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::fmt::Debug; 3 | use std::fmt::Display; 4 | use std::io::Error as IoError; 5 | 6 | use crate::K8Obj; 7 | use crate::ObjectMeta; 8 | use crate::Spec; 9 | 10 | // Spec that can store in meta store 11 | pub trait StoreSpec: Sized + Default + Debug + Clone { 12 | type K8Spec: Spec; 13 | type Status: Sized + Clone + Default + Debug; 14 | type Key: Ord + Clone + Debug + ToString; 15 | type Owner: StoreSpec; 16 | 17 | const LABEL: &'static str; 18 | 19 | // convert kubernetes objects into KV value 20 | fn convert_from_k8(k8_obj: K8Obj) -> Result>, IoError>; 21 | } 22 | 23 | /// Metadata object. Used to be KVObject int sc-core 24 | #[derive(Debug, Clone, Eq, PartialEq)] 25 | pub struct MetaItem 26 | where 27 | S: StoreSpec, 28 | { 29 | pub spec: S, 30 | pub status: S::Status, 31 | pub key: S::Key, 32 | pub ctx: MetaItemContext, 33 | } 34 | 35 | impl MetaItem 36 | where 37 | S: StoreSpec, 38 | { 39 | pub fn new(key: J, spec: S, status: S::Status, ctx: MetaItemContext) -> Self 40 | where 41 | J: Into, 42 | { 43 | Self { 44 | key: key.into(), 45 | spec, 46 | status, 47 | ctx, 48 | } 49 | } 50 | 51 | pub fn with_ctx(mut self, ctx: MetaItemContext) -> Self { 52 | self.ctx = ctx; 53 | self 54 | } 55 | 56 | pub fn key(&self) -> &S::Key { 57 | &self.key 58 | } 59 | 60 | pub fn key_owned(&self) -> S::Key { 61 | self.key.clone() 62 | } 63 | 64 | pub fn my_key(self) -> S::Key { 65 | self.key 66 | } 67 | 68 | pub fn spec(&self) -> &S { 69 | &self.spec 70 | } 71 | pub fn status(&self) -> &S::Status { 72 | &self.status 73 | } 74 | 75 | pub fn set_status(&mut self, status: S::Status) { 76 | self.status = status; 77 | } 78 | 79 | pub fn ctx(&self) -> &MetaItemContext { 80 | &self.ctx 81 | } 82 | 83 | pub fn set_ctx(&mut self, ctx: MetaItemContext) { 84 | self.ctx = ctx; 85 | } 86 | 87 | pub fn parts(self) -> (S::Key, S, MetaItemContext) { 88 | (self.key, self.spec, self.ctx) 89 | } 90 | 91 | pub fn is_owned(&self, uid: &str) -> bool { 92 | match &self.ctx.parent_ctx { 93 | Some(parent) => parent.uid == uid, 94 | None => false, 95 | } 96 | } 97 | 98 | pub fn with_spec(key: J, spec: S) -> Self 99 | where 100 | J: Into, 101 | { 102 | Self::new( 103 | key.into(), 104 | spec, 105 | S::Status::default(), 106 | MetaItemContext::default(), 107 | ) 108 | } 109 | } 110 | 111 | impl fmt::Display for MetaItem 112 | where 113 | S: StoreSpec, 114 | S::Key: Display, 115 | { 116 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 117 | write!(f, "MetaItem {} key: {}", S::LABEL, self.key()) 118 | } 119 | } 120 | 121 | impl From> for (S::Key, S, S::Status) 122 | where 123 | S: StoreSpec, 124 | { 125 | fn from(val: MetaItem) -> Self { 126 | (val.key, val.spec, val.status) 127 | } 128 | } 129 | 130 | #[derive(Default, Debug, Eq, PartialEq, Clone)] 131 | pub struct MetaItemContext { 132 | pub item_ctx: Option, 133 | pub parent_ctx: Option, 134 | } 135 | 136 | impl MetaItemContext { 137 | pub fn with_ctx(mut self, ctx: ObjectMeta) -> Self { 138 | self.item_ctx = Some(ctx); 139 | self 140 | } 141 | 142 | pub fn with_parent_ctx(mut self, ctx: ObjectMeta) -> Self { 143 | self.parent_ctx = Some(ctx); 144 | self 145 | } 146 | 147 | pub fn make_parent_ctx(&self) -> Self { 148 | if self.item_ctx.is_some() { 149 | Self::default().with_parent_ctx(self.item_ctx.as_ref().unwrap().clone()) 150 | } else { 151 | Self::default() 152 | } 153 | } 154 | } 155 | 156 | /// define default store spec assuming key is string 157 | #[macro_export] 158 | macro_rules! default_store_spec { 159 | ($spec:ident,$status:ident,$name:expr) => { 160 | impl $crate::store::StoreSpec for $spec { 161 | const LABEL: &'static str = $name; 162 | 163 | type K8Spec = Self; 164 | type Status = $status; 165 | type Key = String; 166 | type Owner = Self; 167 | 168 | fn convert_from_k8( 169 | k8_obj: $crate::K8Obj, 170 | ) -> Result>, std::io::Error> { 171 | let ctx = 172 | $crate::store::MetaItemContext::default().with_ctx(k8_obj.metadata.clone()); 173 | Ok(Some($crate::store::MetaItem::new( 174 | k8_obj.metadata.name, 175 | k8_obj.spec, 176 | k8_obj.status, 177 | ctx, 178 | ))) 179 | } 180 | } 181 | }; 182 | } 183 | --------------------------------------------------------------------------------