├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── get-secret.rs ├── list-nodes.rs └── list-pods.rs └── src ├── clients ├── low_level.rs ├── mod.rs └── resource_clients.rs ├── config.rs ├── errors.rs ├── lib.rs └── resources ├── config_map.rs ├── config_map.rs:3:5 ├── daemon_set.rs ├── deployment.rs ├── mod.rs ├── network_policy.rs ├── node.rs ├── node.rs:2:30 ├── pod.rs ├── secret.rs ├── secret.rs:4:5 └── service.rs /.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | target/ 3 | .vscode/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | sudo: false 3 | cache: cargo 4 | 5 | rust: 6 | - 1.26.2 7 | - stable 8 | - beta 9 | - nightly 10 | 11 | os: 12 | - linux 13 | - osx 14 | 15 | matrix: 16 | allow_failures: 17 | - rust: nightly 18 | 19 | script: 20 | - cargo build -v 21 | # Tests disabled because feature(ques_in_main) isn't ready 22 | # - cargo test -v 23 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at anowell@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kubeclient" 3 | version = "0.1.0" 4 | license = "MIT" 5 | authors = ["Anthony Nowell "] 6 | 7 | description = "An ergonomic Kubernetes API client to manage Kubernetes resources" 8 | documentation = "http://docs.rs/kubeclient" 9 | repository = "https://github.com/anowell/kubeclient-rs" 10 | readme = "README.md" 11 | keywords = ["kubernetes", "kubectl", "k8s"] 12 | categories =["web-programming::http-client"] 13 | 14 | [dependencies] 15 | base64 = "0.6.0" 16 | chrono = { version = "0.4", features = ["serde"] } 17 | error-chain = "0.11.0" 18 | serde = "1.0.11" 19 | serde_derive = "1.0.11" 20 | serde_json = "1.0.2" 21 | serde_yaml = "0.7.1" 22 | url = "1.5.1" 23 | url_serde = "0.2.0" 24 | openssl = "0.10.15" 25 | walkdir = "1.0.7" 26 | reqwest = "0.9.4" 27 | headers-ext = "0.0.3" 28 | k8s-openapi = { git = "https://github.com/Arnavion/k8s-openapi-codegen", branch = "master", features = ["v1_9"] } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Anthony Nowell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | An ergonomic Kubernetes API client to manage Kubernetes resources 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/kubeclient.svg?maxAge=2592000)](https://crates.io/crates/kubeclient) 4 | 5 | ## Documentation 6 | 7 | [docs.rs/kubeclient](http://docs.rs/kubeclient) 8 | 9 | ## Usage 10 | 11 | You can find out about the basic usage in [examples](/examples). 12 | 13 | ``` 14 | # Ensure you have a valid kubeconfig in admin.conf 15 | 16 | ## Get secret 17 | cargo run --example get-secret secret123 18 | [...] 19 | 20 | ## List nodes 21 | cargo run --example list-nodes 22 | [...] 23 | 24 | ``` 25 | 26 | ## Status 27 | 28 | This client is still very incomplete, so expect to file issues and PRs to 29 | unblock yourself if you actually take this crate as a dependency. 30 | 31 | It has basic support for many common operations, namely the ones I've personally needed, 32 | but I'm not yet using this library in production, so it's not very high priority for me. 33 | That said, I will commit to discussing issues and reviewing PRs in a timely manner. -------------------------------------------------------------------------------- /examples/get-secret.rs: -------------------------------------------------------------------------------- 1 | extern crate kubeclient; 2 | use std::env; 3 | use kubeclient::prelude::*; 4 | use kubeclient::errors::*; 5 | 6 | fn get_secret(name: &str) -> Result { 7 | // filename is set to $KUBECONFIG if the env var is available. 8 | // Otherwise it falls back to "admin.conf". 9 | let filename = env::var("KUBECONFIG").ok(); 10 | let filename = filename 11 | .as_ref() 12 | .map(String::as_str) 13 | .and_then(|s| if s.is_empty() { None } else { Some(s) }) 14 | .unwrap_or("admin.conf"); 15 | let kube = Kubernetes::load_conf(filename)?; 16 | 17 | if kube.healthy()? { 18 | let __secret = kube.secrets().get(name)?; 19 | return Ok(true); 20 | } 21 | 22 | Ok(false) 23 | } 24 | 25 | fn main() { 26 | let args: Vec<_> = env::args().collect(); 27 | let secret_name = match args.len() > 1 { 28 | true => &args[1], 29 | false => "secret1", 30 | }; 31 | 32 | match get_secret(secret_name) { 33 | Ok(s) => println!("The secret {} exists {}", secret_name, s), 34 | Err(e) => println!("Error: {}", e), 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/list-nodes.rs: -------------------------------------------------------------------------------- 1 | extern crate kubeclient; 2 | use kubeclient::prelude::*; 3 | use kubeclient::errors::*; 4 | use std::env; 5 | 6 | fn run_main() -> Result { 7 | // filename is set to $KUBECONFIG if the env var is available. 8 | // Otherwise it falls back to "admin.conf". 9 | let filename = env::var("KUBECONFIG").ok(); 10 | let filename = filename 11 | .as_ref() 12 | .map(String::as_str) 13 | .and_then(|s| if s.is_empty() { None } else { Some(s) }) 14 | .unwrap_or("admin.conf"); 15 | let kube = Kubernetes::load_conf(filename)?; 16 | 17 | if kube.healthy()? { 18 | for node in kube.nodes().list(None)? { 19 | println!("found node: {:?}", node); 20 | } 21 | } 22 | 23 | Ok(0) 24 | } 25 | 26 | fn main() { 27 | match run_main() { 28 | Ok(n) => println!("Success error code is {}", n), 29 | Err(e) => println!("Error: {}", e), 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/list-pods.rs: -------------------------------------------------------------------------------- 1 | extern crate kubeclient; 2 | use kubeclient::prelude::*; 3 | use kubeclient::errors::*; 4 | use std::env; 5 | 6 | fn run_list_pods() -> Result { 7 | // filename is set to $KUBECONFIG if the env var is available. 8 | // Otherwise it falls back to "admin.conf". 9 | let filename = env::var("KUBECONFIG").ok(); 10 | let filename = filename 11 | .as_ref() 12 | .map(String::as_str) 13 | .and_then(|s| if s.is_empty() { None } else { Some(s) }) 14 | .unwrap_or("admin.conf"); 15 | let kube = Kubernetes::load_conf(filename)?; 16 | 17 | if kube.healthy()? { 18 | for pod in kube.pods().namespace("default").list(None)? { 19 | println!("found pod: {:?}", pod); 20 | } 21 | } 22 | 23 | Ok(0) 24 | } 25 | 26 | fn main() { 27 | match run_list_pods() { 28 | Ok(n) => println!("Success error code is {}", n), 29 | Err(e) => println!("Error: {}", e), 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/clients/low_level.rs: -------------------------------------------------------------------------------- 1 | use reqwest::{self, header, StatusCode}; 2 | use headers_ext::{self, HeaderMapExt}; 3 | use std::path::Path; 4 | use config::KubeConfig; 5 | use resources::*; 6 | use std::fs::File; 7 | use std::io::Read; 8 | use openssl::pkcs12::Pkcs12; 9 | use serde::Serialize; 10 | use serde::de::DeserializeOwned; 11 | use serde_json::{self, Value}; 12 | use serde_yaml; 13 | use url::Url; 14 | use std::borrow::Borrow; 15 | use walkdir::WalkDir; 16 | use errors::*; 17 | use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; 18 | 19 | #[derive(Clone)] 20 | pub struct KubeLowLevel { 21 | pub(crate) client: reqwest::Client, 22 | pub(crate) base_url: Url, 23 | } 24 | 25 | // This is only used for figuring out the API endpoint to use 26 | #[derive(Deserialize, Debug)] 27 | #[serde(rename_all = "camelCase")] 28 | struct MinimalResource { 29 | api_version: String, 30 | kind: Kind, 31 | metadata: ObjectMeta, 32 | } 33 | 34 | impl KubeLowLevel { 35 | pub fn load_conf>(path: P) -> Result { 36 | let kubeconfig = KubeConfig::load(path)?; 37 | let context = kubeconfig.default_context()?; 38 | let auth_info = context.user; 39 | 40 | let cluster = context.cluster; 41 | 42 | let mut headers = header::HeaderMap::new(); 43 | let client = reqwest::Client::builder(); 44 | 45 | let client = if let Some(ca_cert) = cluster.ca_cert() { 46 | let req_ca_cert = reqwest::Certificate::from_der(&ca_cert.to_der().unwrap()).unwrap(); 47 | client.add_root_certificate(req_ca_cert) 48 | } else { client }; 49 | 50 | let client = if auth_info.client_certificate().is_some() && auth_info.client_key().is_some() { 51 | let crt = auth_info.client_certificate().unwrap(); 52 | let key = auth_info.client_key().unwrap(); 53 | let pkcs_cert = Pkcs12::builder().build("", "admin", &key, &crt).chain_err(|| "Failed to build Pkcs12")?; 54 | let req_pkcs_cert = reqwest::Identity::from_pkcs12_der(&pkcs_cert.to_der().unwrap(), "").unwrap(); 55 | client.identity(req_pkcs_cert) 56 | } else { client }; 57 | 58 | if let (Some(username), Some(password)) = (auth_info.username, auth_info.password) { 59 | headers.typed_insert(headers_ext::Authorization::basic( 60 | &username, &password 61 | )); 62 | } else if let Some(token) = auth_info.token { 63 | headers.typed_insert(headers_ext::Authorization::bearer(&token) 64 | .map_err(|_| Error::from("Invalid bearer token"))?); 65 | } 66 | 67 | let client = client.default_headers(headers) 68 | .build() 69 | .chain_err(|| "Failed to build reqwest client")?; 70 | 71 | Ok(KubeLowLevel { client, base_url: cluster.server }) 72 | } 73 | 74 | pub fn health(&self) -> Result { 75 | let mut response = self.http_get(self.base_url.join("healthz")?)?; 76 | let mut output = String::new(); 77 | let _ = response.read_to_string(&mut output)?; 78 | Ok(output) 79 | } 80 | 81 | 82 | pub fn exists(&self, route: &ResourceRoute) -> Result { 83 | let url = route.build(&self.base_url)?; 84 | let mut response = self.client.get(url) 85 | .send() 86 | .chain_err(|| "Failed to GET URL")?; 87 | 88 | match response.status() { 89 | StatusCode::NOT_FOUND => Ok(false), 90 | s if s.is_success() => Ok(true), 91 | _ => { 92 | let status: Status = response.json() 93 | .chain_err(|| "Failed to decode error response as 'Status'")?; 94 | bail!(status.message); 95 | } 96 | } 97 | } 98 | 99 | pub fn list(&self, route: &KindRoute) -> Result 100 | where D: DeserializeOwned { 101 | let url = route.build(&self.base_url)?; 102 | self.http_get_json(url) 103 | } 104 | 105 | pub fn get(&self, route: &ResourceRoute) -> Result 106 | where D: DeserializeOwned 107 | { 108 | let url = route.build(&self.base_url)?; 109 | self.http_get_json(url) 110 | } 111 | 112 | // pub fn create(&self, route: &KindRoute, resource: &str, data: &S) -> Result 113 | // where S: Serialize, 114 | // D: DeserializeOwned 115 | // { 116 | // let body = json!({ 117 | // "data": data, 118 | // "metadata": { "name": resource } 119 | // }); 120 | // self.apply(route, &body) 121 | // } 122 | 123 | pub fn apply(&self, route: &KindRoute, body: &S) -> Result 124 | where S: Serialize, 125 | D: DeserializeOwned 126 | { 127 | let url = route.build(&self.base_url)?; 128 | self.http_post_json(url, &body) 129 | } 130 | 131 | pub(crate) fn each_resource_path>(&self, path: P, handler: F) -> Result> 132 | where 133 | D: DeserializeOwned + ::std::fmt::Debug, 134 | F: Fn(&Path) -> Result, 135 | { 136 | WalkDir::new(path).max_depth(1).into_iter() 137 | .filter_map(|e| e.ok()) 138 | .filter(|entry| entry.file_type().is_file()) 139 | .filter(|entry| { 140 | if let Some(ext) = entry.path().extension() { 141 | let ext = ext.to_string_lossy().to_lowercase(); 142 | return ext == "json" || ext == "yaml"; 143 | } 144 | false 145 | }) 146 | .map(|entry| handler(entry.path())) 147 | .collect() 148 | } 149 | 150 | // TODO: This function could use a serious refactoring 151 | pub(crate) fn apply_file(&self, path: &Path) -> Result 152 | where D: DeserializeOwned + ::std::fmt::Debug 153 | { 154 | let mut bytes = Vec::new(); 155 | let ext = path.extension().unwrap().to_string_lossy().to_lowercase(); 156 | let mut file = File::open(path)?; 157 | file.read_to_end(&mut bytes)?; 158 | let body: Value = match &*ext { 159 | "json" => serde_json::from_slice(&bytes)?, 160 | "yaml" => serde_yaml::from_slice(&bytes)?, 161 | _ => unreachable!("kubeclient bug: unexpected and unfiltered file extension"), 162 | }; 163 | let mini: MinimalResource = serde_json::from_value(body.clone())?; 164 | 165 | let root = if mini.api_version.starts_with("v") { 166 | "/api" 167 | } else { 168 | "/apis" 169 | }; 170 | let name = mini.metadata.name.expect("must set metadata.name to apply kubernetes resource"); 171 | let kind_path = match mini.metadata.namespace.as_ref().map(|x| &**x).borrow().or(mini.kind.default_namespace) { 172 | Some(ns) => format!("{}/{}/namespaces/{}/{}", root, mini.api_version, ns, mini.kind.plural), 173 | None =>format!("{}/{}/{}", root, mini.api_version, mini.kind.plural), 174 | }; 175 | let kind_url = self.base_url.join(&kind_path)?; 176 | let resource_url = self.base_url.join(&format!("{}/{}", kind_path, name))?; 177 | 178 | // First check if resource already exists 179 | let mut response = self.client.get(resource_url).send() 180 | .chain_err(|| "Failed to GET URL")?; 181 | match response.status() { 182 | // Apply if resource doesn't exist 183 | StatusCode::NOT_FOUND => { 184 | let resp = self.http_post_json(kind_url, &body)?; 185 | Ok(resp) 186 | } 187 | // Return it if it already exists 188 | s if s.is_success() => { 189 | let resp = response.json().chain_err(|| "Failed to decode JSON response")?; 190 | Ok(resp) 191 | } 192 | // Propogate any other error 193 | _ => { 194 | let status: Status = response.json() 195 | .chain_err(|| "Failed to decode error response as 'Status'")?; 196 | bail!(status.message); 197 | } 198 | } 199 | } 200 | 201 | pub(crate) fn replace_file(&self, path: &Path) -> Result 202 | where D: DeserializeOwned + ::std::fmt::Debug 203 | { 204 | let mut bytes = Vec::new(); 205 | let ext = path.extension().unwrap().to_string_lossy().to_lowercase(); 206 | let mut file = File::open(path)?; 207 | file.read_to_end(&mut bytes)?; 208 | let body: Value = match &*ext { 209 | "json" => serde_json::from_slice(&bytes)?, 210 | "yaml" => serde_yaml::from_slice(&bytes)?, 211 | _ => unreachable!("kubeclient bug: unexpected and unfiltered file extension"), 212 | }; 213 | let mini: MinimalResource = serde_json::from_value(body.clone())?; 214 | 215 | let root = if mini.api_version.starts_with("v") { 216 | "/api" 217 | } else { 218 | "/apis" 219 | }; 220 | let name = mini.metadata.name.expect("must set metadata.name to apply kubernetes resource"); 221 | let url = match mini.metadata.namespace { 222 | Some(ns) => self.base_url.join( 223 | &format!("{}/{}/namespaces/{}/{}/{}", root, mini.api_version, ns, mini.kind.plural, name) 224 | )?, 225 | None => self.base_url.join( 226 | &format!("{}/{}/{}/{}", root, mini.api_version, mini.kind.plural, name) 227 | )?, 228 | }; 229 | let resp = self.http_put_json(url, &body)?; 230 | Ok(resp) 231 | } 232 | 233 | pub fn delete(&self, route: &ResourceRoute) -> Result<()> { 234 | let url = route.build(&self.base_url)?; 235 | self.http_delete(url).map(|_| ()) 236 | } 237 | 238 | // 239 | // Low-level 240 | // 241 | 242 | pub(crate) fn http_get(&self, url: Url) -> Result { 243 | let req = self.client.get(url); 244 | 245 | let mut response = req.send().chain_err(|| "Failed to GET URL")?; 246 | 247 | if !response.status().is_success() { 248 | let status: Status = response.json() 249 | .chain_err(|| "Failed to decode kubernetes error response as 'Status'")?; 250 | bail!(format!("Kubernetes API error: {}", status.message)); 251 | } 252 | Ok(response) 253 | } 254 | 255 | pub(crate) fn http_get_json(&self, url: Url) -> Result { 256 | let mut response = self.http_get(url)?; 257 | Ok(response.json().chain_err(|| "Failed to decode JSON response")?) 258 | } 259 | 260 | pub(crate) fn http_post_json(&self, url: Url, body: &S) -> Result 261 | where S: Serialize, 262 | D: DeserializeOwned, 263 | { 264 | let mut response = self.client.post(url) 265 | .json(&body) 266 | .send() 267 | .chain_err(|| "Failed to POST URL")?; 268 | 269 | if !response.status().is_success() { 270 | let status: Status = response.json() 271 | .chain_err(|| "Failed to decode kubernetes error response as 'Status'")?; 272 | bail!(format!("Kubernetes API error: {}", status.message)); 273 | } 274 | 275 | Ok(response.json().chain_err(|| "Failed to decode JSON response")?) 276 | } 277 | 278 | pub(crate) fn http_put_json(&self, url: Url, body: &S) -> Result 279 | where S: Serialize, 280 | D: DeserializeOwned, 281 | { 282 | let mut response = self.client.put(url) 283 | .json(&body) 284 | .send() 285 | .chain_err(|| "Failed to PUT URL")?; 286 | 287 | if !response.status().is_success() { 288 | let status: Status = response.json() 289 | .chain_err(|| "Failed to decode kubernetes error response as 'Status'")?; 290 | bail!(format!("Kubernetes API error: {}", status.message)); 291 | } 292 | 293 | Ok(response.json().chain_err(|| "Failed to decode JSON response")?) 294 | } 295 | 296 | pub(crate) fn http_delete(&self, url: Url) -> Result { 297 | let mut response = self.client.delete(url) 298 | .send() 299 | .chain_err(|| "Failed to DELETE URL")?; 300 | 301 | if !response.status().is_success() { 302 | let status: Status = response.json() 303 | .chain_err(|| "Failed to decode kubernetes error response as 'Status'")?; 304 | bail!(format!("Kubernetes API error: {}", status.message)); 305 | } 306 | 307 | Ok(response) 308 | } 309 | 310 | } 311 | 312 | 313 | pub struct KindRoute<'a> { 314 | api: &'a str, 315 | namespace: Option<&'a str>, 316 | kind: &'a str, 317 | query: Option>, 318 | } 319 | 320 | pub struct ResourceRoute<'a> { 321 | api: &'a str, 322 | namespace: Option<&'a str>, 323 | kind: &'a str, 324 | resource: &'a str, 325 | query: Option>, 326 | } 327 | 328 | 329 | impl<'a> KindRoute<'a> { 330 | pub fn new(api: &'a str, kind: &'a str) -> KindRoute<'a> { 331 | KindRoute { 332 | api, kind, 333 | namespace: None, 334 | query: None, 335 | } 336 | } 337 | 338 | pub fn namespace(&mut self, namespace: &'a str) -> &mut KindRoute<'a> { 339 | self.namespace = Some(namespace); 340 | self 341 | } 342 | 343 | pub fn query(&mut self, query: I) -> &mut KindRoute<'a> 344 | where 345 | I: IntoIterator, 346 | I::Item: Borrow<(K, V)>, 347 | K: AsRef, 348 | V: AsRef, 349 | { 350 | // This is ugly, but today the borrow checker beat me 351 | let pairs = query.into_iter() 352 | .map(|i| { 353 | let (ref k, ref v) = *i.borrow(); 354 | (k.as_ref().to_owned(), v.as_ref().to_owned()) 355 | }) 356 | .collect(); 357 | self.query = Some(pairs); 358 | self 359 | } 360 | 361 | pub fn build(&self, base_url: &Url) -> Result { 362 | let path = match self.namespace { 363 | Some(ns) => format!("{}/namespaces/{}/{}", self.api, ns, self.kind), 364 | None => format!("{}/{}", self.api, self.kind), 365 | }; 366 | let mut url = base_url.join(&path)?; 367 | if let Some(ref query) = self.query { 368 | url.query_pairs_mut().extend_pairs(query); 369 | } 370 | Ok(url) 371 | } 372 | } 373 | 374 | impl<'a> ResourceRoute<'a> { 375 | pub fn new(api: &'a str, kind: &'a str, resource: &'a str) -> ResourceRoute<'a> { 376 | ResourceRoute { 377 | api, kind, resource, 378 | namespace: None, 379 | query: None, 380 | } 381 | } 382 | 383 | pub fn namespace(&mut self, namespace: &'a str) -> &mut ResourceRoute<'a> { 384 | self.namespace = Some(namespace); 385 | self 386 | } 387 | 388 | 389 | // pub fn query(&mut self, query: I) -> &mut ResourceRoute<'a> 390 | // where 391 | // I: IntoIterator, 392 | // I::Item: Borrow<(K, V)>, 393 | // K: AsRef, 394 | // V: AsRef, 395 | // { 396 | // // This is ugly, but today the borrow checker beat me 397 | // let pairs = query.into_iter() 398 | // .map(|i| { 399 | // let (ref k, ref v) = *i.borrow(); 400 | // (k.as_ref().to_owned(), v.as_ref().to_owned()) 401 | // }) 402 | // .collect(); 403 | // self.query = Some(pairs); 404 | // self 405 | // } 406 | 407 | pub(crate) fn build(&self, base_url: &Url) -> Result { 408 | let path = match self.namespace { 409 | Some(ns) => format!("{}/namespaces/{}/{}/{}", self.api, ns, self.kind, self.resource), 410 | None => format!("{}/{}/{}", self.api, self.kind, self.resource), 411 | }; 412 | let mut url = base_url.join(&path)?; 413 | if let Some(ref query) = self.query { 414 | url.query_pairs_mut().extend_pairs(query); 415 | } 416 | Ok(url) 417 | } 418 | } 419 | -------------------------------------------------------------------------------- /src/clients/mod.rs: -------------------------------------------------------------------------------- 1 | mod low_level; 2 | mod resource_clients; 3 | 4 | pub use self::resource_clients::*; 5 | use self::low_level::*; 6 | 7 | use std::path::Path; 8 | use resources::*; 9 | use serde_json::Value; 10 | use errors::*; 11 | use std::marker::PhantomData; 12 | 13 | 14 | /// The main type for instantiating clients for managing kubernetes resources 15 | #[derive(Clone)] 16 | pub struct Kubernetes { 17 | pub(crate) low_level: KubeLowLevel, 18 | namespace: Option, 19 | } 20 | 21 | impl Kubernetes { 22 | /// Initialize a Kubernetes client from a Kubernets config file 23 | /// 24 | /// **Incomplete**: `load_conf` was only implemented to meet 25 | /// the needs of a single config, so it is currently hard-coded 26 | /// to require a CA cert, a client cert, and skip hostname verification. 27 | /// PRs for improving this are much appreciated. 28 | /// 29 | /// ## Examples 30 | /// 31 | /// ```no_run 32 | /// # use kubeclient::prelude::*; 33 | /// let kube = Kubernetes::load_conf("admin.conf")?; 34 | /// ``` 35 | pub fn load_conf>(path: P) -> Result { 36 | Ok(Kubernetes{ 37 | low_level: KubeLowLevel::load_conf(path)?, 38 | namespace: None, 39 | }) 40 | } 41 | 42 | /// Get a kubernetes client for managing `ConfigMaps` 43 | /// 44 | /// ## Examples 45 | /// 46 | /// ```no_run 47 | /// # use kubeclient::prelude::*; 48 | /// let kube = Kubernetes::load_conf("admin.conf")?; 49 | /// if kube.config_maps().exists("my-config-map")? { 50 | /// println!("Found 'my-config-map'") 51 | /// } 52 | /// ``` 53 | pub fn config_maps(&self) -> KubeClient { 54 | KubeClient { kube: self.clone(), _marker: PhantomData } 55 | } 56 | 57 | /// Get a kubernetes client for managing `Deployments` 58 | /// 59 | /// ## Examples 60 | /// 61 | /// ```no_run 62 | /// # use kubeclient::prelude::*; 63 | /// let kube = Kubernetes::load_conf("admin.conf")?; 64 | /// if kube.deployments().exists("web-server")? { 65 | /// println!("Found 'web-server' deployment") 66 | /// } 67 | /// ``` 68 | pub fn deployments(&self) -> KubeClient { 69 | KubeClient { kube: self.clone(), _marker: PhantomData } 70 | } 71 | 72 | /// Get a kubernetes client for managing `NetworkPolicies` 73 | /// 74 | /// ## Examples 75 | /// 76 | /// ```no_run 77 | /// # use kubeclient::prelude::*; 78 | /// let kube = Kubernetes::load_conf("admin.conf")?; 79 | /// if kube.network_policies().exists("web-server")? { 80 | /// println!("Found 'web-server' network policy") 81 | /// } 82 | /// ``` 83 | pub fn network_policies(&self) -> KubeClient { 84 | KubeClient { kube: self.clone(), _marker: PhantomData } 85 | } 86 | 87 | /// Get a kubernetes client for managing `Nodes` 88 | /// 89 | /// ## Examples 90 | /// 91 | /// ```no_run 92 | /// # use kubeclient::prelude::*; 93 | /// let kube = Kubernetes::load_conf("admin.conf")?; 94 | /// if kube.nodes().exists("node-123")? { 95 | /// println!("Found 'node-123'") 96 | /// } 97 | /// ``` 98 | pub fn nodes(&self) -> KubeClient { 99 | KubeClient { kube: self.clone(), _marker: PhantomData } 100 | } 101 | 102 | /// Get a kubernetes client for managing `Pods` 103 | /// 104 | /// ## Examples 105 | /// 106 | /// ```no_run 107 | /// # use kubeclient::prelude::*; 108 | /// let kube = Kubernetes::load_conf("admin.conf")?; 109 | /// if kube.pods().exists("web-server-abcdefgh12345678")? { 110 | /// println!("Found 'web-server-abcdefgh12345678' pod") 111 | /// } 112 | /// ``` 113 | pub fn pods(&self) -> KubeClient { 114 | KubeClient { kube: self.clone(), _marker: PhantomData } 115 | } 116 | 117 | /// Get a kubernetes client for managing `Secrets` 118 | /// 119 | /// ## Examples 120 | /// 121 | /// ```no_run 122 | /// # use kubeclient::prelude::*; 123 | /// let kube = Kubernetes::load_conf("admin.conf")?; 124 | /// if kube.secrets().exists("my-secret")? { 125 | /// println!("Found 'my-secret'") 126 | /// } 127 | /// ``` 128 | pub fn secrets(&self) -> KubeClient { 129 | KubeClient { kube: self.clone(), _marker: PhantomData } 130 | } 131 | 132 | /// Get a kubernetes client for managing `Services` 133 | /// 134 | /// ## Examples 135 | /// 136 | /// ```no_run 137 | /// # use kubeclient::prelude::*; 138 | /// let kube = Kubernetes::load_conf("admin.conf")?; 139 | /// if kube.services().exists("web-server")? { 140 | /// println!("Found 'web-server' service") 141 | /// } 142 | /// ``` 143 | pub fn services(&self) -> KubeClient { 144 | KubeClient { kube: self.clone(), _marker: PhantomData } 145 | } 146 | 147 | /// Get a kubernetes client that uses a specific namespace 148 | /// 149 | /// ## Examples 150 | /// 151 | /// ```no_run 152 | /// # use kubeclient::prelude::*; 153 | /// let kube = Kubernetes::load_conf("admin.conf")?; 154 | /// let cluster_info = kube.namespace("kube-system") 155 | /// .secrets() 156 | /// .get("clusterinfo")?; 157 | /// ``` 158 | pub fn namespace(&self, namespace: &str) -> Kubernetes { 159 | Kubernetes { low_level: self.low_level.clone(), namespace: Some(namespace.to_owned()) } 160 | } 161 | 162 | /// Check to see if the Kubernetes API is healthy 163 | /// 164 | /// ## Examples 165 | /// 166 | /// ```no_run 167 | /// # use kubeclient::prelude::*; 168 | /// let kube = Kubernetes::load_conf("admin.conf")?; 169 | /// let is_healthy = kube.healthy()?; 170 | /// ``` 171 | pub fn healthy(&self) -> Result { 172 | Ok(self.low_level.health()? == "ok") 173 | } 174 | 175 | /// Applies a JSON or YAML resource file 176 | /// 177 | /// This is similar to the `kubectl apply` CLI commands. 178 | /// 179 | /// This may be a single file or an entire directory. 180 | /// If the resource(s) specified already exists, this method 181 | /// will NOT replace the resource. 182 | /// 183 | /// ## Examples 184 | /// 185 | /// ```no_run 186 | /// # use kubeclient::prelude::*; 187 | /// let kube = Kubernetes::load_conf("admin.conf")?; 188 | /// let is_healthy = kube.apply("web-server/deployment.yaml")?; 189 | /// ``` 190 | pub fn apply>(&self, path: P) -> Result<()> { 191 | let _: Vec = self.low_level.each_resource_path(path, |path| { 192 | self.low_level.apply_file(&path) 193 | .chain_err(|| format!("Failed to apply {}", path.display())) 194 | })?; 195 | 196 | Ok(()) 197 | } 198 | 199 | /// Replaces a JSON or YAML resource file 200 | /// 201 | /// This is similar to the `kubectl replace` CLI commands. 202 | /// 203 | /// This may be a single file or an entire directory. 204 | /// If the resource(s) specified already exists, this method 205 | /// will replace the resource. 206 | /// 207 | /// ## Examples 208 | /// 209 | /// ```no_run 210 | /// # use kubeclient::prelude::*; 211 | /// let kube = Kubernetes::load_conf("admin.conf")?; 212 | /// let is_healthy = kube.replace("web-server/deployment.yaml")?; 213 | /// ``` 214 | pub fn replace>(&self, path: P) -> Result<()> { 215 | let _: Vec = self.low_level.each_resource_path(path, |path| { 216 | self.low_level.replace_file(&path) 217 | .chain_err(|| format!("Failed to replace {}", path.display())) 218 | })?; 219 | 220 | Ok(()) 221 | } 222 | 223 | /// Creates a resource from a typed resource defintion 224 | /// 225 | /// This is similar to the `kubectl create` CLI commands. 226 | /// 227 | /// **Note**: most of the resource type defintions are incomplete 228 | /// Pull requests to fill missing fields/types are appreciated (especially if documented). 229 | /// 230 | /// ## Examples: 231 | /// 232 | /// ```no_run 233 | /// # use kubeclient::prelude::*; 234 | /// # use kubeclient::resources::Secret; 235 | /// let kube = Kubernetes::load_conf("admin.conf")?; 236 | /// let mut secret = Secret::new("web-server"); 237 | /// secret.insert("db_password", "abc123"); 238 | /// let response = kube.create(&secret)?; 239 | /// ``` 240 | pub fn create(&self, resource: &R) -> Result { 241 | let mut route = KindRoute::new(R::api(), R::kind().plural); 242 | if let Some(ns) = self.get_ns::() { 243 | route.namespace(ns); 244 | } 245 | self.low_level.apply(&route, resource) 246 | } 247 | 248 | // Methods below this point are the generic resource read/write methods. 249 | // They are not exposed publicly, as most of them have no way to infer 250 | // the generic argument in typical usage, `kube.exists::("web-server")?` 251 | // is decidedly less ergonomic than `kube.deployments().exists("web-server")?`. 252 | 253 | fn exists(&self, name: &str) -> Result { 254 | let mut route = ResourceRoute::new(R::api(), R::kind().plural, name); 255 | if let Some(ns) = self.get_ns::() { 256 | route.namespace(ns); 257 | } 258 | self.low_level.exists(&route) 259 | } 260 | 261 | fn get(&self, name: &str) -> Result { 262 | let mut route = ResourceRoute::new(R::api(), R::kind().plural, name); 263 | if let Some(ns) = self.get_ns::() { 264 | route.namespace(ns); 265 | } 266 | self.low_level.get(&route) 267 | } 268 | 269 | fn list(&self, query: Option<&ListQuery>) -> Result> { 270 | let mut route = KindRoute::new(R::api(), R::kind().plural); 271 | if let Some(ns) = self.get_ns::() { 272 | route.namespace(ns); 273 | } 274 | if let Some(query) = query { 275 | route.query(query.as_query_pairs()); 276 | } 277 | let response: R::ListResponse = self.low_level.list(&route)?; 278 | Ok(R::list_items(response)) 279 | } 280 | 281 | fn delete(&self, name: &str) -> Result<()> { 282 | let mut route = ResourceRoute::new(R::api(), R::kind().plural, name); 283 | if let Some(ns) = self.get_ns::() { 284 | route.namespace(ns); 285 | } 286 | self.low_level.delete(&route) 287 | } 288 | 289 | fn get_ns<'a, R: Resource>(&'a self) -> Option<&'a str> { 290 | match self.namespace { 291 | Some(ref ns) => Some(ns), 292 | None => R::default_namespace(), 293 | } 294 | } 295 | } -------------------------------------------------------------------------------- /src/clients/resource_clients.rs: -------------------------------------------------------------------------------- 1 | use super::Kubernetes; 2 | use resources::*; 3 | use errors::*; 4 | use std::marker::PhantomData; 5 | use super::ResourceRoute; 6 | 7 | 8 | pub struct KubeClient { 9 | pub(super) kube: Kubernetes, 10 | pub(super) _marker: PhantomData, 11 | } 12 | 13 | impl KubeClient { 14 | /// Get a kubernetes client that uses a specific namespace 15 | pub fn namespace(&self, namespace: &str) -> Self { 16 | KubeClient { kube: self.kube.namespace(namespace), _marker: PhantomData } 17 | } 18 | } 19 | 20 | // impl KubeClient { 21 | // // FIXME_FOR_BEER: exec requires SPD upgrade. Here are a few relevant issues 22 | // // https://stackoverflow.com/questions/37349440/upgrade-request-required-when-running-exec-in-kubernetes#37396806 23 | // // https://github.com/kubernetes-incubator/client-python/issues/58 24 | // pub fn exec(&self, pod_name: &str, exec: PodExec) -> Result { 25 | // let resource = format!("{}/exec", pod_name); 26 | // let mut route = ResourceRoute::new(Pod::api(), Pod::kind().route(), &resource); 27 | // if let Some(ns) = self.kube.get_ns::() { 28 | // route.namespace(ns); 29 | // } 30 | // route.query(exec.as_query_pairs()); 31 | 32 | // let url = route.build(&self.kube.low_level.base_url)?; 33 | // println!("URL: {}", url); 34 | // let resp = self.kube.low_level.http_get(url)?; 35 | // println!("EXEC: {:#?}", resp); 36 | // Ok("FIXME".to_owned()) 37 | // } 38 | // } 39 | 40 | impl KubeClient { 41 | /// Scale a deployment to a specific number of pods 42 | /// 43 | /// ## Examples 44 | /// 45 | /// ```no_run 46 | /// # use kubeclient::prelude::*; 47 | /// let kube = Kubernetes::load_conf("admin.conf")?; 48 | /// kube.deployments().scale("web-server", 7)?; 49 | /// ``` 50 | pub fn scale(&self, deployment_name: &str, count: u32) -> Result { 51 | let resource = format!("{}/scale", deployment_name); 52 | let mut route = ResourceRoute::new(Deployment::api(), Deployment::kind().plural, &resource); 53 | let ns = self.kube.get_ns::().expect("Namespace necessary for kubernetes scale operation"); 54 | route.namespace(ns); 55 | 56 | let url = route.build(&self.kube.low_level.base_url)?; 57 | 58 | let body = Scale::replicas(&ns, &deployment_name, count); 59 | let resp = self.kube.low_level.http_put_json(url, &body)?; 60 | Ok(resp) 61 | } 62 | } 63 | 64 | pub trait ReadClient { 65 | type R; 66 | /// Indicates whether or not the named resource exists in the Kubernetes cluster 67 | /// 68 | /// ## Examples 69 | /// 70 | /// ```no_run 71 | /// # use kubeclient::prelude::*; 72 | /// let kube = Kubernetes::load_conf("admin.conf")?; 73 | /// if kube.config_maps().exists("my-config-map")? { 74 | /// println!("Found 'my-config-map'") 75 | /// } 76 | /// ``` 77 | fn exists(&self, name: &str) -> Result; 78 | 79 | /// Gets the named resource 80 | /// 81 | /// This is similar to the `kubectl describe` CLI commands. 82 | /// 83 | /// ## Examples 84 | /// 85 | /// ```no_run 86 | /// # use kubeclient::prelude::*; 87 | /// let kube = Kubernetes::load_conf("admin.conf")?; 88 | /// let cfg_map = kube.config_maps().get("my-config-map")?; 89 | /// ``` 90 | fn get(&self, name: &str) -> Result; 91 | } 92 | 93 | pub trait WriteClient { 94 | type R; 95 | 96 | /// Creates the named resource 97 | /// 98 | /// This is similar to the `kubectl create` CLI commands. 99 | /// 100 | /// **Note**: most of the resource type defintions are incomplete 101 | /// Pull requests to fill missing fields/types are appreciated (especially if documented). 102 | /// 103 | /// ## Examples: 104 | /// 105 | /// ```no_run 106 | /// # use kubeclient::prelude::*; 107 | /// # use kubeclient::resources::ConfigMap; 108 | /// let kube = Kubernetes::load_conf("admin.conf")?; 109 | /// let mut cfg_map = ConfigMap::new("stage-config")?; 110 | /// cfg_map.insert("environment", "production"); 111 | /// // Note: using the `config_maps()` builder here is optional since resource type can be inferred 112 | /// let response = kube.config_maps().create(&cfg_map)?; 113 | /// ``` 114 | fn create(&self, resource: &Self::R) -> Result; 115 | 116 | /// Deleteds the named resource 117 | /// 118 | /// This is similar to the `kubectl delete` CLI commands. 119 | /// 120 | /// ## Examples 121 | /// 122 | /// ```no_run 123 | /// # use kubeclient::prelude::*; 124 | /// let kube = Kubernetes::load_conf("admin.conf")?; 125 | /// kube.config_maps().delete("my-config-map")?; 126 | /// ``` 127 | fn delete(&self, name: &str) -> Result<()>; 128 | } 129 | 130 | pub trait ListClient { 131 | type R; 132 | /// Lists resources of a particular type 133 | /// 134 | /// This is similar to the `kubectl get` CLI commands. 135 | /// 136 | /// The `query` paramater allows for customizing the list request, e.g., 137 | /// setting a timeout or specifying a label selector for filtering results. 138 | /// 139 | /// ## Examples 140 | /// 141 | /// ```no_run 142 | /// # use kubeclient::prelude::*; 143 | /// let kube = Kubernetes::load_conf("admin.conf")?; 144 | /// let cfg_maps = kube.config_maps().list("my-config-map", None)?; 145 | /// ``` 146 | fn list(&self, query: Option<&ListQuery>) -> Result>; 147 | } 148 | 149 | impl ReadClient for KubeClient { 150 | type R = R; 151 | 152 | fn exists(&self, name: &str) -> Result { 153 | self.kube.exists::(name) 154 | } 155 | fn get(&self, name: &str) -> Result { 156 | self.kube.get::(name) 157 | } 158 | } 159 | 160 | impl ListClient for KubeClient { 161 | type R = R; 162 | 163 | fn list(&self, query: Option<&ListQuery>) -> Result> { 164 | self.kube.list::(query) 165 | } 166 | 167 | } 168 | 169 | impl WriteClient for KubeClient { 170 | type R = R; 171 | 172 | fn create(&self, resource: &Self::R) -> Result { 173 | self.kube.create(resource) 174 | } 175 | 176 | fn delete(&self, name: &str) -> Result<()> { 177 | self.kube.delete::(name) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | //! Types and helpers for `kubeconfig` parsing. 2 | 3 | // Lifted from https://github.com/camallo/k8s-client-rs/blob/master/src/kubeconfig.rs 4 | // until a more complete kubernetes client exists 5 | 6 | use std::env; 7 | use std::fs::File; 8 | use std::io::prelude::*; 9 | use std::path::{Path, PathBuf}; 10 | use serde_yaml; 11 | use openssl::x509::X509; 12 | use openssl::pkey::{PKey, Private}; 13 | use url::Url; 14 | use url_serde; 15 | use base64; 16 | use errors::*; 17 | 18 | /// Configuration to build a Kubernetes client. 19 | #[derive(Debug, Serialize, Deserialize)] 20 | pub struct KubeConfig { 21 | pub kind: Option, 22 | #[serde(rename = "apiVersion")] 23 | pub api_version: Option, 24 | pub preferences: Option, 25 | pub clusters: Vec, 26 | pub users: Vec, 27 | pub contexts: Vec, 28 | #[serde(rename = "current-context")] 29 | pub current_context: String, 30 | pub extensions: Option>, 31 | } 32 | 33 | #[derive(Debug, Serialize, Deserialize)] 34 | pub struct Preferences { 35 | pub colors: Option, 36 | pub extensions: Option>, 37 | } 38 | 39 | #[derive(Debug, Serialize, Deserialize)] 40 | pub struct NamedCluster { 41 | pub name: String, 42 | pub cluster: Cluster, 43 | } 44 | 45 | #[derive(Clone, Debug, Serialize, Deserialize)] 46 | pub struct Cluster { 47 | #[serde(with = "url_serde")] 48 | pub server: Url, 49 | #[serde(rename = "insecure-skip-tls-verify")] 50 | pub insecure_tls: Option, 51 | #[serde(rename = "certificate-authority")] 52 | ca_file: Option, 53 | #[serde(rename = "certificate-authority-data")] 54 | ca_data: Option, 55 | pub extensions: Option>, 56 | } 57 | 58 | /// Given two Option parameters representing 59 | /// base64 encoded data, or file name with the data 60 | /// return the data form the first found in the specified order. 61 | fn get_from_b64data_or_file(data: &Option, file: &Option) -> Option { 62 | if let &Some(ref data) = data { 63 | let decoded = base64::decode(&data).expect("Unable to decode base64."); 64 | Some(String::from_utf8(decoded).expect("Unable to convert to string.")) 65 | } else if let &Some(ref file) = file { 66 | let mut data = String::new(); 67 | let mut f = File::open(file).expect("Unable to open file."); 68 | f.read_to_string(&mut data) 69 | .expect("File data is not UTF-8."); 70 | Some(data) 71 | } else { 72 | None 73 | } 74 | } 75 | 76 | impl Cluster { 77 | pub fn ca_cert(&self) -> Option { 78 | get_from_b64data_or_file(&self.ca_data, &self.ca_file).map(|k| { 79 | X509::from_pem(k.as_ref()).expect("Invalid kubeconfig - ca cert is not PEM-encoded") 80 | }) 81 | } 82 | } 83 | 84 | #[derive(Clone, Debug, Serialize, Deserialize)] 85 | pub struct NamedAuthInfo { 86 | pub name: String, 87 | pub user: AuthInfo, 88 | } 89 | 90 | #[derive(Clone, Debug, Serialize, Deserialize)] 91 | pub struct AuthInfo { 92 | pub username: Option, 93 | pub password: Option, 94 | pub token: Option, 95 | #[serde(rename = "tokenFile")] 96 | pub token_file: Option, 97 | #[serde(rename = "client-certificate")] 98 | client_certificate_file: Option, 99 | #[serde(rename = "client-certificate-data")] 100 | client_certificate_data: Option, 101 | #[serde(rename = "client-key")] 102 | pub client_key_file: Option, 103 | #[serde(rename = "client-key-data")] 104 | pub client_key_data: Option, 105 | pub impersonate: Option, 106 | //TODO 107 | } 108 | 109 | impl AuthInfo { 110 | pub fn client_certificate(&self) -> Option { 111 | get_from_b64data_or_file(&self.client_certificate_data, &self.client_certificate_file) 112 | .map(|k| X509::from_pem(k.as_ref()) 113 | .expect("Invalid kubeconfig - client cert is not PEM-encoded")) 114 | } 115 | pub fn client_key(&self) -> Option> { 116 | get_from_b64data_or_file(&self.client_key_data, &self.client_key_file) 117 | .map(|k| PKey::private_key_from_pem(k.as_ref()) 118 | .expect("Invalid kubeconfig - client key is not PEM-encoded")) 119 | } 120 | } 121 | 122 | #[derive(Clone, Debug, Serialize, Deserialize)] 123 | pub struct NamedContext { 124 | pub name: String, 125 | pub context: Context, 126 | } 127 | 128 | #[derive(Clone, Debug, Serialize, Deserialize)] 129 | pub struct Context { 130 | pub cluster: String, 131 | pub user: String, 132 | pub namespace: Option, 133 | pub extensions: Option>, 134 | } 135 | 136 | #[derive(Clone, Debug, Serialize, Deserialize)] 137 | pub struct NamedExtension { 138 | pub name: String, 139 | pub extension: Extension, 140 | } 141 | 142 | #[derive(Clone, Debug, Serialize, Deserialize)] 143 | pub struct Extension { 144 | pub extension: String, 145 | } 146 | 147 | #[derive(Clone, Debug)] 148 | pub struct ClusterContext { 149 | pub name: String, 150 | pub cluster: Cluster, 151 | pub user: AuthInfo, 152 | pub namespace: Option, 153 | pub extensions: Option>, 154 | } 155 | 156 | impl KubeConfig { 157 | pub fn load>(path: P) -> Result { 158 | let f = File::open(path.as_ref()).chain_err(|| "Unable to open kubeconfig file")?; 159 | serde_yaml::from_reader(f).chain_err(|| "Unable to parse kubeconfig file") 160 | } 161 | 162 | pub fn context(&self, name: &str) -> Result { 163 | let ctxs: Vec<&NamedContext> = self.contexts.iter().filter(|c| c.name == name).collect(); 164 | let ctx = match ctxs.len() { 165 | 0 => bail!("unknown context {}", name), 166 | 1 => &ctxs[0].context, 167 | _ => bail!("ambiguous context {}", name), 168 | }; 169 | let clus: Vec<&NamedCluster> = self.clusters 170 | .iter() 171 | .filter(|c| c.name == ctx.cluster) 172 | .collect(); 173 | let clu = match clus.len() { 174 | 0 => bail!("unknown cluster {}", name), 175 | 1 => &clus[0].cluster, 176 | _ => bail!("ambiguous cluster {}", name), 177 | }; 178 | let auths: Vec<&NamedAuthInfo> = self.users 179 | .iter() 180 | .filter(|c| c.name == ctx.user) 181 | .collect(); 182 | let auth = match auths.len() { 183 | 0 => bail!("unknown auth-info {}", name), 184 | 1 => &auths[0].user, 185 | _ => bail!("ambiguous auth-info {}", name), 186 | }; 187 | let rc = ClusterContext { 188 | name: name.to_string(), 189 | cluster: clu.clone(), 190 | user: auth.clone(), 191 | namespace: ctx.namespace.clone(), 192 | extensions: None, 193 | }; 194 | Ok(rc) 195 | } 196 | 197 | pub fn default_context(&self) -> Result { 198 | let dname = self.current_context.as_ref(); 199 | self.context(dname) 200 | } 201 | 202 | pub fn default_path() -> PathBuf { 203 | env::home_dir() 204 | .unwrap_or("/root".into()) 205 | .join(".kube") 206 | .join("config") 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | 2 | 3 | error_chain! { 4 | foreign_links { 5 | Io(::std::io::Error); 6 | Json(::serde_json::Error); 7 | Yaml(::serde_yaml::Error); 8 | Url(::url::ParseError); 9 | Http(::reqwest::Error); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! An ergonomic Kubernetes API client to manage kubernetes resources 2 | //! 3 | //! **Disclaimer**: This crate is still super very incomplete in functionality. 4 | //! So expect to file issues and PRs to unblock yourself if you actually 5 | //! take this crate as a dependency. 6 | //! 7 | //! ## Basic Usage 8 | //! 9 | //! The `prelude` contains several the main [`Kubernetes`](clients/struct.Kubernetes.html) type 10 | //! as well as several traits that expose the resource-specific methods for reading and writing 11 | //! kubernetes resources. 12 | //! 13 | //! ```no_run 14 | //! use kubeclient::prelude::*; 15 | //! 16 | //! let kube = Kubernetes::load_conf("admin.conf")?; 17 | //! 18 | //! if kube.healthy()? { 19 | //! if !kube.secrets().exists("my-secret")? { 20 | //! let output = kube.secrets().get("my-secret")? 21 | //! // ... 22 | //! } 23 | //! 24 | //! for node in kube.nodes().list()? { 25 | //! println!("Found node: {}", node.metadata.name.unwrap()); 26 | //! } 27 | //! } 28 | //! ``` 29 | 30 | #[macro_use] extern crate error_chain; 31 | #[macro_use] extern crate serde_derive; 32 | 33 | extern crate base64; 34 | extern crate chrono; 35 | extern crate headers_ext; 36 | extern crate openssl; 37 | extern crate k8s_openapi; 38 | extern crate reqwest; 39 | extern crate serde; 40 | extern crate serde_json; 41 | extern crate serde_yaml; 42 | extern crate url; 43 | extern crate url_serde; 44 | extern crate walkdir; 45 | 46 | pub mod errors; 47 | pub mod config; 48 | pub mod clients; 49 | pub mod resources; 50 | 51 | pub mod prelude { 52 | pub use clients::{Kubernetes, ReadClient, WriteClient, ListClient}; 53 | } 54 | 55 | pub use clients::Kubernetes; 56 | pub use config::KubeConfig; 57 | pub use errors::Error; 58 | 59 | -------------------------------------------------------------------------------- /src/resources/config_map.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use std::collections::BTreeMap; 3 | use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; 4 | 5 | pub(crate) static CONFIG_MAP_INFO: KindInfo = KindInfo { 6 | plural: "configmaps", 7 | default_namespace: Some("default"), 8 | api: V1_API, 9 | }; 10 | 11 | #[derive(Serialize, Deserialize, Debug)] 12 | pub struct ConfigMap { 13 | /// Data contains the configuration data. Each key must consist of alphanumeric characters, '-', '_' or '.'. 14 | data: BTreeMap, 15 | 16 | /// Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata 17 | metadata: ObjectMeta, 18 | } 19 | 20 | impl ConfigMap { 21 | pub fn new(name: &str) -> ConfigMap { 22 | let data = BTreeMap::new(); 23 | let metadata = ObjectMeta{ name: Some(name.to_owned()), ..Default::default() }; 24 | ConfigMap { data, metadata } 25 | } 26 | 27 | pub fn insert(&mut self, name: K, data: V) -> &mut ConfigMap 28 | where K: Into, 29 | V: Into, 30 | { 31 | self.data.insert(name.into(), data.into()); 32 | self 33 | } 34 | 35 | pub fn append(&mut self, map: M) -> &mut ConfigMap 36 | where K: Into, 37 | V: Into, 38 | M: IntoIterator 39 | { 40 | let mut encoded_map = map.into_iter() 41 | .map(|(k,v)| (k.into(), v.into())) 42 | .collect(); 43 | self.data.append(&mut encoded_map); 44 | self 45 | } 46 | } 47 | 48 | impl Resource for ConfigMap { 49 | fn kind() -> Kind { Kind::ConfigMap } 50 | } 51 | -------------------------------------------------------------------------------- /src/resources/config_map.rs:3:5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anowell/kubeclient-rs/38c93d0e315f25f77cf8645dfd016ef31d970d44/src/resources/config_map.rs:3:5 -------------------------------------------------------------------------------- /src/resources/daemon_set.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use k8s_openapi::api::apps::v1beta2::{DaemonSetSpec, DaemonSetStatus}; 3 | use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; 4 | 5 | pub(crate) static DAEMON_SET_INFO: KindInfo = KindInfo { 6 | plural: "daemonsets", 7 | default_namespace: Some("default"), 8 | api: V1_BETA_API, 9 | }; 10 | 11 | #[derive(Serialize, Deserialize, Debug)] 12 | pub struct DaemonSet { 13 | /// The desired behavior of this daemon set. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#spec-and-status 14 | pub spec: DaemonSetSpec, 15 | 16 | /// Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata 17 | pub metadata: ObjectMeta, 18 | 19 | /// The current status of this daemon set. This data may be out of date by some window of time. Populated by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#spec-and-status 20 | #[serde(skip_serializing_if = "Option::is_none")] 21 | pub status: Option, 22 | } 23 | 24 | #[derive(Serialize, Deserialize, Debug, Default)] 25 | pub struct DaemonSetList { 26 | items: Vec, 27 | } 28 | 29 | impl DaemonSet { 30 | pub fn new(name: &str) -> DaemonSet { 31 | let spec = DaemonSetSpec::default(); 32 | let metadata = ObjectMeta{ name: Some(name.to_owned()), ..Default::default() }; 33 | DaemonSet { spec, metadata, status: None } 34 | } 35 | } 36 | 37 | impl Resource for DaemonSet { 38 | fn kind() -> Kind { Kind::DaemonSet } 39 | } 40 | 41 | impl ListableResource for DaemonSet { 42 | type ListResponse = DaemonSetList; 43 | fn list_items(response: Self::ListResponse) -> Vec { 44 | response.items 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/resources/deployment.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use k8s_openapi::api::apps::v1::{DeploymentSpec, DeploymentStatus}; 3 | use k8s_openapi::api::apps::v1beta1::{ScaleSpec, ScaleStatus}; 4 | use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; 5 | 6 | pub(crate) static DEPLOYMENT_INFO: KindInfo = KindInfo { 7 | plural: "deployments", 8 | default_namespace: Some("default"), 9 | api: V1_BETA_API, 10 | }; 11 | 12 | #[derive(Serialize, Deserialize, Debug, Default)] 13 | pub struct Deployment { 14 | /// Specification of the desired behavior of the Deployment. 15 | pub spec: DeploymentSpec, 16 | 17 | /// Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata 18 | pub metadata: ObjectMeta, 19 | 20 | /// Most recently observed status of the Deployment. 21 | pub status: Option, 22 | } 23 | 24 | #[derive(Serialize, Deserialize, Debug, Default)] 25 | #[serde(rename_all = "camelCase")] 26 | pub struct Scale { 27 | /// defines the behavior of the scale. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#spec-and-status. 28 | pub spec: ScaleSpec, 29 | 30 | /// Standard object metadata; More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata. 31 | pub metadata: ObjectMeta, 32 | 33 | /// current status of the scale. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#spec-and-status. Read-only. 34 | #[serde(skip_serializing_if = "Option::is_none")] 35 | pub status: Option, 36 | } 37 | 38 | impl Scale { 39 | pub(crate) fn replicas(namespace: &str, name: &str, count: u32) -> Scale { 40 | Scale { 41 | spec: ScaleSpec { replicas: Some(count as i32) }, 42 | metadata: ObjectMeta { 43 | name: Some(name.to_owned()), 44 | namespace: Some(namespace.to_owned()), 45 | ..Default::default() 46 | }, 47 | ..Default::default() 48 | } 49 | } 50 | } 51 | 52 | #[derive(Serialize, Deserialize, Debug, Default)] 53 | pub struct DeploymentList { 54 | items: Vec, 55 | } 56 | 57 | impl Deployment { 58 | pub fn new(name: &str) -> Deployment { 59 | let metadata = ObjectMeta{ name: Some(name.to_owned()), ..Default::default() }; 60 | Deployment { metadata, ..Default::default() } 61 | } 62 | } 63 | 64 | impl Resource for Deployment { 65 | fn kind() -> Kind { Kind::Deployment } 66 | } 67 | 68 | impl ListableResource for Deployment { 69 | type ListResponse = DeploymentList; 70 | fn list_items(response: Self::ListResponse) -> Vec { 71 | response.items 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/resources/mod.rs: -------------------------------------------------------------------------------- 1 | mod secret; 2 | mod config_map; 3 | mod node; 4 | mod daemon_set; 5 | mod deployment; 6 | mod network_policy; 7 | mod pod; 8 | mod service; 9 | 10 | pub use self::secret::*; 11 | pub use self::config_map::*; 12 | pub use self::node::*; 13 | pub use self::daemon_set::*; 14 | pub use self::deployment::*; 15 | pub use self::network_policy::*; 16 | pub use self::pod::*; 17 | pub use self::service::*; 18 | 19 | use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; 20 | use serde::Serialize; 21 | use serde::de::DeserializeOwned; 22 | use std::fmt; 23 | use std::collections::BTreeMap; 24 | use std::ops::Deref; 25 | 26 | pub(crate) const V1_API: &str = "/api/v1"; 27 | pub(crate) const V1_BETA_API: &str = "/apis/extensions/v1beta1"; 28 | 29 | #[derive(Serialize, Deserialize, Debug)] 30 | pub enum Kind { DaemonSet, Deployment, ConfigMap, NetworkPolicy, Node, Pod, Secret, Service } 31 | 32 | impl Deref for Kind { 33 | type Target = KindInfo; 34 | fn deref(&self) -> &KindInfo { 35 | match *self { 36 | Kind::ConfigMap => &CONFIG_MAP_INFO, 37 | Kind::DaemonSet => &DAEMON_SET_INFO, 38 | Kind::Deployment => &DEPLOYMENT_INFO, 39 | Kind::NetworkPolicy => &NETWORK_POLICY_INFO, 40 | Kind::Node => &NODE_INFO, 41 | Kind::Pod => &POD_INFO, 42 | Kind::Secret => &SECRET_INFO, 43 | Kind::Service => &SERVICE_INFO, 44 | } 45 | } 46 | } 47 | 48 | #[derive(Copy, Clone, Debug)] 49 | pub struct KindInfo { 50 | pub plural: &'static str, 51 | pub default_namespace: Option<&'static str>, 52 | pub api: &'static str, 53 | } 54 | 55 | // Debug output of Kind is exactly what we want for Display 56 | impl fmt::Display for Kind { 57 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 58 | fmt::Debug::fmt(self, f) 59 | } 60 | } 61 | 62 | pub trait Resource: Serialize + DeserializeOwned { 63 | fn kind() -> Kind; 64 | fn api() -> &'static str { 65 | Self::kind().api 66 | } 67 | fn default_namespace() -> Option<&'static str> { 68 | Self::kind().default_namespace 69 | } 70 | } 71 | 72 | pub trait ListableResource: Resource { 73 | type ListResponse: DeserializeOwned; 74 | fn list_items(response: Self::ListResponse) -> Vec; 75 | } 76 | 77 | #[derive(Serialize, Deserialize, Debug)] 78 | #[serde(rename_all = "camelCase")] 79 | pub struct Status { 80 | pub kind: String, 81 | pub api_version: String, 82 | pub metadata: ObjectMeta, 83 | pub status: String, 84 | pub message: String, 85 | } 86 | 87 | #[derive(Clone, Debug, Default)] 88 | pub struct ListQuery { 89 | field_selector: Option, 90 | label_selector: Option, 91 | resource_version: Option, 92 | timeout_seconds: Option, 93 | } 94 | 95 | impl ListQuery { 96 | pub fn as_query_pairs(&self) -> BTreeMap<&str, String> { 97 | let mut map = BTreeMap::new(); 98 | if let Some(ref fs) = self.field_selector { 99 | map.insert("fieldSelector", fs.to_owned()); 100 | } 101 | if let Some(ref ls) = self.label_selector { 102 | map.insert("labelSelector", ls.to_owned()); 103 | } 104 | if let Some(ref rv) = self.resource_version { 105 | map.insert("resourceVersion", rv.to_owned()); 106 | } 107 | if let Some(ref ts) = self.timeout_seconds { 108 | map.insert("timeoutSeconds", ts.to_owned()); 109 | } 110 | map 111 | } 112 | 113 | /// Be aware of: https://github.com/kubernetes/kubernetes/issues/1362 114 | pub fn field_selector>(mut self, field_selector: S) -> Self { 115 | self.field_selector = Some(field_selector.into()); 116 | self 117 | } 118 | 119 | pub fn label_selector>(&self, label_selector: S) -> Self { 120 | let mut new = self.clone(); 121 | new.label_selector = Some(label_selector.into()); 122 | new 123 | } 124 | pub fn resource_version>(&self, resource_version: S) -> Self { 125 | let mut new = self.clone(); 126 | new.resource_version = Some(resource_version.into()); 127 | new 128 | } 129 | pub fn timeout_seconds(&self, timeout_seconds: u32) -> Self { 130 | let mut new = self.clone(); 131 | new.timeout_seconds = Some(timeout_seconds.to_string()); 132 | new 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/resources/network_policy.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use k8s_openapi::api::extensions::v1beta1::NetworkPolicySpec; 3 | use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; 4 | 5 | pub(crate) static NETWORK_POLICY_INFO: KindInfo = KindInfo { 6 | plural: "networkpolicies", 7 | default_namespace: Some("default"), 8 | api: V1_BETA_API, 9 | }; 10 | 11 | #[derive(Serialize, Deserialize, Default, Debug)] 12 | pub struct NetworkPolicy { 13 | /// Specification of the desired behavior for this NetworkPolicy. 14 | pub spec: NetworkPolicySpec, 15 | 16 | /// Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata 17 | pub metadata: ObjectMeta, 18 | } 19 | 20 | #[derive(Serialize, Deserialize, Debug, Default)] 21 | pub struct NetworkPolicyList { 22 | items: Vec, 23 | } 24 | 25 | 26 | impl NetworkPolicy { 27 | pub fn new(name: &str) -> NetworkPolicy { 28 | let metadata = ObjectMeta{ name: Some(name.to_owned()), ..Default::default() }; 29 | NetworkPolicy { metadata, ..Default::default() } 30 | } 31 | } 32 | 33 | impl Resource for NetworkPolicy { 34 | fn kind() -> Kind { Kind::NetworkPolicy } 35 | } 36 | 37 | 38 | impl ListableResource for NetworkPolicy { 39 | type ListResponse = NetworkPolicyList; 40 | fn list_items(response: Self::ListResponse) -> Vec { 41 | response.items 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/resources/node.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use k8s_openapi::api::core::v1::{NodeSpec, NodeStatus}; 3 | use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; 4 | 5 | pub(crate) static NODE_INFO: KindInfo = KindInfo { 6 | plural: "nodes", 7 | default_namespace: None, 8 | api: V1_API, 9 | }; 10 | 11 | #[derive(Serialize, Deserialize, Default, Debug)] 12 | pub struct Node { 13 | /// Spec defines the behavior of a node. https://git.k8s.io/community/contributors/devel/api-conventions.md#spec-and-status 14 | pub spec: NodeSpec, 15 | 16 | /// Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata 17 | pub metadata: ObjectMeta, 18 | 19 | /// Most recently observed status of the node. Populated by the system. Read-only. 20 | /// More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#spec-and-status 21 | #[serde(skip_serializing_if = "Option::is_none")] 22 | pub status: Option, 23 | } 24 | 25 | #[derive(Serialize, Deserialize, Debug, Default)] 26 | pub struct NodeList { 27 | items: Vec, 28 | } 29 | 30 | impl Node { 31 | pub fn new(name: &str) -> Node { 32 | let metadata = ObjectMeta{ name: Some(name.to_owned()), ..Default::default() }; 33 | Node { metadata, ..Default::default() } 34 | } 35 | } 36 | 37 | impl Resource for Node { 38 | fn kind() -> Kind { Kind::Node } 39 | } 40 | 41 | impl ListableResource for Node { 42 | type ListResponse = NodeList; 43 | fn list_items(response: Self::ListResponse) -> Vec { 44 | response.items 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/resources/node.rs:2:30: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anowell/kubeclient-rs/38c93d0e315f25f77cf8645dfd016ef31d970d44/src/resources/node.rs:2:30 -------------------------------------------------------------------------------- /src/resources/pod.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use k8s_openapi::api::core::v1::{PodSpec, PodStatus}; 3 | use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; 4 | 5 | pub(crate) static POD_INFO: KindInfo = KindInfo { 6 | plural: "pods", 7 | default_namespace: Some("default"), 8 | api: V1_API, 9 | }; 10 | 11 | #[derive(Serialize, Deserialize, Debug, Default)] 12 | pub struct Pod { 13 | /// Specification of the desired behavior of the pod. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#spec-and-status 14 | pub spec: PodSpec, 15 | 16 | /// Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata 17 | pub metadata: ObjectMeta, 18 | 19 | /// Most recently observed status of the pod. This data may not be up to date. Populated by the system. Read-only. 20 | /// More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#spec-and-status 21 | pub status: Option, 22 | } 23 | 24 | #[derive(Serialize, Deserialize, Debug, Default)] 25 | pub struct PodList { 26 | items: Vec, 27 | } 28 | 29 | #[derive(Serialize, Debug, Default)] 30 | pub struct PodExec { 31 | stdin: Option, 32 | stdout: Option, 33 | stderr: Option, 34 | tty: Option, 35 | container: Option, 36 | command: Option>, 37 | } 38 | 39 | impl PodExec { 40 | pub fn tty(mut self) -> PodExec { 41 | self.tty = Some(true); 42 | self 43 | } 44 | 45 | pub fn command(mut self, command: Vec) -> PodExec { 46 | self.command = Some(command); 47 | self 48 | } 49 | 50 | pub fn as_query_pairs(&self) -> BTreeMap<&'static str, String> { 51 | let mut query = BTreeMap::new(); 52 | if let Some(stdin) = self.stdin { 53 | query.insert("stdin", stdin.to_string()); 54 | } 55 | if let Some(stdout) = self.stdout { 56 | query.insert("stdout", stdout.to_string()); 57 | } 58 | if let Some(tty) = self.tty { 59 | query.insert("tty", tty.to_string()); 60 | } 61 | if let Some(ref container) = self.container { 62 | query.insert("container", container.to_owned()); 63 | } 64 | if let Some(ref command) = self.command { 65 | query.insert("command", command.join(" ")); 66 | } 67 | query 68 | } 69 | } 70 | 71 | impl Pod { 72 | pub fn new(name: &str) -> Pod { 73 | let metadata = ObjectMeta{ name: Some(name.to_owned()), ..Default::default() }; 74 | Pod { metadata, ..Default::default() } 75 | } 76 | } 77 | 78 | impl Resource for Pod { 79 | fn kind() -> Kind { Kind::Pod } 80 | } 81 | 82 | 83 | impl ListableResource for Pod { 84 | type ListResponse = PodList; 85 | fn list_items(response: Self::ListResponse) -> Vec { 86 | response.items 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/resources/secret.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use std::collections::BTreeMap; 3 | use base64; 4 | use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; 5 | 6 | pub(crate) static SECRET_INFO: KindInfo = KindInfo { 7 | plural: "secrets", 8 | default_namespace: Some("default"), 9 | api: V1_API, 10 | }; 11 | 12 | #[derive(Serialize, Deserialize, Debug)] 13 | pub struct Secret { 14 | data: BTreeMap, 15 | metadata: ObjectMeta, 16 | } 17 | 18 | impl Secret { 19 | pub fn new(name: &str) -> Secret { 20 | let data = BTreeMap::new(); 21 | let metadata = ObjectMeta{ name: Some(name.to_owned()), ..Default::default() }; 22 | Secret { data, metadata } 23 | } 24 | 25 | pub fn insert(&mut self, name: K, secret: V) -> &mut Secret 26 | where K: Into, 27 | V: AsRef<[u8]>, 28 | { 29 | self.data.insert(name.into(), base64::encode(secret.as_ref())); 30 | self 31 | } 32 | 33 | pub fn append(&mut self, map: M) -> &mut Secret 34 | where K: Into, 35 | V: AsRef<[u8]>, 36 | M: IntoIterator 37 | { 38 | let mut encoded_map = map.into_iter() 39 | .map(|(k,v)| (k.into(), base64::encode(v.as_ref()))) 40 | .collect(); 41 | self.data.append(&mut encoded_map); 42 | self 43 | } 44 | 45 | pub fn get(&self, name: K) -> Option> 46 | where K: AsRef 47 | { 48 | self.data.get(name.as_ref()) 49 | .map(|raw| base64::decode(&raw).expect("BUG: secret wasn't base64 encoded")) 50 | } 51 | } 52 | 53 | impl Resource for Secret { 54 | fn kind() -> Kind { Kind::Secret } 55 | } 56 | -------------------------------------------------------------------------------- /src/resources/secret.rs:4:5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anowell/kubeclient-rs/38c93d0e315f25f77cf8645dfd016ef31d970d44/src/resources/secret.rs:4:5 -------------------------------------------------------------------------------- /src/resources/service.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; 3 | use k8s_openapi::api::core::v1::{ServiceSpec, ServiceStatus}; 4 | 5 | pub(crate) static SERVICE_INFO: KindInfo = KindInfo { 6 | plural: "services", 7 | default_namespace: Some("default"), 8 | api: V1_API, 9 | }; 10 | 11 | #[derive(Serialize, Deserialize, Debug, Default)] 12 | pub struct Service { 13 | /// Spec defines the behavior of a service. https://git.k8s.io/community/contributors/devel/api-conventions.md#spec-and-status 14 | pub spec: ServiceSpec, 15 | 16 | /// Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata 17 | pub metadata: ObjectMeta, 18 | 19 | /// Most recently observed status of the service. Populated by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#spec-and-status 20 | #[serde(skip_serializing_if = "Option::is_none")] 21 | pub status: Option, 22 | } 23 | 24 | #[derive(Serialize, Deserialize, Debug, Default)] 25 | pub struct ServiceList { 26 | items: Vec, 27 | } 28 | 29 | impl Service { 30 | pub fn new(name: &str) -> Service { 31 | let metadata = ObjectMeta{ name: Some(name.to_owned()), ..Default::default() }; 32 | Service { metadata, ..Default::default() } 33 | } 34 | } 35 | 36 | impl Resource for Service { 37 | fn kind() -> Kind { Kind::Service } 38 | } 39 | 40 | 41 | impl ListableResource for Service { 42 | type ListResponse = ServiceList; 43 | fn list_items(response: Self::ListResponse) -> Vec { 44 | response.items 45 | } 46 | } 47 | --------------------------------------------------------------------------------