├── .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 | 
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