diff --git a/CHANGELOG.md b/CHANGELOG.md index e312efaaa..ba3b641d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,9 @@ UNRELEASED =================== * see https://github.com/clux/kube-rs/compare/0.50.1...master * `kube` `Config` now allows arbirary extension objects - #425 + * `kube` `Config` now allows multiple yaml documents per kubeconfig - #440 via #441 * `kube-derive` now more robust and is using `darling` - #435 + * docs improvements to patch + runtime 0.50.1 / 2021-02-17 =================== diff --git a/Makefile b/Makefile index 024a1066d..25401c7b5 100644 --- a/Makefile +++ b/Makefile @@ -12,9 +12,7 @@ fmt: cargo +nightly fmt doc: - # TODO: replace with RUSTDOCFLAGS="--cfg docsrs" once it works - cargo +nightly doc --lib --workspace --features=derive,ws,oauth,jsonpatch - xdg-open target/doc/kube/index.html + RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --lib --workspace --features=derive,ws,oauth,jsonpatch --open test: cargo test --all diff --git a/examples/Cargo.toml b/examples/Cargo.toml index f8590df62..332a4c5f7 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -28,7 +28,7 @@ k8s-openapi = { version = "0.11.0", features = ["v1_20"], default-features = fal log = "0.4.11" serde = { version = "1.0.118", features = ["derive"] } serde_json = "1.0.61" -serde_yaml = "0.8.14" +serde_yaml = "0.8.17" tokio = { version = "1.0.1", features = ["full"] } color-eyre = "0.5.10" snafu = { version = "0.6.10", features = ["futures"] } diff --git a/kube-derive/Cargo.toml b/kube-derive/Cargo.toml index 690751fb6..683dfd7f5 100644 --- a/kube-derive/Cargo.toml +++ b/kube-derive/Cargo.toml @@ -28,7 +28,7 @@ schema = [] [dev-dependencies] serde = { version = "1.0.118", features = ["derive"] } -serde_yaml = "0.8.14" +serde_yaml = "0.8.17" k8s-openapi = { version = "0.11.0", default-features = false, features = ["v1_20"] } schemars = { version = "0.8.0", features = ["chrono"] } chrono = "0.4.19" diff --git a/kube/Cargo.toml b/kube/Cargo.toml index 063a53b3b..7647b06b7 100644 --- a/kube/Cargo.toml +++ b/kube/Cargo.toml @@ -36,7 +36,7 @@ chrono = "0.4.19" dirs = { package = "dirs-next", version = "2.0.0" } serde = { version = "1.0.118", features = ["derive"] } serde_json = "1.0.61" -serde_yaml = "0.8.14" +serde_yaml = "0.8.17" http = "0.2.2" url = "2.2.0" log = "0.4.11" diff --git a/kube/src/api/params.rs b/kube/src/api/params.rs index fab9a87dc..7b8d834a8 100644 --- a/kube/src/api/params.rs +++ b/kube/src/api/params.rs @@ -352,12 +352,12 @@ mod test { fn delete_param_serialize() { let mut dp = DeleteParams::default(); let emptyser = serde_json::to_string(&dp).unwrap(); - println!("emptyser is: {}", emptyser); + //println!("emptyser is: {}", emptyser); assert_eq!(emptyser, "{}"); dp.dry_run = true; let ser = serde_json::to_string(&dp).unwrap(); - println!("ser is: {}", ser); + //println!("ser is: {}", ser); assert_eq!(ser, "{\"dryRun\":[\"All\"]}"); } } diff --git a/kube/src/api/resource.rs b/kube/src/api/resource.rs index bc35a5bc4..eac22de28 100644 --- a/kube/src/api/resource.rs +++ b/kube/src/api/resource.rs @@ -362,10 +362,8 @@ mod test { // these are sanity tests for macros that create the Resource::v1Ctors #[test] fn api_url_secret() { - use k8s_openapi::Resource as ResourceTrait; let r = Resource::namespaced::("ns"); let req = r.create(&PostParams::default(), vec![]).unwrap(); - println!("trait is: {:?}", corev1::Secret::GROUP); assert_eq!(req.uri(), "/api/v1/namespaces/ns/secrets?"); } diff --git a/kube/src/config/file_config.rs b/kube/src/config/file_config.rs index f485c94ed..8d7d9f552 100644 --- a/kube/src/config/file_config.rs +++ b/kube/src/config/file_config.rs @@ -1,30 +1,38 @@ #![allow(missing_docs)] - -use std::{collections::HashMap, fs::File, path::Path}; - use crate::{config::utils, error::ConfigError, Result}; - use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, fs, path::Path}; /// [`Kubeconfig`] represents information on how to connect to a remote Kubernetes cluster -/// that is normally stored in `~/.kube/config` /// -/// This type (and its children) are exposed for convenience only. -/// Please load a [`Config`][crate::Config] object for use with a [`Client`][crate::Client] -/// which will read and parse the kubeconfig file +/// Stored in `~/.kube/config` by default, but can be distributed across multiple paths in passed through `KUBECONFIG`. +/// An analogue of the [config type from client-go](https://github.com/kubernetes/kubernetes/blob/cea1d4e20b4a7886d8ff65f34c6d4f95efcb4742/staging/src/k8s.io/client-go/tools/clientcmd/api/types.go#L28-L55). +/// +/// This type (and its children) are exposed primarily for convenience. +/// +/// [`Config`][crate::Config] is the __intended__ developer interface to help create a [`Client`][crate::Client], +/// and this will handle the difference between in-cluster deployment and local development. #[derive(Clone, Debug, Serialize, Deserialize, Default)] pub struct Kubeconfig { - pub kind: Option, - #[serde(rename = "apiVersion")] - pub api_version: Option, + /// General information to be use for cli interactions pub preferences: Option, + /// Referencable names to cluster configs pub clusters: Vec, + /// Referencable names to user configs #[serde(rename = "users")] pub auth_infos: Vec, + /// Referencable names to context configs pub contexts: Vec, + /// The name of the context that you would like to use by default #[serde(rename = "current-context")] pub current_context: Option, + /// Additional information for extenders so that reads and writes don't clobber unknown fields. pub extensions: Option>, + + // legacy fields TODO: remove + pub kind: Option, + #[serde(rename = "apiVersion")] + pub api_version: Option, } /// Preferences stores extensions for cli. @@ -51,13 +59,17 @@ pub struct NamedCluster { /// Cluster stores information to connect Kubernetes cluster. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Cluster { + /// The address of the kubernetes cluster (https://hostname:port). pub server: String, #[serde(rename = "insecure-skip-tls-verify")] pub insecure_skip_tls_verify: Option, + /// The path to a cert file for the certificate authority. #[serde(rename = "certificate-authority")] pub certificate_authority: Option, + /// PEM-encoded certificate authority certificates. Overrides `certificate_authority` #[serde(rename = "certificate-authority-data")] pub certificate_authority_data: Option, + /// Additional information for extenders so that reads and writes don't clobber unknown fields pub extensions: Option>, } @@ -72,31 +84,43 @@ pub struct NamedAuthInfo { /// AuthInfo stores information to tell cluster who you are. #[derive(Clone, Debug, Serialize, Deserialize, Default)] pub struct AuthInfo { + /// The username for basic authentication to the kubernetes cluster. pub username: Option, + /// The password for basic authentication to the kubernetes cluster. pub password: Option, + /// The bearer token for authentication to the kubernetes cluster. pub token: Option, + /// Pointer to a file that contains a bearer token (as described above). If both `token` and token_file` are present, `token` takes precedence. #[serde(rename = "tokenFile")] pub token_file: Option, + /// Path to a client cert file for TLS. #[serde(rename = "client-certificate")] pub client_certificate: Option, + /// PEM-encoded data from a client cert file for TLS. Overrides `client_certificate` #[serde(rename = "client-certificate-data")] pub client_certificate_data: Option, + /// Path to a client key file for TLS. #[serde(rename = "client-key")] pub client_key: Option, + /// PEM-encoded data from a client key file for TLS. Overrides `client_key` #[serde(rename = "client-key-data")] pub client_key_data: Option, + /// The username to act-as. #[serde(rename = "as")] pub impersonate: Option, + /// The groups to imperonate. #[serde(rename = "as-groups")] pub impersonate_groups: Option>, + /// Specifies a custom authentication plugin for the kubernetes cluster. #[serde(rename = "auth-provider")] pub auth_provider: Option, + /// Specifies a custom exec-based authentication plugin for the kubernetes cluster. pub exec: Option, } @@ -110,10 +134,18 @@ pub struct AuthProviderConfig { /// ExecConfig stores credential-plugin configuration. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ExecConfig { + /// Preferred input version of the ExecInfo. + /// + /// The returned ExecCredentials MUST use the same encoding version as the input. #[serde(rename = "apiVersion")] pub api_version: Option, - pub args: Option>, + /// Command to execute. pub command: String, + /// Arguments to pass to the command when executing it. + pub args: Option>, + /// Env defines additional environment variables to expose to the process. + /// + /// TODO: These are unioned with the host's environment, as well as variables client-go uses to pass argument to the plugin. pub env: Option>>, } @@ -127,9 +159,13 @@ pub struct NamedContext { /// Context stores tuple of cluster and user information. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Context { + /// Name of the cluster for this context pub cluster: String, + /// Name of the `AuthInfo` for this context pub user: String, + /// The default namespace to use on unspecified requests pub namespace: Option, + /// Additional information for extenders so that reads and writes don't clobber unknown fields pub extensions: Option>, } @@ -139,39 +175,54 @@ const KUBECONFIG: &str = "KUBECONFIG"; impl Kubeconfig { /// Read a Config from an arbitrary location pub fn read_from>(path: P) -> Result { - let f = File::open(&path).map_err(|source| ConfigError::ReadFile { + let data = fs::read_to_string(&path).map_err(|source| ConfigError::ReadFile { path: path.as_ref().into(), source, })?; - let mut config: Kubeconfig = serde_yaml::from_reader(f).map_err(ConfigError::ParseYaml)?; + // support multiple documents + let mut documents: Vec = vec![]; + for doc in serde_yaml::Deserializer::from_str(&data) { + let value = serde_yaml::Value::deserialize(doc).map_err(ConfigError::ParseYaml)?; + let kconf = serde_yaml::from_value(value).map_err(ConfigError::ParseYaml)?; + documents.push(kconf) + } // Remap all files we read to absolute paths. - if let Some(dir) = path.as_ref().parent() { - for named in config.clusters.iter_mut() { - if let Some(path) = &named.cluster.certificate_authority { - if let Some(abs_path) = to_absolute(dir, path) { - named.cluster.certificate_authority = Some(abs_path); + let mut merged_docs = None; + for mut config in documents { + if let Some(dir) = path.as_ref().parent() { + for named in config.clusters.iter_mut() { + if let Some(path) = &named.cluster.certificate_authority { + if let Some(abs_path) = to_absolute(dir, path) { + named.cluster.certificate_authority = Some(abs_path); + } } } - } - for named in config.auth_infos.iter_mut() { - if let Some(path) = &named.auth_info.client_certificate { - if let Some(abs_path) = to_absolute(dir, path) { - named.auth_info.client_certificate = Some(abs_path); + for named in config.auth_infos.iter_mut() { + if let Some(path) = &named.auth_info.client_certificate { + if let Some(abs_path) = to_absolute(dir, path) { + named.auth_info.client_certificate = Some(abs_path); + } } - } - if let Some(path) = &named.auth_info.client_key { - if let Some(abs_path) = to_absolute(dir, path) { - named.auth_info.client_key = Some(abs_path); + if let Some(path) = &named.auth_info.client_key { + if let Some(abs_path) = to_absolute(dir, path) { + named.auth_info.client_key = Some(abs_path); + } } - } - if let Some(path) = &named.auth_info.token_file { - if let Some(abs_path) = to_absolute(dir, path) { - named.auth_info.token_file = Some(abs_path); + if let Some(path) = &named.auth_info.token_file { + if let Some(abs_path) = to_absolute(dir, path) { + named.auth_info.token_file = Some(abs_path); + } } } } + if let Some(c) = merged_docs { + merged_docs = Some(Kubeconfig::merge(c, config)?); + } else { + merged_docs = Some(config); + } } + let config = merged_docs.ok_or_else(|| ConfigError::EmptyKubeconfig(path.as_ref().to_path_buf()))?; Ok(config) } @@ -409,4 +460,58 @@ users: Some(&Value::String("minikube.sigs.k8s.io".to_owned())) ); } + + #[test] + fn kubeconfig_multi_document_merge() -> Result<()> { + let config_yaml = r#"--- +apiVersion: v1 +clusters: +- cluster: + certificate-authority-data: aGVsbG8K + server: https://0.0.0.0:6443 + name: k3d-promstack +contexts: +- context: + cluster: k3d-promstack + user: admin@k3d-promstack + name: k3d-promstack +current-context: k3d-promstack +kind: Config +preferences: {} +users: +- name: admin@k3d-promstack + user: + client-certificate-data: aGVsbG8K + client-key-data: aGVsbG8K +--- +apiVersion: v1 +clusters: +- cluster: + certificate-authority-data: aGVsbG8K + server: https://0.0.0.0:6443 + name: k3d-k3s-default +contexts: +- context: + cluster: k3d-k3s-default + user: admin@k3d-k3s-default + name: k3d-k3s-default +current-context: k3d-k3s-default +kind: Config +preferences: {} +users: +- name: admin@k3d-k3s-default + user: + client-certificate-data: aGVsbG8K + client-key-data: aGVsbG8K +"#; + let file = tempfile::NamedTempFile::new().expect("create config tempfile"); + fs::write(file.path(), config_yaml).unwrap(); + let cfg = Kubeconfig::read_from(file.path())?; + + // Ensure we have data from both documents: + assert_eq!(cfg.clusters[0].name, "k3d-promstack"); + assert_eq!(cfg.clusters[1].name, "k3d-k3s-default"); + + Ok(()) + } } diff --git a/kube/src/error.rs b/kube/src/error.rs index 1a4d83632..d14905d6a 100644 --- a/kube/src/error.rs +++ b/kube/src/error.rs @@ -185,6 +185,9 @@ pub enum ConfigError { #[error("Failed to parse Kubeconfig YAML: {0}")] ParseYaml(#[source] serde_yaml::Error), + #[error("Failed to find a single YAML document in Kubeconfig: {0}")] + EmptyKubeconfig(PathBuf), + #[error("Unable to run auth exec: {0}")] AuthExecStart(#[source] std::io::Error), #[error("Auth exec command '{cmd}' failed with status {status}: {out:?}")]