diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 2b11066e2..5d810c13b 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/rust:1.65.0-bullseye +FROM docker.io/rust:1.70.0-bullseye ENV DEBIAN_FRONTEND=noninteractive RUN apt update && apt upgrade -y diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c28eeb294..e63093e3a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -142,7 +142,7 @@ jobs: fail-fast: false matrix: # Run these tests against older clusters as well - k8s: [v1.23, latest] + k8s: [v1.24, latest] steps: - uses: actions/checkout@v4 - uses: actions-rs/toolchain@v1 @@ -216,7 +216,7 @@ jobs: - uses: nolar/setup-k3d-k3s@v1 with: - version: v1.23 + version: v1.24 # k3d-kube k3d-name: kube # Used to avoid rate limits when fetching the releases from k3s repo. diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 00f85dcb6..1545c32f7 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -28,7 +28,7 @@ jobs: cluster-name: "test-cluster-1" args: >- --agents 1 - --image docker.io/rancher/k3s:v1.23.4-k3s1 + --image docker.io/rancher/k3s:v1.24.4-k3s1 --k3s-arg "--no-deploy=traefik,servicelb,metrics-server@server:*" - name: Run cargo-tarpaulin uses: actions-rs/tarpaulin@v0.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index c67092a24..bae0b2d9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,15 +5,42 @@ UNRELEASED =================== - * see https://github.com/kube-rs/kube/compare/0.87.1...main + * see https://github.com/kube-rs/kube/compare/0.88.0...main + +[0.88.0](https://github.com/kube-rs/kube/releases/tag/0.88.0) / 2024-01-21 +=================== + + +## Kubernetes `v1_29` support via `k8s-openapi` [0.21](https://github.com/Arnavion/k8s-openapi/releases/tag/v0.21.0) +Please [upgrade k8s-openapi along with kube](https://kube.rs/upgrading/) to avoid conflicts. + +## What's Changed +### Added +* Add type meta data for list types by @Danil-Grigorev in https://github.com/kube-rs/kube/pull/1380 +### Changed +* Bump MSRV to 1.70 by @clux in https://github.com/kube-rs/kube/pull/1384 +* Upgrade `k8s-openapi` for Kubernetes `v1_29` support by @clux in https://github.com/kube-rs/kube/pull/1394 + +[0.87.2](https://github.com/kube-rs/kube/releases/tag/0.87.2) / 2023-12-22 +=================== + + +## What's Changed +### Added +* Add support for `LogParams::since_time` by @clux in https://github.com/kube-rs/kube/pull/1342 +* Provide cluster info to exec plugins by @aviramha in https://github.com/kube-rs/kube/pull/1331 +* Allow setting a description on a derived CRD by @sbernauer in https://github.com/kube-rs/kube/pull/1359 +### Changed +* Bump MSRV from 1.64 to 1.65 by @clux in https://github.com/kube-rs/kube/pull/1353 +* Switch from `jsonpath_lib` to `jsonpath-rust` by @ilya-bobyr in https://github.com/kube-rs/kube/pull/1345 [0.87.1](https://github.com/kube-rs/kube/releases/tag/0.87.1) / 2023-11-01 =================== ## Headlines -- fixed a `Controller` issue with reconciliation requests disappearing when using `concurrency` #1324 -- improved `Client` with better exec auth behaviour #1320, timeout control #1314, and socks5 proxy handling #1311 -- small changes to an unstable streams feature #1304, and a a derive property that is now illegal with `syn` 2 #1307 +- fixed a `Controller` issue with reconciliation requests disappearing when using `concurrency` [#1324](https://github.com/kube-rs/kube/issues/1324) +- improved `Client` with better exec auth behaviour [#1320](https://github.com/kube-rs/kube/issues/1320), timeout control [#1314](https://github.com/kube-rs/kube/issues/1314), and socks5 proxy handling [#1311](https://github.com/kube-rs/kube/issues/1311) +- small changes to an unstable streams feature [#1304](https://github.com/kube-rs/kube/issues/1304), and a a derive property that is now illegal with `syn` 2 [#1307](https://github.com/kube-rs/kube/issues/1307) Big thanks to everyone involved 🎃 diff --git a/README.md b/README.md index 831ac2f98..1a61c1be8 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # kube-rs [![Crates.io](https://img.shields.io/crates/v/kube.svg)](https://crates.io/crates/kube) -[![Rust 1.65](https://img.shields.io/badge/MSRV-1.65-dea584.svg)](https://github.com/rust-lang/rust/releases/tag/1.65.0) -[![Tested against Kubernetes v1_23 and above](https://img.shields.io/badge/MK8SV-v1_23-326ce5.svg)](https://kube.rs/kubernetes-version) +[![Rust 1.70](https://img.shields.io/badge/MSRV-1.70-dea584.svg)](https://github.com/rust-lang/rust/releases/tag/1.70.0) +[![Tested against Kubernetes v1_24 and above](https://img.shields.io/badge/MK8SV-v1_24-326ce5.svg)](https://kube.rs/kubernetes-version) [![Best Practices](https://bestpractices.coreinfrastructure.org/projects/5413/badge)](https://bestpractices.coreinfrastructure.org/projects/5413) [![Discord chat](https://img.shields.io/discord/500028886025895936.svg?logo=discord&style=plastic)](https://discord.gg/tokio) @@ -16,8 +16,8 @@ Select a version of `kube` along with the generated [k8s-openapi](https://github ```toml [dependencies] -kube = { version = "0.87.1", features = ["runtime", "derive"] } -k8s-openapi = { version = "0.20.0", features = ["latest"] } +kube = { version = "0.88.0", features = ["runtime", "derive"] } +k8s-openapi = { version = "0.21.0", features = ["latest"] } ``` [Features are available](https://github.com/kube-rs/kube/blob/main/kube/Cargo.toml#L18). @@ -152,8 +152,8 @@ By default [rustls](https://github.com/ctz/rustls) is used for TLS, but `openssl ```toml [dependencies] -kube = { version = "0.87.1", default-features = false, features = ["client", "openssl-tls"] } -k8s-openapi = { version = "0.20.0", features = ["latest"] } +kube = { version = "0.88.0", default-features = false, features = ["client", "openssl-tls"] } +k8s-openapi = { version = "0.21.0", features = ["latest"] } ``` This will pull in `openssl` and `hyper-openssl`. If `default-features` is left enabled, you will pull in two TLS stacks, and the default will remain as `rustls`. diff --git a/e2e/Cargo.toml b/e2e/Cargo.toml index 399a9238c..7151459ae 100644 --- a/e2e/Cargo.toml +++ b/e2e/Cargo.toml @@ -19,7 +19,7 @@ path = "boot.rs" [features] latest = ["k8s-openapi/latest"] -mk8sv = ["k8s-openapi/v1_23"] +mk8sv = ["k8s-openapi/v1_24"] rustls = ["kube/rustls-tls"] openssl = ["kube/openssl-tls"] @@ -28,7 +28,7 @@ anyhow = "1.0.44" tracing = "0.1.36" tracing-subscriber = "0.3.3" futures = "0.3.17" -kube = { path = "../kube", version = "^0.87.1", default-features = false, features = ["client", "runtime", "ws", "admission", "gzip"] } -k8s-openapi = { version = "0.20.0", default-features = false } +kube = { path = "../kube", version = "^0.88.0", default-features = false, features = ["client", "runtime", "ws", "admission", "gzip"] } +k8s-openapi = { version = "0.21.0", default-features = false } serde_json = "1.0.68" tokio = { version = "1.14.0", features = ["full"] } diff --git a/examples/Cargo.toml b/examples/Cargo.toml index ef11801d2..7c047a610 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -27,13 +27,13 @@ latest = ["k8s-openapi/latest"] [dev-dependencies] tokio-util = "0.7.0" assert-json-diff = "2.0.1" -garde = { version = "0.16.1", default-features = false, features = ["derive"] } +garde = { version = "0.17.0", default-features = false, features = ["derive"] } anyhow = "1.0.44" futures = "0.3.17" -jsonpath-rust = "0.3.4" -kube = { path = "../kube", version = "^0.87.1", default-features = false, features = ["admission"] } -kube-derive = { path = "../kube-derive", version = "^0.87.1", default-features = false } # only needed to opt out of schema -k8s-openapi = { version = "0.20.0", default-features = false } +jsonpath-rust = "0.4.0" +kube = { path = "../kube", version = "^0.88.0", default-features = false, features = ["admission"] } +kube-derive = { path = "../kube-derive", version = "^0.88.0", default-features = false } # only needed to opt out of schema +k8s-openapi = { version = "0.21.0", default-features = false } serde = { version = "1.0.130", features = ["derive"] } serde_json = "1.0.68" serde_yaml = "0.9.19" diff --git a/examples/crd_derive.rs b/examples/crd_derive.rs index 6715d5616..17a742600 100644 --- a/examples/crd_derive.rs +++ b/examples/crd_derive.rs @@ -17,6 +17,7 @@ use serde::{Deserialize, Serialize}; plural = "fooz", root = "FooCrd", namespaced, + doc = "Custom resource representing a Foo", status = "FooStatus", derive = "PartialEq", derive = "Default", @@ -119,7 +120,7 @@ fn verify_crd() { ], "schema": { "openAPIV3Schema": { - "description": "Auto-generated derived type for MyFoo via `CustomResource`", + "description": "Custom resource representing a Foo", "properties": { "spec": { "description": "Our spec for Foo\n\nA struct with our chosen Kind will be created for us, using the following kube attrs", diff --git a/examples/crd_derive_schema.rs b/examples/crd_derive_schema.rs index 25efb32ff..8c58afacb 100644 --- a/examples/crd_derive_schema.rs +++ b/examples/crd_derive_schema.rs @@ -85,8 +85,11 @@ pub struct FooSpec { #[serde(default)] #[schemars(schema_with = "set_listable_schema")] set_listable: Vec, + // Field with CEL validation + #[serde(default)] + #[schemars(schema_with = "cel_validations")] + cel_validated: Option, } - // https://kubernetes.io/docs/reference/using-api/server-side-apply/#merge-strategy fn set_listable_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { serde_json::from_value(serde_json::json!({ @@ -101,6 +104,18 @@ fn set_listable_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::sche .unwrap() } +// https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#validation-rules +fn cel_validations(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + serde_json::from_value(serde_json::json!({ + "type": "string", + "x-kubernetes-validations": [{ + "rule": "self != 'illegal'", + "message": "string cannot be illegal" + }] + })) + .unwrap() +} + fn default_value() -> String { "default_value".into() } @@ -144,6 +159,7 @@ async fn main() -> Result<()> { // Empty listables to be patched in later default_listable: Default::default(), set_listable: Default::default(), + cel_validated: Default::default(), }); // Set up dynamic resource to test using raw values. @@ -190,10 +206,8 @@ async fn main() -> Result<()> { assert_eq!(err.code, 422); assert_eq!(err.reason, "Invalid"); assert_eq!(err.status, "Failure"); - assert_eq!( - err.message, - "Foo.clux.dev \"qux\" is invalid: spec.non_nullable: Required value" - ); + assert!(err.message.contains("clux.dev \"qux\" is invalid")); + assert!(err.message.contains("spec.non_nullable: Required value")); } _ => panic!(), } @@ -213,8 +227,39 @@ async fn main() -> Result<()> { assert_eq!(pres.spec.set_listable, vec![2, 3]); println!("{:?}", serde_json::to_value(pres.spec)); - delete_crd(client.clone()).await?; + // cel validation triggers: + let cel_patch = serde_json::json!({ + "apiVersion": "clux.dev/v1", + "kind": "Foo", + "spec": { + "cel_validated": Some("illegal") + } + }); + let cel_res = foos.patch("baz", &ssapply, &Patch::Apply(cel_patch)).await; + assert!(cel_res.is_err()); + match cel_res.err() { + Some(kube::Error::Api(err)) => { + assert_eq!(err.code, 422); + assert_eq!(err.reason, "Invalid"); + assert_eq!(err.status, "Failure"); + assert!(err.message.contains("Foo.clux.dev \"baz\" is invalid")); + assert!(err.message.contains("spec.cel_validated: Invalid value")); + assert!(err.message.contains("string cannot be illegal")); + } + _ => panic!(), + } + // cel validation happy: + let cel_patch_ok = serde_json::json!({ + "apiVersion": "clux.dev/v1", + "kind": "Foo", + "spec": { + "cel_validated": Some("legal") + } + }); + foos.patch("baz", &ssapply, &Patch::Apply(cel_patch_ok)).await?; + // all done + delete_crd(client.clone()).await?; Ok(()) } diff --git a/kube-client/Cargo.toml b/kube-client/Cargo.toml index d2d037911..9272b87e7 100644 --- a/kube-client/Cargo.toml +++ b/kube-client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kube-client" -version = "0.87.1" +version = "0.88.0" description = "Kubernetes client" authors = [ "clux ", @@ -12,7 +12,7 @@ repository = "https://github.com/kube-rs/kube" readme = "../README.md" keywords = ["kubernetes", "client",] categories = ["web-programming::http-client", "configuration", "network-programming", "api-bindings"] -rust-version = "1.65.0" +rust-version = "1.70.0" edition = "2021" [features] @@ -55,8 +55,8 @@ rustls = { version = "0.21.4", features = ["dangerous_configuration"], optional rustls-pemfile = { version = "1.0.0", optional = true } bytes = { version = "1.1.0", optional = true } tokio = { version = "1.14.0", features = ["time", "signal", "sync"], optional = true } -kube-core = { path = "../kube-core", version = "=0.87.1" } -jsonpath-rust = { version = "0.3.4", optional = true } +kube-core = { path = "../kube-core", version = "=0.88.0" } +jsonpath-rust = { version = "0.4.0", optional = true } tokio-util = { version = "0.7.0", optional = true, features = ["io", "codec"] } hyper = { version = "1.0.1", optional = true, features = ["client", "http1"] } hyper-rustls = { version = "0.24.0", optional = true } @@ -77,7 +77,7 @@ hyper-openssl = { version = "0.10.1", optional = true } form_urlencoded = { version = "1.2.0", optional = true } [dependencies.k8s-openapi] -version = "0.20.0" +version = "0.21.0" default-features = false features = [] @@ -90,6 +90,6 @@ tokio-test = "0.4.0" tower-test = "0.4.0" [dev-dependencies.k8s-openapi] -version = "0.20.0" +version = "0.21.0" default-features = false features = ["latest"] diff --git a/kube-client/src/api/mod.rs b/kube-client/src/api/mod.rs index 88342025b..554b96876 100644 --- a/kube-client/src/api/mod.rs +++ b/kube-client/src/api/mod.rs @@ -70,6 +70,11 @@ impl Api { /// Cluster level resources, or resources viewed across all namespaces /// /// This function accepts `K::DynamicType` so it can be used with dynamic resources. + /// + /// # Warning + /// + /// This variant **can only `list` and `watch` namespaced resources** and is commonly used with a `watcher`. + /// If you need to create/patch/replace/get on a namespaced resource, you need a separate `Api::namespaced`. pub fn all_with(client: Client, dyntype: &K::DynamicType) -> Self { let url = K::url_path(dyntype, None); Self { @@ -149,6 +154,11 @@ where /// use k8s_openapi::api::core::v1::Node; /// let api: Api = Api::all(client); /// ``` + /// + /// # Warning + /// + /// This variant **can only `list` and `watch` namespaced resources** and is commonly used with a `watcher`. + /// If you need to create/patch/replace/get on a namespaced resource, you need a separate `Api::namespaced`. pub fn all(client: Client) -> Self { Self::all_with(client, &K::DynamicType::default()) } diff --git a/kube-client/src/client/auth/mod.rs b/kube-client/src/client/auth/mod.rs index 74515a483..eaca2d8ea 100644 --- a/kube-client/src/client/auth/mod.rs +++ b/kube-client/src/client/auth/mod.rs @@ -17,7 +17,7 @@ use thiserror::Error; use tokio::sync::{Mutex, RwLock}; use tower::{filter::AsyncPredicate, BoxError}; -use crate::config::{AuthInfo, AuthProviderConfig, ExecConfig, ExecInteractiveMode}; +use crate::config::{AuthInfo, AuthProviderConfig, ExecAuthCluster, ExecConfig, ExecInteractiveMode}; #[cfg(feature = "oauth")] mod oauth; #[cfg(feature = "oauth")] pub use oauth::Error as OAuthError; @@ -98,6 +98,10 @@ pub enum Error { #[cfg_attr(docsrs, doc(cfg(feature = "oidc")))] #[error("failed OIDC: {0}")] Oidc(#[source] oidc_errors::Error), + + /// cluster spec missing while `provideClusterInfo` is true + #[error("Cluster spec must be populated when `provideClusterInfo` is true")] + ExecMissingClusterInfo, } #[derive(Debug, Clone)] @@ -521,6 +525,9 @@ pub struct ExecCredential { pub struct ExecCredentialSpec { #[serde(skip_serializing_if = "Option::is_none")] interactive: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + cluster: Option, } /// ExecCredentialStatus holds credentials for the transport to use. @@ -561,13 +568,20 @@ fn auth_exec(auth: &ExecConfig) -> Result { cmd.stdin(std::process::Stdio::piped()); } + let mut exec_credential_spec = ExecCredentialSpec { + interactive: Some(interactive), + cluster: None, + }; + + if auth.provide_cluster_info { + exec_credential_spec.cluster = Some(auth.cluster.clone().ok_or(Error::ExecMissingClusterInfo)?); + } + // Provide exec info to child process let exec_info = serde_json::to_string(&ExecCredential { api_version: auth.api_version.clone(), kind: "ExecCredential".to_string().into(), - spec: Some(ExecCredentialSpec { - interactive: Some(interactive), - }), + spec: Some(exec_credential_spec), status: None, }) .map_err(Error::AuthExecSerialize)?; diff --git a/kube-client/src/config/file_config.rs b/kube-client/src/config/file_config.rs index 4e658145e..1f051f0ca 100644 --- a/kube-client/src/config/file_config.rs +++ b/kube-client/src/config/file_config.rs @@ -9,6 +9,9 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use super::{KubeconfigError, LoadDataError}; +/// [`CLUSTER_EXTENSION_KEY`] is reserved in the cluster extensions list for exec plugin config. +const CLUSTER_EXTENSION_KEY: &str = "client.authentication.k8s.io/exec"; + /// [`Kubeconfig`] represents information on how to connect to a remote Kubernetes cluster /// /// Stored in `~/.kube/config` by default, but can be distributed across multiple paths in passed through `KUBECONFIG`. @@ -278,6 +281,19 @@ pub struct ExecConfig { #[serde(rename = "interactiveMode")] #[serde(skip_serializing_if = "Option::is_none")] pub interactive_mode: Option, + + /// ProvideClusterInfo determines whether or not to provide cluster information, + /// which could potentially contain very large CA data, to this exec plugin as a + /// part of the KUBERNETES_EXEC_INFO environment variable. By default, it is set + /// to false. Package k8s.io/client-go/tools/auth/exec provides helper methods for + /// reading this environment variable. + #[serde(default, rename = "provideClusterInfo")] + pub provide_cluster_info: bool, + + /// Cluster information to pass to the plugin. + /// Should be used only when `provide_cluster_info` is True. + #[serde(skip)] + pub cluster: Option, } /// ExecInteractiveMode define the interactity of the child process @@ -525,6 +541,58 @@ impl AuthInfo { } } +/// Cluster stores information to connect Kubernetes cluster used with auth plugins +/// that have `provideClusterInfo`` enabled. +/// This is a copy of [`kube::config::Cluster`] with certificate_authority passed as bytes without the path. +/// Taken from [clientauthentication/types.go#Cluster](https://github.com/kubernetes/client-go/blob/477cb782cf024bc70b7239f0dca91e5774811950/pkg/apis/clientauthentication/types.go#L73-L129) +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +#[serde(rename_all = "kebab-case")] +#[cfg_attr(test, derive(PartialEq, Eq))] +pub struct ExecAuthCluster { + /// The address of the kubernetes cluster (https://hostname:port). + #[serde(skip_serializing_if = "Option::is_none")] + pub server: Option, + /// Skips the validity check for the server's certificate. This will make your HTTPS connections insecure. + #[serde(skip_serializing_if = "Option::is_none")] + pub insecure_skip_tls_verify: Option, + /// PEM-encoded certificate authority certificates. Overrides `certificate_authority` + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(with = "base64serde")] + pub certificate_authority_data: Option>, + /// URL to the proxy to be used for all requests. + #[serde(skip_serializing_if = "Option::is_none")] + pub proxy_url: Option, + /// Name used to check server certificate. + /// + /// If `tls_server_name` is `None`, the hostname used to contact the server is used. + #[serde(skip_serializing_if = "Option::is_none")] + pub tls_server_name: Option, + /// This can be anything + #[serde(skip_serializing_if = "Option::is_none")] + pub config: Option, +} + +impl TryFrom<&Cluster> for ExecAuthCluster { + type Error = KubeconfigError; + + fn try_from(cluster: &crate::config::Cluster) -> Result { + let certificate_authority_data = cluster.load_certificate_authority()?; + Ok(Self { + server: cluster.server.clone(), + insecure_skip_tls_verify: cluster.insecure_skip_tls_verify, + certificate_authority_data, + proxy_url: cluster.proxy_url.clone(), + tls_server_name: cluster.tls_server_name.clone(), + config: cluster.extensions.as_ref().and_then(|extensions| { + extensions + .iter() + .find(|extension| extension.name == CLUSTER_EXTENSION_KEY) + .map(|extension| extension.extension.clone()) + }), + }) + } +} + fn load_from_base64_or_file>( value: &Option<&str>, file: &Option

