Skip to content

Commit

Permalink
Remove API operations-related codegen.
Browse files Browse the repository at this point in the history
Updating the codegen for v1.28 is non-trivial, and nobody seems to use it
over kube anyway. It doesn't seem worth delaying v1.28 support for or
keeping around in general.

Operations are still parsed minimally because they're used to identify
resource scope.

The tests now use clientset code based on the original operations code,
which is generic over the resource type like kube but still sans-io as before.
So that is an option for users who still need the sans-io-style operations,
though it only handles common CRUD by default and not bespoke operations
like reading pod logs. If there is demand it could be released as
a separate crate.

Ref #145
  • Loading branch information
Arnavion committed Sep 1, 2023
1 parent 7364ee0 commit 40f2892
Show file tree
Hide file tree
Showing 753 changed files with 890 additions and 226,572 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ Corresponding Kubernetes API server versions:

- BREAKING CHANGE: This version partially reverts the change in v0.9.0 that made `k8s_openapi::apimachinery::pkg::apis::meta::v1::WatchEvent<T>` require `T: k8s_openapi::Resource`; now it only requires `T: serde::Deserialize<'de>` once more. This has been done to make it possible to use `WatchEvent` with custom user-provided resource types that do not implement `k8s_openapi::Resource`.

The `k8s_openapi::Resource` bound in v0.9.0 was added to be able to enforce that the `WatchEvent::<T>::Bookmark` events contain the correct `apiVersion` and `kind` fields for the specified `T` during deserialization. Without the bound now, it is no longer possible to do that. So it is now possible to deserialize, say, a `WatchEvent::<Pod>::Bookmark` as a `WatchEvent::<Node>::Bookmark` without any runtime error. Take care to deserialize `watch_*` API responses into the right `k8s_openapi::WatchResponse<T>` type, such as by relying on the returned `k8s_openapi::ResponseBody<T>` as documented in the crate docs.
The `k8s_openapi::Resource` bound in v0.9.0 was added to be able to enforce that the `WatchEvent::<T>::Bookmark` events contain the correct `apiVersion` and `kind` fields for the specified `T` during deserialization. Without the bound now, it is no longer possible to do that. So it is now possible to deserialize, say, a `WatchEvent::<Pod>::Bookmark` as a `WatchEvent::<Node>::Bookmark` without any runtime error. Take care to deserialize `watch_*` API responses into the right `crate::clientset::WatchResponse<T>` type, such as by relying on the returned `k8s_openapi::ResponseBody<T>` as documented in the crate docs.

- BREAKING CHANGE: The `bytes` dependency has been updated to match the `tokio` v1 ecosystem.

Expand Down
7 changes: 0 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,14 @@ chrono = { version = "0.4.1", default-features = false, features = [
"alloc", # for chrono::DateTime::<Utc>::to_rfc3339_opts
"serde", # for chrono::DateTime<Utc>: serde::Deserialize, serde::Serialize
] }
http = { version = "0.2", optional = true, default-features = false }
percent-encoding = { version = "2", optional = true, default-features = false }
schemars = { version = "0.8", optional = true, default-features = false }
serde = { version = "1", default-features = false }
serde_json = { version = "1", default-features = false, features = [
"alloc", # "serde_json requires that either `std` (default) or `alloc` feature is enabled"
] }
serde-value = { version = "0.7", default-features = false }
url = { version = "2", optional = true, default-features = false }

[features]
default = ["api"]

api = ["http", "percent-encoding", "url"] # Enables API operation functions and response types. If disabled, only the resource types will be exported.

# Each feature corresponds to a supported version of Kubernetes
earliest = ["v1_22"]
v1_22 = []
Expand Down
55 changes: 1 addition & 54 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
This crate is a Rust Kubernetes API client. It contains bindings for the resources and operations in the Kubernetes client API, auto-generated from the OpenAPI spec.
This crate is a Rust Kubernetes API client. It contains bindings for the resources in the Kubernetes client API, auto-generated from the OpenAPI spec.

