diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 000000000..0d6b03761 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,34 @@ +name: Coverage + +on: + push: + #branches: + # - master + +jobs: + rust: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + - uses: Swatinem/rust-cache@v1 + - uses: AbsaOSS/k3d-action@v2 + name: "Create Single Cluster" + with: + cluster-name: "test-cluster-1" + args: >- + --agents 1 + --image docker.io/rancher/k3s:v1.22.4-k3s1 + --k3s-arg "--no-deploy=traefik,servicelb,metrics-server@server:*" + - name: Run cargo-tarpaulin + uses: actions-rs/tarpaulin@v0.1 + with: + version: '0.18.5' + out-type: Xml + args: ' -- --test-threads 1' + - uses: codecov/codecov-action@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b9749b327..f773c3e11 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,25 +19,3 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: body_path: release.txt - code-coverage: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Install stable toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - - name: Run cargo-tarpaulin - uses: actions-rs/tarpaulin@v0.1 - with: - version: '0.18.0' - out-type: Lcov - timeout: 600 - args: '--all -e tests -- --test-threads 1' - - name: Coveralls Finished - uses: coverallsapp/github-action@master - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - path-to-lcov: ./lcov.info diff --git a/Makefile b/Makefile index ba4394b20..a9975644f 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ test: test-kubernetes: cargo test --lib --all -- --ignored # also run tests that fail on github actions - cargo test -p kube --features=derive -- --ignored + cargo test -p kube --lib --features=derive,runtime -- --ignored cargo run -p kube-examples --example crd_derive cargo run -p kube-examples --example crd_api diff --git a/kube-core/src/admission.rs b/kube-core/src/admission.rs index 0a39c5ebc..5230fb248 100644 --- a/kube-core/src/admission.rs +++ b/kube-core/src/admission.rs @@ -31,7 +31,6 @@ pub struct SerializePatchError(#[source] serde_json::Error); /// Failed to convert `AdmissionReview` into `AdmissionRequest`. pub struct ConvertAdmissionReviewError; - /// The `kind` field in [`TypeMeta`]. pub const META_KIND: &str = "AdmissionReview"; /// The `api_version` field in [`TypeMeta`] on the v1 version. diff --git a/kube-derive/src/lib.rs b/kube-derive/src/lib.rs index 871012e12..9581ef44a 100644 --- a/kube-derive/src/lib.rs +++ b/kube-derive/src/lib.rs @@ -209,8 +209,10 @@ mod custom_resource; /// If you have to override a lot, [you can opt-out of schema-generation entirely](#kubeschema--mode) /// /// ## Advanced Features +/// /// - **embedding k8s-openapi types** can be done by enabling the `schemars` feature of `k8s-openapi` from [`0.13.0`](https://github.com/Arnavion/k8s-openapi/blob/master/CHANGELOG.md#v0130-2021-08-09) /// - **adding validation** via [validator crate](https://github.com/Keats/validator) is supported from `schemars` >= [`0.8.5`](https://github.com/GREsau/schemars/blob/master/CHANGELOG.md#085---2021-09-20) +/// - **generating rust code from schemas** can be done via [kopium](https://github.com/kube-rs/kopium) and is supported on stable crds (> 1.16 kubernetes) /// /// ### Validation Caveats /// The supported **`#[validate]` attrs also exist as `#[schemars]` attrs** so you can use those directly if you do not require the validation to run client-side (in your code). diff --git a/kube/src/lib.rs b/kube/src/lib.rs index 63cf6d04c..c042b1e93 100644 --- a/kube/src/lib.rs +++ b/kube/src/lib.rs @@ -179,26 +179,37 @@ pub use crate::core::{CustomResourceExt, Resource, ResourceExt}; #[doc(inline)] pub use kube_core as core; - +// Tests that require a cluster and the complete feature set +// Can be run with `cargo test -p kube --lib --features=runtime,derive -- --ignored` #[cfg(test)] +#[cfg(all(feature = "derive", feature = "client"))] mod test { - #[cfg(all(feature = "derive", feature = "client"))] + use crate::{Api, Client, CustomResourceExt, ResourceExt}; + use kube_derive::CustomResource; + use schemars::JsonSchema; + use serde::{Deserialize, Serialize}; + + #[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)] + #[kube(group = "clux.dev", version = "v1", kind = "Foo", namespaced)] + #[kube(status = "FooStatus")] + #[kube(scale = r#"{"specReplicasPath":".spec.replicas", "statusReplicasPath":".status.replicas"}"#)] + #[kube(crates(kube_core = "crate::core"))] // for dev-dep test structure + pub struct FooSpec { + name: String, + info: Option, + replicas: isize, + } + + #[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] + pub struct FooStatus { + is_bad: bool, + replicas: isize, + } + #[tokio::test] #[ignore] // needs kubeconfig - async fn convenient_custom_resource() { - use crate::{ - core::{ApiResource, DynamicObject, GroupVersionKind}, - Api, Client, CustomResource, - }; - use schemars::JsonSchema; - use serde::{Deserialize, Serialize}; - - #[derive(Clone, Debug, CustomResource, Deserialize, Serialize, JsonSchema)] - #[kube(group = "clux.dev", version = "v1", kind = "Foo", namespaced)] - #[kube(crates(kube_core = "crate::core"))] - struct FooSpec { - foo: String, - } + async fn custom_resource_generates_correct_core_structs() { + use crate::core::{ApiResource, DynamicObject, GroupVersionKind}; let client = Client::try_default().await.unwrap(); let gvk = GroupVersionKind::gvk("clux.dev", "v1", "Foo"); @@ -209,4 +220,82 @@ mod test { // make sure they return the same url_path through their impls assert_eq!(a1.resource_url(), a2.resource_url()); } + + use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition; + #[tokio::test] + #[ignore] // needs cluster (creates + patches foo crd) + #[cfg(all(feature = "derive", feature = "runtime"))] + async fn derived_resource_queriable() -> Result<(), Box> { + use crate::{ + core::params::{DeleteParams, Patch, PatchParams}, + runtime::wait::{await_condition, conditions}, + }; + let client = Client::try_default().await?; + let ssapply = PatchParams::apply("kube").force(); + let crds: Api = Api::all(client.clone()); + // Server-side apply CRD + crds.patch("foos.clux.dev", &ssapply, &Patch::Apply(Foo::crd())) + .await?; + // Wait for it to be ready: + let establish = await_condition(crds, "foos.clux.dev", conditions::is_crd_established()); + let _ = tokio::time::timeout(std::time::Duration::from_secs(10), establish).await?; + // Use it + let foos: Api = Api::default_namespaced(client.clone()); + // Apply from generated struct + let foo = Foo::new("baz", FooSpec { + name: "baz".into(), + info: Some("old baz".into()), + replicas: 3, + }); + let o = foos.patch("baz", &ssapply, &Patch::Apply(&foo)).await?; + assert_eq!(o.spec.name, "baz"); + // Apply from partial json! + let patch = serde_json::json!({ + "apiVersion": "clux.dev/v1", + "kind": "Foo", + "spec": { + "name": "foo", + "replicas": 2 + } + }); + let o2 = foos.patch("baz", &ssapply, &Patch::Apply(patch)).await?; + assert_eq!(o2.spec.replicas, 2); + assert_eq!(foos.get_scale("baz").await?.spec.unwrap().replicas, Some(2)); + assert!(foos.get_status("baz").await?.status.is_none()); // nothing has set this + foos.delete("baz", &DeleteParams::default()).await?; + Ok(()) + } + + #[tokio::test] + #[ignore] // needs cluster (fetches api resources, and lists all) + #[cfg(all(feature = "derive", feature = "runtime"))] + async fn dynamic_resources_discoverable() -> Result<(), Box> { + use crate::{ + core::DynamicObject, + discovery::{verbs, Discovery, Scope}, + }; + let client = Client::try_default().await?; + + let discovery = Discovery::new(client.clone()).run().await?; + for group in discovery.groups() { + for (ar, caps) in group.recommended_resources() { + if !caps.supports_operation(verbs::LIST) { + continue; + } + let api: Api = if caps.scope == Scope::Namespaced { + Api::default_namespaced_with(client.clone(), &ar) + } else { + Api::all_with(client.clone(), &ar) + }; + println!("{}/{} : {}", group.name(), ar.version, ar.kind); + let list = api.list(&Default::default()).await?; + for item in list.items { + let name = item.name(); + let ns = item.metadata.namespace.map(|s| s + "/").unwrap_or_default(); + println!("\t\t{}{}", ns, name); + } + } + } + Ok(()) + } } diff --git a/tarpaulin.toml b/tarpaulin.toml new file mode 100644 index 000000000..cc3b002f8 --- /dev/null +++ b/tarpaulin.toml @@ -0,0 +1,12 @@ +# Usage cargo tarpaulin -- --test-threads 1 + +[one_pass_coverage] +workspace = true +features = "kube/derive kube/runtime" +color = "Always" +ignored = true +timeout = "600s" +exclude = ["integration"] +# NB: skipping Doctests because they are slow to build and generally marked no_run +run-types = ["Tests"] +generate = ["Json"]