Skip to content

Commit

Permalink
Merge pull request #46 from ragne/patch-strategy-support
Browse files Browse the repository at this point in the history
#24 PatchParams and PatchStrategy implementation
  • Loading branch information
clux authored Jul 10, 2019
2 parents 93d663f + ec69b6b commit 4360d46
Show file tree
Hide file tree
Showing 7 changed files with 127 additions and 32 deletions.
7 changes: 4 additions & 3 deletions examples/crd_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use either::Either::{Left, Right};
use serde_json::json;

use kube::{
api::{RawApi, PostParams, DeleteParams, ListParams, Object, ObjectList, Void},
api::{RawApi, PostParams, DeleteParams, ListParams, Object, ObjectList, Void, PatchParams},
client::APIClient,
config,
};
Expand Down Expand Up @@ -79,6 +79,7 @@ fn main() -> Result<(), failure::Error> {

info!("Creating CRD foos.clux.dev");
let pp = PostParams::default();
let patch_params = PatchParams::default();
let req = crds.create(&pp, serde_json::to_vec(&foocrd)?)?;
match client.request::<FullCrd>(req) {
Ok(o) => {
Expand Down Expand Up @@ -170,7 +171,7 @@ fn main() -> Result<(), failure::Error> {
let fs = json!({
"status": FooStatus { is_bad: false }
});
let req = foos.patch_status("qux", &pp, serde_json::to_vec(&fs)?)?;
let req = foos.patch_status("qux", &patch_params, serde_json::to_vec(&fs)?)?;
let o = client.request::<Foo>(req)?;
info!("Patched status {:?} for {}", o.status, o.metadata.name);
assert!(!o.status.unwrap().is_bad);
Expand All @@ -187,7 +188,7 @@ fn main() -> Result<(), failure::Error> {
let patch = json!({
"spec": { "info": "patched qux" }
});
let req = foos.patch("qux", &pp, serde_json::to_vec(&patch)?)?;
let req = foos.patch("qux", &patch_params, serde_json::to_vec(&patch)?)?;
let o = client.request::<Foo>(req)?;
info!("Patched {} with new name: {}", o.metadata.name, o.spec.name);
assert_eq!(o.spec.info, "patched qux");
Expand Down
9 changes: 5 additions & 4 deletions examples/crd_openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use either::Either::{Left, Right};
use serde_json::json;

use kube::{
api::{Api, PostParams, DeleteParams, ListParams, Object},
api::{Api, PostParams, DeleteParams, ListParams, Object, PatchParams},
client::{APIClient},
config,
};
Expand Down Expand Up @@ -76,6 +76,7 @@ fn main() -> Result<(), failure::Error> {

info!("Creating CRD foos.clux.dev");
let pp = PostParams::default();
let patch_params = PatchParams::default();
match crds.create(&pp, serde_json::to_vec(&foocrd)?) {
Ok(o) => {
info!("Created {} ({:?})", o.metadata.name, o.status);
Expand Down Expand Up @@ -168,7 +169,7 @@ fn main() -> Result<(), failure::Error> {
let fs = json!({
"status": FooStatus { is_bad: false, replicas: 1 }
});
let o = foos.patch_status("qux", &pp, serde_json::to_vec(&fs)?)?;
let o = foos.patch_status("qux", &patch_params, serde_json::to_vec(&fs)?)?;
info!("Patched status {:?} for {}", o.status, o.metadata.name);
assert!(!o.status.unwrap().is_bad);

Expand All @@ -187,7 +188,7 @@ fn main() -> Result<(), failure::Error> {
let fs = json!({
"spec": { "replicas": 2 }
});
let o = foos.patch_scale("qux", &pp, serde_json::to_vec(&fs)?)?;
let o = foos.patch_scale("qux", &patch_params, serde_json::to_vec(&fs)?)?;
info!("Patched scale {:?} for {}", o.spec, o.metadata.name);
assert_eq!(o.status.unwrap().replicas, 1);
assert_eq!(o.spec.replicas.unwrap(), 2); // we only asked for more
Expand All @@ -197,7 +198,7 @@ fn main() -> Result<(), failure::Error> {
let patch = json!({
"spec": { "info": "patched qux" }
});
let o = foos.patch("qux", &pp, serde_json::to_vec(&patch)?)?;
let o = foos.patch("qux", &patch_params, serde_json::to_vec(&patch)?)?;
info!("Patched {} with new name: {}", o.metadata.name, o.spec.name);
assert_eq!(o.spec.info, "patched qux");
assert_eq!(o.spec.name, "qux"); // didn't blat existing params
Expand Down
5 changes: 3 additions & 2 deletions examples/pod_openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
use serde_json::json;

use kube::{
api::{Api, PostParams, DeleteParams, ListParams},
api::{Api, PostParams, DeleteParams, ListParams, PatchParams},
client::{APIClient},
config,
};
Expand Down Expand Up @@ -63,7 +63,8 @@ fn main() -> Result<(), failure::Error> {
"activeDeadlineSeconds": 5
}
});
let p_patched = pods.patch("blog", &pp, serde_json::to_vec(&patch)?)?;
let patch_params = PatchParams::default();
let p_patched = pods.patch("blog", &patch_params, serde_json::to_vec(&patch)?)?;
assert_eq!(p_patched.spec.active_deadline_seconds, Some(5));

for p in pods.list(&ListParams::default())?.items {
Expand Down
2 changes: 2 additions & 0 deletions src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ pub use raw::{
RawApi,
ListParams,
PostParams,
PatchParams,
DeleteParams,
PropagationPolicy,
PatchStrategy
};

mod typed;
Expand Down
125 changes: 106 additions & 19 deletions src/api/raw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,12 +206,86 @@ pub struct ListParams {
pub timeout: Option<u32>
}

/// Common query parameters for put/post/patch calls
/// Common query parameters for put/post calls
#[derive(Default, Clone)]
pub struct PostParams {
pub dry_run: bool,
}

/// Common query parameters for patch calls
#[derive(Default, Clone)]
pub struct PatchParams {
pub dry_run: bool,
/// Strategy which will be used. Defaults to `PatchStrategy::Strategic`
pub patch_strategy: PatchStrategy,
/// force Apply requests. Applicable only to `PatchStrategy::Apply`
pub force: bool,
/// fieldManager is a name of the actor that is making changes. Required for `PatchStrategy::Apply`
/// optional for everything else
pub field_manager: Option<String>
}

impl PatchParams {
fn validate(&self) -> Result<()> {
if let Some(field_manager) = &self.field_manager {
// Implement the easy part of validation, in future this may be extended to provide validation as in go code
// For now it's fine, because k8s API server will return an error
if field_manager.len() > 128 {
return Err(ErrorKind::RequestValidation("Failed to validate PatchParameters::field_manager!".to_owned()).into())
}
}

if self.patch_strategy != PatchStrategy::Apply && self.force {
// if not force, all other fields are valid for all types of patch requests
Err(ErrorKind::RequestValidation("Force is applicable only for Apply strategy!".to_owned()).into())
} else {
Ok(())
}
}

fn populate_qp(&self, qp: &mut url::form_urlencoded::Serializer<String>) {
if self.dry_run {
qp.append_pair("dryRun", "true");
}
if self.force {
qp.append_pair("force", "true");
}
if let Some(ref field_manager) = self.field_manager {
qp.append_pair("fieldManager", &field_manager);
}
}
}


/// For patch different patch types are supported. See https://kubernetes.io/docs/tasks/run-application/update-api-object-kubectl-patch/#use-a-json-merge-patch-to-update-a-deployment
/// Apply strategy is kinda special
#[derive(Clone, PartialEq)]
pub enum PatchStrategy {
Apply,
JSON,
Merge,
Strategic
}

impl std::fmt::Display for PatchStrategy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let content_type = match &self {
PatchStrategy::Apply => "application/apply-patch+yaml",
PatchStrategy::JSON => "application/json-patch+json",
PatchStrategy::Merge => "application/merge-patch+json",
PatchStrategy::Strategic => "application/strategic-merge-patch+json"
};
f.write_str(content_type)
}
}

// Kubectl defaults to Strategic strategy, but doing so will break existing consumers
// so, currently we still default to Merge it may change in future versions
// Strategic merge doesn't work with CRD types https://github.com/kubernetes/kubernetes/issues/52772
impl Default for PatchStrategy {
fn default() -> Self { PatchStrategy::Merge }
}

/// Common query parameters for delete calls
#[derive(Default, Clone)]
pub struct DeleteParams {
Expand Down Expand Up @@ -364,17 +438,16 @@ impl RawApi {
/// Patch an instance of a resource
///
/// Requires a serialized merge-patch+json at the moment.
pub fn patch(&self, name: &str, pp: &PostParams, patch: Vec<u8>) -> Result<http::Request<Vec<u8>>> {
pub fn patch(&self, name: &str, pp: &PatchParams, patch: Vec<u8>) -> Result<http::Request<Vec<u8>>> {
pp.validate()?;
let base_url = self.make_url() + "/" + name + "?";
let mut qp = url::form_urlencoded::Serializer::new(base_url);
if pp.dry_run {
qp.append_pair("dryRun", "All");
}
pp.populate_qp(&mut qp);
let urlstr = qp.finish();

Ok(http::Request::patch(urlstr)
.header("Accept", "application/json")
.header("Content-Type", "application/merge-patch+json")
.header("Content-Type", pp.patch_strategy.to_string())
.body(patch).context(ErrorKind::RequestBuild)?)
}

Expand Down Expand Up @@ -402,16 +475,15 @@ impl RawApi {
}

/// Patch an instance of the scale subresource
pub fn patch_scale(&self, name: &str, pp: &PostParams, patch: Vec<u8>) -> Result<http::Request<Vec<u8>>> {
pub fn patch_scale(&self, name: &str, pp: &PatchParams, patch: Vec<u8>) -> Result<http::Request<Vec<u8>>> {
pp.validate()?;
let base_url = self.make_url() + "/" + name + "/scale?";
let mut qp = url::form_urlencoded::Serializer::new(base_url);
if pp.dry_run {
qp.append_pair("dryRun", "All");
}
pp.populate_qp(&mut qp);
let urlstr = qp.finish();
Ok(http::Request::patch(urlstr)
.header("Accept", "application/json")
.header("Content-Type", "application/merge-patch+json")
.header("Content-Type", pp.patch_strategy.to_string())
.body(patch).context(ErrorKind::RequestBuild)?)
}

Expand All @@ -437,16 +509,15 @@ impl RawApi {
}

/// Patch an instance of the status subresource
pub fn patch_status(&self, name: &str, pp: &PostParams, patch: Vec<u8>) -> Result<http::Request<Vec<u8>>> {
pub fn patch_status(&self, name: &str, pp: &PatchParams, patch: Vec<u8>) -> Result<http::Request<Vec<u8>>> {
pp.validate()?;
let base_url = self.make_url() + "/" + name + "/status?";
let mut qp = url::form_urlencoded::Serializer::new(base_url);
if pp.dry_run {
qp.append_pair("dryRun", "All");
}
pp.populate_qp(&mut qp);
let urlstr = qp.finish();
Ok(http::Request::patch(urlstr)
.header("Accept", "application/json")
.header("Content-Type", "application/merge-patch+json")
.header("Content-Type", pp.patch_strategy.to_string())
.body(patch).context(ErrorKind::RequestBuild)?)
}

Expand Down Expand Up @@ -518,13 +589,27 @@ fn namespace_path() { // weird object compared to other v1
assert_eq!(req.uri(), "/api/v1/namespaces")
}

#[test]
fn patch_params_validation() {
let pp = PatchParams::default();
assert!(pp.validate().is_ok(), "default params should always be valid");

let patch_strategy_apply_true = PatchParams {
patch_strategy: PatchStrategy::Merge,
force: true,
..Default::default()
};
assert!(patch_strategy_apply_true.validate().is_err(), "Merge strategy shouldn't be valid if `force` set to true");
}

// subresources with weird version accuracy
#[test]
fn patch_status_path(){
let r = RawApi::v1Node();
let pp = PostParams::default();
let pp = PatchParams::default();
let req = r.patch_status("mynode", &pp, vec![]).unwrap();
assert_eq!(req.uri(), "/api/v1/nodes/mynode/status?");
assert_eq!(req.headers().get("Content-Type").unwrap().to_str().unwrap(), format!("{}", PatchStrategy::Merge));
assert_eq!(req.method(), "PATCH");
}
#[test]
Expand All @@ -546,10 +631,12 @@ fn create_custom_resource() {
assert_eq!(req.uri(),
"/apis/clux.dev/v1/namespaces/myns/foos?"
);
let req = r.patch("baz", &pp, vec![]).unwrap();
let patch_params = PatchParams::default();
let req = r.patch("baz", &patch_params, vec![]).unwrap();
assert_eq!(req.uri(),
"/apis/clux.dev/v1/namespaces/myns/foos/baz?"
);
assert_eq!(req.method(), "PATCH");
}

#[test]
Expand All @@ -571,7 +658,7 @@ fn get_scale_path(){
#[test]
fn patch_scale_path(){
let r = RawApi::v1Node();
let pp = PostParams::default();
let pp = PatchParams::default();
let req = r.patch_scale("mynode", &pp, vec![]).unwrap();
assert_eq!(req.uri(), "/api/v1/nodes/mynode/scale?");
assert_eq!(req.method(), "PATCH");
Expand Down
7 changes: 4 additions & 3 deletions src/api/typed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::api::{
PostParams,
DeleteParams,
ListParams,
PatchParams
};
use crate::api::resource::{
ObjectList, Object, WatchEvent, KubeObject,
Expand Down Expand Up @@ -80,7 +81,7 @@ impl<K> Api<K> where
let req = self.api.delete_collection(&lp)?;
self.client.request_status::<ObjectList<K>>(req)
}
pub fn patch(&self, name: &str, pp: &PostParams, patch: Vec<u8>) -> Result<K> {
pub fn patch(&self, name: &str, pp: &PatchParams, patch: Vec<u8>) -> Result<K> {
let req = self.api.patch(name, &pp, patch)?;
self.client.request::<K>(req)
}
Expand All @@ -96,7 +97,7 @@ impl<K> Api<K> where
let req = self.api.get_status(name)?;
self.client.request::<K>(req)
}
pub fn patch_status(&self, name: &str, pp: &PostParams, patch: Vec<u8>) -> Result<K> {
pub fn patch_status(&self, name: &str, pp: &PatchParams, patch: Vec<u8>) -> Result<K> {
let req = self.api.patch_status(name, &pp, patch)?;
self.client.request::<K>(req)
}
Expand Down Expand Up @@ -129,7 +130,7 @@ impl<K> Api<K> where
let req = self.api.get_scale(name)?;
self.client.request::<Scale>(req)
}
pub fn patch_scale(&self, name: &str, pp: &PostParams, patch: Vec<u8>) -> Result<Scale> {
pub fn patch_scale(&self, name: &str, pp: &PatchParams, patch: Vec<u8>) -> Result<Scale> {
let req = self.api.patch_scale(name, &pp, patch)?;
self.client.request::<Scale>(req)
}
Expand Down
4 changes: 3 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ pub enum ErrorKind {
#[fail(display = "Error parsing response")]
RequestParse,
#[fail(display = "Invalid API method {}", _0)]
InvalidMethod(String)
InvalidMethod(String),
#[fail(display = "Request validation failed with {}", _0)]
RequestValidation(String),
}

use std::fmt::{self, Display};
Expand Down

0 comments on commit 4360d46

Please sign in to comment.