[crates.io](https://crates.io/crates/k8s-openapi)

Expand All @@ -17,59 +17,6 @@ The upstream OpenAPI spec is not written by hand; it's itself generated from the
Since this crate uses a custom code generator, it is able to work around these mistakes and emit correct bindings. See the list of fixes [here](https://github.com/Arnavion/k8s-openapi/blob/master/k8s-openapi-codegen/src/fixups/upstream_bugs.rs) and the breakdown of fixes applied to each Kubernetes version [here.](https://github.com/Arnavion/k8s-openapi/blob/master/k8s-openapi-codegen/src/supported_version.rs)


### Better code organization, closer to the Go API

Upstream's generated clients tend to place all the API operations in massive top-level modules. For example, the Python client contains a single [CoreV1Api class](https://github.com/kubernetes-client/python/blob/master/kubernetes/client/api/core_v1_api.py) with a couple of hundred methods, one for each `core/v1` API operation like `list_namespaced_pod`.

This crate instead associates these functions with the corresponding resource type. The `list_namespaced_pod` function is accessed as `Pod::list`, where `Pod` is the resource type for pods. This is similar to the Go API's [PodInterface::List](https://godoc.org/k8s.io/client-go/kubernetes/typed/core/v1#PodInterface)

Since all types are under the `io.k8s` namespace, this crate also removes those two components from the module path. The end result is that the path to `Pod` is `k8s_openapi::api::core::v1::Pod`, similar to the Go path `k8s.io/api/core/v1.Pod`.


### Better handling of optional parameters, for a more Rust-like and ergonomic API

Almost every API operation has optional parameters. For example, v1.23's `Pod::list` API has one required parameter (the namespace) and eight optional parameters.

The clients of other languages use language features to allow the caller to not specify all these parameters when invoking the function. The Python client's functions parse optional parameters from `**kwargs`. The C# client's functions assign default values to these parameters in the function definition.

Since Rust does not have such a feature, auto-generated Rust clients use `Option<>` parameters to represent optional parameters. This ends up requiring callers to pass in a lot of `None` parameters just to satisfy the compiler. Invoking the `Pod::list` of an auto-generated client would look like:

```rust
// List all pods in the kube-system namespace
Pod::list("kube-system", None, None, None, None, None, None, None, None);

// List all pods in the kube-system namespace with label foo=bar
Pod::list("kube-system", None, None, /* label_selector */ Some("foo=bar"), None, None, None, None, None);
```

Apart from being hard to read, you could easily make a typo and pass in `Some("foo=bar")` for one of the five other optional String parameters without any errors from the compiler.

This crate moves all optional parameters to separate structs, one for each API. Each of these structs implements `Default` and the names of the fields match the function parameter names, so that the above calls look like:

```rust
// List all pods in the kube-system namespace
Pod::list("kube-system", Default::default());

// List all pods in the kube-system namespace with label foo=bar
Pod::list("kube-system", ListOptional { label_selector: Some("foo=bar"), ..Default::default() });
```

The second example uses struct update syntax to explicitly set one field of the struct and `Default` the rest.


### Not restricted to a single HTTP client implementation, and works with both synchronous and asynchronous HTTP clients

Auto-generated clients have to choose between providing a synchronous or asynchronous API, and have to choose what kind of HTTP client they want to use internally (`hyper::Client`, `reqwest::Client`, `reqwest::blocking::Client`, etc). If you want to use a different HTTP client, you cannot use the crate.

This crate is instead based on the [sans-io approach](https://sans-io.readthedocs.io/) popularized by Python for network protocols and applications.

For example, the `Pod::list` function does not return `Result<ListResponse<Pod>>` or `impl Future<Output = ListResponse<Pod>>`. It returns an `http::Request<Vec<u8>>` with the URL path, query string, request headers and request body filled out. You are free to execute this `http::Request` using any HTTP client you want to use.

After you've executed the request, your HTTP client will give you the response's `http::StatusCode` and some `[u8]` bytes of the response body. To parse these into a `ListResponse<Pod>`, you use that type's `fn try_from_parts(http::StatusCode, &[u8]) -> Result<(Self, usize), crate::ResponseError>` function. The result is either a successful `ListResponse<Pod>` value, or an error that the response is incomplete and you need to get more bytes of the response body and try again, or a fatal error because the response is invalid JSON.

To make this easier, the `Pod::list` function also returns a callback `fn(http::StatusCode) -> ResponseBody<ListResponse<Pod>>`. `ResponseBody` is a type that contains its own internal growable byte buffer, so you can use it if you don't want to manage a byte buffer yourself. It also ensures that you deserialize the response to the appropriate type corresponding to the request, ie `ListResponse<Pod>`, and not any other. See the crate docs for more details about this type.


### Supports more versions of Kubernetes

Official clients tend to support only the three latest versions of Kubernetes. This crate supports a few more.
Expand Down
25 changes: 8 additions & 17 deletions ci/per_version.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,22 @@ set -euo pipefail

export CARGO_TARGET_DIR="$PWD/target-tests-v$K8S_OPENAPI_ENABLED_VERSION"

for api_feature in 'yes' 'no'; do
case "$api_feature" in
'yes') features_args='';;
'no') features_args='--no-default-features';;
esac
echo "### k8s-openapi:${K8S_OPENAPI_ENABLED_VERSION}:lib-tests ###"
RUST_BACKTRACE=full cargo test

echo "### k8s-openapi:${K8S_OPENAPI_ENABLED_VERSION}:${api_feature}:lib-tests ###"
RUST_BACKTRACE=full cargo test $features_args
echo "### k8s-openapi:${K8S_OPENAPI_ENABLED_VERSION}:clippy ###"
cargo clippy -- -D warnings

echo "### k8s-openapi:${K8S_OPENAPI_ENABLED_VERSION}:${api_feature}:clippy ###"
cargo clippy $features_args -- -D warnings

echo "### k8s-openapi:${K8S_OPENAPI_ENABLED_VERSION}:${api_feature}:doc ###"
RUSTDOCFLAGS='-D warnings' cargo doc --no-deps $features_args
done
echo "### k8s-openapi:${K8S_OPENAPI_ENABLED_VERSION}:doc ###"
RUSTDOCFLAGS='-D warnings' cargo doc --no-deps

echo "### k8s-openapi:${K8S_OPENAPI_ENABLED_VERSION}:tests ###"
RUST_BACKTRACE=full ./test.sh "$K8S_OPENAPI_ENABLED_VERSION" run-tests


echo '### k8s-openapi-tests:clippy ###'
test_version_feature_arg="--features test_v${K8S_OPENAPI_ENABLED_VERSION//./_}"
pushd k8s-openapi-tests
cargo clippy --tests $test_version_feature_arg
cargo clippy --tests --features "test_v${K8S_OPENAPI_ENABLED_VERSION//./_}"
popd
pushd k8s-openapi-tests-macro-deps
cargo clippy --tests $test_version_feature_arg
cargo clippy --tests --features "test_v${K8S_OPENAPI_ENABLED_VERSION//./_}"
popd
Loading

0 comments on commit 40f2892

Please sign in to comment.