, @@ -561,10 +629,39 @@ fn default_kube_path() -> Option { home::home_dir().map(|h| h.join(".kube").join("config")) } +mod base64serde { + use base64::Engine; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub fn serialize(v: &Option>, s: S) -> Result { + match v { + Some(v) => { + let encoded = base64::engine::general_purpose::STANDARD.encode(v); + String::serialize(&encoded, s) + } + None => >::serialize(&None, s), + } + } + + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result>, D::Error> { + let data = >::deserialize(d)?; + match data { + Some(data) => Ok(Some( + base64::engine::general_purpose::STANDARD + .decode(data.as_bytes()) + .map_err(serde::de::Error::custom)?, + )), + None => Ok(None), + } + } +} + #[cfg(test)] mod tests { + use crate::config::file_loader::ConfigLoader; + use super::*; - use serde_json::Value; + use serde_json::{json, Value}; use std::str::FromStr; #[test] @@ -822,4 +919,48 @@ password: kube_rs assert_eq!(authinfo_debug_output, expected_output) } + + #[tokio::test] + async fn authinfo_exec_provide_cluster_info() { + let config = r#" +apiVersion: v1 +clusters: +- cluster: + server: https://localhost:8080 + extensions: + - name: client.authentication.k8s.io/exec + extension: + audience: foo + other: bar + name: foo-cluster +contexts: +- context: + cluster: foo-cluster + user: foo-user + namespace: bar + name: foo-context +current-context: foo-context +kind: Config +users: +- name: foo-user + user: + exec: + apiVersion: client.authentication.k8s.io/v1alpha1 + args: + - arg-1 + - arg-2 + command: foo-command + provideClusterInfo: true +"#; + let kube_config = Kubeconfig::from_yaml(config).unwrap(); + let config_loader = ConfigLoader::load(kube_config, None, None, None).await.unwrap(); + let auth_info = config_loader.user; + let exec = auth_info.exec.unwrap(); + assert!(exec.provide_cluster_info); + let cluster = exec.cluster.unwrap(); + assert_eq!( + cluster.config.unwrap(), + json!({"audience": "foo", "other": "bar"}) + ); + } } diff --git a/kube-client/src/config/file_loader.rs b/kube-client/src/config/file_loader.rs index 2eeb1b01f..152793b03 100644 --- a/kube-client/src/config/file_loader.rs +++ b/kube-client/src/config/file_loader.rs @@ -83,13 +83,19 @@ impl ConfigLoader { .ok_or_else(|| KubeconfigError::LoadClusterOfContext(cluster_name.clone()))?; let user_name = user.unwrap_or(¤t_context.user); - let user = config + let mut user = config .auth_infos .iter() .find(|named_user| &named_user.name == user_name) .and_then(|named_user| named_user.auth_info.clone()) .ok_or_else(|| KubeconfigError::FindUser(user_name.clone()))?; + if let Some(exec_config) = &mut user.exec { + if exec_config.provide_cluster_info { + exec_config.cluster = Some((&cluster).try_into()?); + } + } + Ok(ConfigLoader { current_context, cluster, diff --git a/kube-client/src/config/mod.rs b/kube-client/src/config/mod.rs index c114a9f06..3d9e2be77 100644 --- a/kube-client/src/config/mod.rs +++ b/kube-client/src/config/mod.rs @@ -380,8 +380,8 @@ const DEFAULT_WRITE_TIMEOUT: Duration = Duration::from_secs(295); // Expose raw config structs pub use file_config::{ - AuthInfo, AuthProviderConfig, Cluster, Context, ExecConfig, ExecInteractiveMode, Kubeconfig, - NamedAuthInfo, NamedCluster, NamedContext, NamedExtension, Preferences, + AuthInfo, AuthProviderConfig, Cluster, Context, ExecAuthCluster, ExecConfig, ExecInteractiveMode, + Kubeconfig, NamedAuthInfo, NamedCluster, NamedContext, NamedExtension, Preferences, }; #[cfg(test)] diff --git a/kube-core/Cargo.toml b/kube-core/Cargo.toml index 8112a26b2..2fc4d5136 100644 --- a/kube-core/Cargo.toml +++ b/kube-core/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "kube-core" description = "Kube shared types, traits and client-less behavior" -version = "0.87.1" +version = "0.88.0" authors = [ "clux ", "kazk ", ] edition = "2021" -rust-version = "1.65.0" +rust-version = "1.70.0" license = "Apache-2.0" keywords = ["kubernetes", "apimachinery"] categories = ["api-bindings", "encoding", "parser-implementations"] @@ -36,12 +36,12 @@ chrono = { version = "0.4.19", default-features = false, features = ["clock"] } schemars = { version = "0.8.6", optional = true } [dependencies.k8s-openapi] -version = "0.20.0" +version = "0.21.0" default-features = false features = [] [dev-dependencies.k8s-openapi] -version = "0.20.0" +version = "0.21.0" default-features = false features = ["latest"] diff --git a/kube-core/src/metadata.rs b/kube-core/src/metadata.rs index d087d51b6..67edf6e16 100644 --- a/kube-core/src/metadata.rs +++ b/kube-core/src/metadata.rs @@ -17,6 +17,42 @@ pub struct TypeMeta { pub kind: String, } +impl TypeMeta { + /// Construct a new `TypeMeta` for the object list from the given resource. + /// + /// ``` + /// # use k8s_openapi::api::core::v1::Pod; + /// # use kube_core::TypeMeta; + /// + /// let type_meta = TypeMeta::list::(); + /// assert_eq!(type_meta.kind, "PodList"); + /// assert_eq!(type_meta.api_version, "v1"); + /// ``` + pub fn list>() -> Self { + TypeMeta { + api_version: K::api_version(&()).into(), + kind: K::kind(&()).to_string() + "List", + } + } + + /// Construct a new `TypeMeta` for the object from the given resource. + /// + /// ``` + /// # use k8s_openapi::api::core::v1::Pod; + /// # use kube_core::TypeMeta; + /// + /// let type_meta = TypeMeta::resource::(); + /// assert_eq!(type_meta.kind, "Pod"); + /// assert_eq!(type_meta.api_version, "v1"); + /// ``` + pub fn resource>() -> Self { + TypeMeta { + api_version: K::api_version(&()).into(), + kind: K::kind(&()).into(), + } + } +} + /// A generic representation of any object with `ObjectMeta`. /// /// It allows clients to get access to a particular `ObjectMeta` diff --git a/kube-core/src/object.rs b/kube-core/src/object.rs index c4e962ac6..2ebd3e634 100644 --- a/kube-core/src/object.rs +++ b/kube-core/src/object.rs @@ -21,7 +21,10 @@ pub struct ObjectList where T: Clone, { - // NB: kind and apiVersion can be set here, but no need for it atm + /// The type fields, always present + #[serde(flatten, default)] + pub types: TypeMeta, + /// ListMeta - only really used for its `resourceVersion` /// /// See [ListMeta](k8s_openapi::apimachinery::pkg::apis::meta::v1::ListMeta) @@ -50,11 +53,13 @@ impl ObjectList { /// # Example /// /// ``` - /// use kube::api::{ListMeta, ObjectList}; + /// use kube::api::{ListMeta, ObjectList, TypeMeta}; + /// use k8s_openapi::api::core::v1::Pod; /// + /// let types: TypeMeta = TypeMeta::list::(); /// let metadata: ListMeta = Default::default(); /// let items = vec![1, 2, 3]; - /// let objectlist = ObjectList { metadata, items }; + /// # let objectlist = ObjectList { types, metadata, items }; /// /// let first = objectlist.iter().next(); /// println!("First element: {:?}", first); // prints "First element: Some(1)" @@ -68,11 +73,13 @@ impl ObjectList { /// # Example /// /// ``` - /// use kube::api::{ObjectList, ListMeta}; + /// use kube::api::{ListMeta, ObjectList, TypeMeta}; + /// use k8s_openapi::api::core::v1::Pod; /// + /// let types: TypeMeta = TypeMeta::list::(); /// let metadata: ListMeta = Default::default(); /// let items = vec![1, 2, 3]; - /// let mut objectlist = ObjectList { metadata, items }; + /// # let mut objectlist = ObjectList { types, metadata, items }; /// /// let mut first = objectlist.iter_mut().next(); /// @@ -300,7 +307,9 @@ pub struct NotUsed {} #[cfg(test)] mod test { - use super::{ApiResource, HasSpec, HasStatus, NotUsed, Object, Resource}; + use k8s_openapi::apimachinery::pkg::apis::meta::v1::{ListMeta, ObjectMeta}; + + use super::{ApiResource, HasSpec, HasStatus, NotUsed, Object, ObjectList, Resource, TypeMeta}; use crate::resource::ResourceExt; #[test] @@ -347,4 +356,42 @@ mod test { assert_eq!(PodSimple::kind(&ar), "Pod"); assert_eq!(PodSimple::group(&ar), ""); } + + #[test] + fn k8s_object_list() { + use k8s_openapi::api::core::v1::Pod; + // by grabbing the ApiResource info from the Resource trait + let ar = ApiResource::erase::(&()); + assert_eq!(ar.group, ""); + assert_eq!(ar.kind, "Pod"); + let podlist: ObjectList = ObjectList { + types: TypeMeta { + api_version: ar.api_version, + kind: ar.kind + "List", + }, + metadata: ListMeta { ..Default::default() }, + items: vec![Pod { + metadata: ObjectMeta { + name: Some("test".into()), + namespace: Some("dev".into()), + ..ObjectMeta::default() + }, + spec: None, + status: None, + }], + }; + + assert_eq!(&podlist.types.kind, "PodList"); + assert_eq!(&podlist.types.api_version, "v1"); + + let mypod = &podlist.items[0]; + let meta = mypod.meta(); + assert_eq!(&mypod.metadata, meta); + assert_eq!(meta.namespace.as_ref().unwrap(), "dev"); + assert_eq!(meta.name.as_ref().unwrap(), "test"); + assert_eq!(mypod.namespace().unwrap(), "dev"); + assert_eq!(mypod.name_unchecked(), "test"); + assert!(mypod.status.is_none()); + assert!(mypod.spec.is_none()); + } } diff --git a/kube-derive/Cargo.toml b/kube-derive/Cargo.toml index 276626e9f..127c93313 100644 --- a/kube-derive/Cargo.toml +++ b/kube-derive/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "kube-derive" description = "Custom derives for the kube kubernetes crates" -version = "0.87.1" +version = "0.88.0" authors = [ "clux ", "kazk ", ] edition = "2021" -rust-version = "1.65.0" +rust-version = "1.70.0" license = "Apache-2.0" repository = "https://github.com/kube-rs/kube" readme = "../README.md" @@ -29,7 +29,7 @@ proc-macro = true serde = { version = "1.0.130", features = ["derive"] } serde_yaml = "0.9.19" kube = { path = "../kube", version = "<1.0.0, >=0.61.0", features = ["derive", "client"] } -k8s-openapi = { version = "0.20.0", default-features = false, features = ["latest"] } +k8s-openapi = { version = "0.21.0", default-features = false, features = ["latest"] } schemars = { version = "0.8.6", features = ["chrono"] } chrono = { version = "0.4.19", default-features = false } trybuild = "1.0.48" diff --git a/kube-derive/README.md b/kube-derive/README.md index fff2bf065..c0bdbdd8b 100644 --- a/kube-derive/README.md +++ b/kube-derive/README.md @@ -6,7 +6,7 @@ Add the `derive` feature to `kube`: ```toml [dependencies] -kube = { version = "0.87.1", feature = ["derive"] } +kube = { version = "0.88.0", feature = ["derive"] } ``` ## Usage diff --git a/kube-derive/src/custom_resource.rs b/kube-derive/src/custom_resource.rs index 177abe7de..891369bfc 100644 --- a/kube-derive/src/custom_resource.rs +++ b/kube-derive/src/custom_resource.rs @@ -10,6 +10,7 @@ struct KubeAttrs { group: String, version: String, kind: String, + doc: Option, #[darling(rename = "root")] kind_struct: Option, /// lowercase plural of kind (inferred if omitted) @@ -145,6 +146,7 @@ pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStrea kind, kind_struct, version, + doc, namespaced, derives, schema: schema_mode, @@ -239,7 +241,8 @@ pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStrea derive_paths.push(syn::parse_quote! { #schemars::JsonSchema }); } - let docstr = format!(" Auto-generated derived type for {ident} via `CustomResource`"); + let docstr = + doc.unwrap_or_else(|| format!(" Auto-generated derived type for {ident} via `CustomResource`")); let quoted_serde = Literal::string(&serde.to_token_stream().to_string()); let root_obj = quote! { #[doc = #docstr] diff --git a/kube-derive/src/lib.rs b/kube-derive/src/lib.rs index 7e296c3e1..68b068dad 100644 --- a/kube-derive/src/lib.rs +++ b/kube-derive/src/lib.rs @@ -141,6 +141,10 @@ mod custom_resource; /// ## `#[kube(category = "apps")]` /// Add a single category to `crd.spec.names.categories`. /// +/// ## `#[kube(doc = "description")]` +/// Sets the description of the schema in the generated CRD. If not specified +/// `Auto-generated derived type for {customResourceName} via CustomResource` will be used instead. +/// /// ## Example with all properties /// /// ```rust @@ -155,6 +159,7 @@ mod custom_resource; /// kind = "Foo", /// root = "FooCrd", /// namespaced, +/// doc = "Custom resource representing a Foo", /// status = "FooStatus", /// derive = "PartialEq", /// singular = "foot", diff --git a/kube-derive/tests/crd_schema_test.rs b/kube-derive/tests/crd_schema_test.rs index 48c1e7b45..d1c9116a7 100644 --- a/kube-derive/tests/crd_schema_test.rs +++ b/kube-derive/tests/crd_schema_test.rs @@ -1,7 +1,7 @@ #![recursion_limit = "256"] use assert_json_diff::assert_json_eq; -use chrono::{DateTime, NaiveDateTime, Utc}; +use chrono::{DateTime, NaiveDateTime, TimeZone, Utc}; use kube_derive::CustomResource; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -15,6 +15,7 @@ use std::collections::HashMap; kind = "Foo", category = "clux", namespaced, + doc = "Custom resource representing a Foo", derive = "PartialEq", shortname = "fo", shortname = "f" @@ -132,7 +133,7 @@ fn test_serialized_matches_expected() { nullable: None, nullable_skipped_with_default: None, nullable_with_default: None, - timestamp: DateTime::from_utc(NaiveDateTime::from_timestamp_opt(0, 0).unwrap(), Utc), + timestamp: TimeZone::from_utc_datetime(&Utc, &NaiveDateTime::from_timestamp_opt(0, 0).unwrap()), complex_enum: ComplexEnum::VariantOne { int: 23 }, untagged_enum_person: UntaggedEnumPerson::GenderAndAge(GenderAndAge { age: 42, @@ -195,7 +196,7 @@ fn test_crd_schema_matches_expected() { "additionalPrinterColumns": [], "schema": { "openAPIV3Schema": { - "description": "Auto-generated derived type for FooSpec via `CustomResource`", + "description": "Custom resource representing a Foo", "properties": { "spec": { "properties": { diff --git a/kube-runtime/Cargo.toml b/kube-runtime/Cargo.toml index 0beee861c..a4d8f149c 100644 --- a/kube-runtime/Cargo.toml +++ b/kube-runtime/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kube-runtime" -version = "0.87.1" +version = "0.88.0" description = "Kubernetes futures controller runtime" authors = [ "Natalie Klestrup Röijezon ", @@ -11,7 +11,7 @@ repository = "https://github.com/kube-rs/kube" readme = "../README.md" keywords = ["kubernetes", "runtime", "reflector", "watcher", "controller"] categories = ["web-programming::http-client", "caching", "network-programming"] -rust-version = "1.65.0" +rust-version = "1.70.0" edition = "2021" [features] @@ -28,7 +28,7 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] futures = "0.3.17" -kube-client = { path = "../kube-client", version = "=0.87.1", default-features = false, features = ["jsonpatch", "client"] } +kube-client = { path = "../kube-client", version = "=0.88.0", default-features = false, features = ["jsonpatch", "client"] } derivative = "2.1.1" serde = "1.0.130" smallvec = "1.7.0" @@ -46,7 +46,7 @@ async-trait = "0.1.64" hashbrown = "0.14.0" [dependencies.k8s-openapi] -version = "0.20.0" +version = "0.21.0" default-features = false [dev-dependencies] @@ -58,6 +58,6 @@ schemars = "0.8.6" tracing-subscriber = "0.3.17" [dev-dependencies.k8s-openapi] -version = "0.20.0" +version = "0.21.0" default-features = false features = ["latest"] diff --git a/kube-runtime/src/controller/mod.rs b/kube-runtime/src/controller/mod.rs index cd85f241f..ef4c662a7 100644 --- a/kube-runtime/src/controller/mod.rs +++ b/kube-runtime/src/controller/mod.rs @@ -434,7 +434,7 @@ impl Config { /// The debounce duration used to deduplicate reconciliation requests. /// /// When set to a non-zero duration, debouncing is enabled in the [`scheduler`](crate::scheduler()) - /// resulting in __trailing edge debouncing__ of reqonciler requests. + /// resulting in __trailing edge debouncing__ of reconciler requests. /// This option can help to reduce the amount of unnecessary reconciler calls /// when using multiple controller relations, or during rapid phase transitions. /// diff --git a/kube/Cargo.toml b/kube/Cargo.toml index 12f86124b..80efc79f6 100644 --- a/kube/Cargo.toml +++ b/kube/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kube" -version = "0.87.1" +version = "0.88.0" description = "Kubernetes client and async controller runtime" authors = [ "clux ", @@ -12,7 +12,7 @@ repository = "https://github.com/kube-rs/kube" readme = "../README.md" keywords = ["kubernetes", "client", "runtime", "cncf"] categories = ["network-programming", "caching", "api-bindings", "configuration", "encoding"] -rust-version = "1.65.0" +rust-version = "1.70.0" edition = "2021" [features] @@ -44,15 +44,15 @@ features = ["client", "rustls-tls", "openssl-tls", "derive", "ws", "oauth", "jso rustdoc-args = ["--cfg", "docsrs"] [dependencies] -kube-derive = { path = "../kube-derive", version = "=0.87.1", optional = true } -kube-core = { path = "../kube-core", version = "=0.87.1" } -kube-client = { path = "../kube-client", version = "=0.87.1", default-features = false, optional = true } -kube-runtime = { path = "../kube-runtime", version = "=0.87.1", optional = true} +kube-derive = { path = "../kube-derive", version = "=0.88.0", optional = true } +kube-core = { path = "../kube-core", version = "=0.88.0" } +kube-client = { path = "../kube-client", version = "=0.88.0", default-features = false, optional = true } +kube-runtime = { path = "../kube-runtime", version = "=0.88.0", optional = true} # Not used directly, but required by resolver 2.0 to ensure that the k8s-openapi dependency # is considered part of the "deps" graph rather than just the "dev-deps" graph [dependencies.k8s-openapi] -version = "0.20.0" +version = "0.21.0" default-features = false [dev-dependencies] @@ -67,6 +67,6 @@ tower-test = "0.4.0" anyhow = "1.0.71" [dev-dependencies.k8s-openapi] -version = "0.20.0" +version = "0.21.0" default-features = false features = ["latest"] diff --git a/kube/src/lib.rs b/kube/src/lib.rs index f82046664..9d4b13e27 100644 --- a/kube/src/lib.rs +++ b/kube/src/lib.rs @@ -7,12 +7,12 @@ //! //! The main modules are: //! -//! - [`client`](crate::client) with the Kubernetes [`Client`](crate::Client) and its layers -//! - [`config`](crate::config) for cluster [`Config`](crate::Config) -//! - [`api`](crate::api) with the generic Kubernetes [`Api`](crate::Api) -//! - [`derive`](kube_derive) with the [`CustomResource`](crate::CustomResource) derive for building controllers types -//! - [`runtime`](crate::runtime) with a [`Controller`](crate::runtime::Controller) / [`watcher`](crate::runtime::watcher()) / [`reflector`](crate::runtime::reflector::reflector) / [`Store`](crate::runtime::reflector::Store) -//! - [`core`](crate::core) with generics from `apimachinery` +//! - [`client`] with the Kubernetes [`Client`] and its layers +//! - [`config`] for cluster [`Config`] +//! - [`api`] with the generic Kubernetes [`Api`] +//! - [`derive`](kube_derive) with the [`CustomResource`] derive for building controllers types +//! - [`runtime`] with a [`Controller`](crate::runtime::Controller) / [`watcher`](crate::runtime::watcher()) / [`reflector`](crate::runtime::reflector::reflector) / [`Store`](crate::runtime::reflector::Store) +//! - [`core`] with generics from `apimachinery` //! //! You can use each of these as you need with the help of the [exported features](https://github.com/kube-rs/kube/blob/main/kube/Cargo.toml#L18). //! @@ -39,7 +39,7 @@ //! For details, see: //! //! - [`Client`](crate::client) for the extensible Kubernetes client -//! - [`Api`](crate::Api) for the generic api methods available on Kubernetes resources +//! - [`Api`] for the generic api methods available on Kubernetes resources //! - [k8s-openapi](https://docs.rs/k8s-openapi/*/k8s_openapi/) for documentation about the generated Kubernetes types //! //! # Using the Runtime with the Derive macro @@ -97,9 +97,9 @@ //! //! For details, see: //! -//! - [`CustomResource`](crate::CustomResource) for documentation how to configure custom resources +//! - [`CustomResource`] for documentation how to configure custom resources //! - [`runtime::watcher`](crate::runtime::watcher()) for how to long-running watches work and why you want to use this over [`Api::watch`](crate::Api::watch) -//! - [`runtime`](crate::runtime) for abstractions that help with more complicated Kubernetes application +//! - [`runtime`] for abstractions that help with more complicated Kubernetes application //! //! # Examples //! A large list of complete, runnable examples with explainations are available in the [examples folder](https://github.com/kube-rs/kube/tree/main/examples). diff --git a/kube/src/mock_tests.rs b/kube/src/mock_tests.rs index 8bfbb818a..c8d7c3b0e 100644 --- a/kube/src/mock_tests.rs +++ b/kube/src/mock_tests.rs @@ -96,6 +96,8 @@ impl ApiServerVerifier { assert!(!req_uri.contains("continue=")); // first list has no continue let respdata = json!({ + "kind": "HackList", + "apiVersion": "kube.rs/v1", "metadata": { "continue": "first", }, @@ -111,6 +113,8 @@ impl ApiServerVerifier { let req_uri = request.uri().to_string(); assert!(req_uri.contains("&continue=first")); let respdata = json!({ + "kind": "HackList", + "apiVersion": "kube.rs/v1", "metadata": { "continue": "", "resourceVersion": "2" diff --git a/release.toml b/release.toml index de2078a2c..93c09d830 100644 --- a/release.toml +++ b/release.toml @@ -1,24 +1,28 @@ # Release process :: cargo-release >= 0.18.3 # # Dependencies: https://kube.rs/tools +# Process: https://kube.rs/release-process/ # # 0. (optional) cargo release minor ; verify readme + changelog bumped; then git reset --hard # 1. PUBLISH_GRACE_SLEEP=20 cargo release minor --execute -# 1X. - on failure: follow plan manually, cd into next dirs and publish insequence with cargo publish --features=k8s-openapi/latest -# 2. check consolidated commit -# 2X. - on failure: git commit --amend and insert version -# 3. ./scripts/release-post.sh +# In the event of failures: +# - on partial cargo publish failures with unexpected build errors; yank partials and fix issues in a PR before retrying +# - on cargo-release issues waiting for crates.io: resume publish in given order manually, cd into next dirs and publish in sequence with cargo publish --features=k8s-openapi/latest +# - after publish; check consolidated commit, amend if needed, then create a manual signed tag without v prefix +# 2. After publishing the release run ./scripts/release-afterdoc.sh VERSION # Reference -# https://github.com/sunng87/cargo-release/blob/master/docs/reference.md +# https://github.com/crate-ci/cargo-release/blob/master/docs/reference.md consolidate-commits = true shared-version = true pre-release-hook = ["../scripts/release-pre.sh"] pre-release-commit-message = "release {{version}}" -# leave tagging + pushing to postrelease script (due to potential failures in 1 and 2) -push = false -tag = false +push = true +tag = true +tag-name = "{{version}}" +sign-tag = true +sign-commit = true # A Kubernetes version is normally supplied by the application consuming the library in the end. # Since we don't have that when verifying, supply one ourselves. enable-features = ["k8s-openapi/latest"] diff --git a/scripts/release-post.sh b/scripts/release-post.sh deleted file mode 100755 index c7804237f..000000000 --- a/scripts/release-post.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -set -euo pipefail - -main() { - cd "$(dirname "${BASH_SOURCE[0]}")" && cd .. # aka $WORKSPACE_ROOT - local -r CURRENT_VER="$(rg 'kube = \{ version = "(\S*)"' -or '$1' README.md | head -n1)" - git tag -a "${CURRENT_VER}" -m "${CURRENT_VER}" - git push - git push --tags -} - -# post release script run manually after cargo-release -# shellcheck disable=SC2068 -main $@