Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support multi-doc kubeconfigs - fixes #440 #441

Merged
merged 3 commits into from Feb 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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
===================
Expand Down
4 changes: 1 addition & 3 deletions Makefile
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion examples/Cargo.toml
Expand Up @@ -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"] }
Expand Down
2 changes: 1 addition & 1 deletion kube-derive/Cargo.toml
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion kube/Cargo.toml
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions kube/src/api/params.rs
Expand Up @@ -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\"]}");
}
}
Expand Down
2 changes: 0 additions & 2 deletions kube/src/api/resource.rs
Expand Up @@ -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::<corev1::Secret>("ns");
let req = r.create(&PostParams::default(), vec![]).unwrap();
println!("trait is: {:?}", corev1::Secret::GROUP);
assert_eq!(req.uri(), "/api/v1/namespaces/ns/secrets?");
}

Expand Down
169 changes: 137 additions & 32 deletions 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<String>,
#[serde(rename = "apiVersion")]
pub api_version: Option<String>,
/// General information to be use for cli interactions
pub preferences: Option<Preferences>,
/// Referencable names to cluster configs
pub clusters: Vec<NamedCluster>,
/// Referencable names to user configs
#[serde(rename = "users")]
pub auth_infos: Vec<NamedAuthInfo>,
/// Referencable names to context configs
pub contexts: Vec<NamedContext>,
/// The name of the context that you would like to use by default
#[serde(rename = "current-context")]
pub current_context: Option<String>,
/// Additional information for extenders so that reads and writes don't clobber unknown fields.
pub extensions: Option<Vec<NamedExtension>>,

// legacy fields TODO: remove
pub kind: Option<String>,
#[serde(rename = "apiVersion")]
pub api_version: Option<String>,
}

/// Preferences stores extensions for cli.
Expand All @@ -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<bool>,
/// The path to a cert file for the certificate authority.
#[serde(rename = "certificate-authority")]
pub certificate_authority: Option<String>,
/// PEM-encoded certificate authority certificates. Overrides `certificate_authority`
#[serde(rename = "certificate-authority-data")]
pub certificate_authority_data: Option<String>,
/// Additional information for extenders so that reads and writes don't clobber unknown fields
pub extensions: Option<Vec<NamedExtension>>,
}

Expand All @@ -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<String>,
/// The password for basic authentication to the kubernetes cluster.
pub password: Option<String>,

/// The bearer token for authentication to the kubernetes cluster.
pub token: Option<String>,
/// 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<String>,

/// Path to a client cert file for TLS.
#[serde(rename = "client-certificate")]
pub client_certificate: Option<String>,
/// PEM-encoded data from a client cert file for TLS. Overrides `client_certificate`
#[serde(rename = "client-certificate-data")]
pub client_certificate_data: Option<String>,

/// Path to a client key file for TLS.
#[serde(rename = "client-key")]
pub client_key: Option<String>,
/// PEM-encoded data from a client key file for TLS. Overrides `client_key`
#[serde(rename = "client-key-data")]
pub client_key_data: Option<String>,

/// The username to act-as.
#[serde(rename = "as")]
pub impersonate: Option<String>,
/// The groups to imperonate.
#[serde(rename = "as-groups")]
pub impersonate_groups: Option<Vec<String>>,

/// Specifies a custom authentication plugin for the kubernetes cluster.
#[serde(rename = "auth-provider")]
pub auth_provider: Option<AuthProviderConfig>,

/// Specifies a custom exec-based authentication plugin for the kubernetes cluster.
pub exec: Option<ExecConfig>,
}

Expand All @@ -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<String>,
pub args: Option<Vec<String>>,
/// Command to execute.
pub command: String,
/// Arguments to pass to the command when executing it.
pub args: Option<Vec<String>>,
/// 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<Vec<HashMap<String, String>>>,
}

Expand All @@ -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<String>,
/// Additional information for extenders so that reads and writes don't clobber unknown fields
pub extensions: Option<Vec<NamedExtension>>,
}

Expand All @@ -139,39 +175,54 @@ const KUBECONFIG: &str = "KUBECONFIG";
impl Kubeconfig {
/// Read a Config from an arbitrary location
pub fn read_from<P: AsRef<Path>>(path: P) -> Result<Kubeconfig> {
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<Kubeconfig> = 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)
}

Expand Down Expand Up @@ -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(())
}
}
3 changes: 3 additions & 0 deletions kube/src/error.rs
Expand Up @@ -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:?}")]
Expand Down