kube_core/
request.rs

1//! Request builder type for arbitrary api types
2use thiserror::Error;
3
4use crate::params::GetParams;
5
6use super::params::{DeleteParams, ListParams, Patch, PatchParams, PostParams, WatchParams};
7
8pub(crate) const JSON_MIME: &str = "application/json";
9/// Extended Accept Header
10///
11/// Requests a meta.k8s.io/v1 PartialObjectMetadata resource (efficiently
12/// retrieves object metadata)
13///
14/// API Servers running Kubernetes v1.14 and below will retrieve the object and then
15/// convert the metadata.
16pub(crate) const JSON_METADATA_MIME: &str = "application/json;as=PartialObjectMetadata;g=meta.k8s.io;v=v1";
17
18pub(crate) const JSON_METADATA_LIST_MIME: &str =
19    "application/json;as=PartialObjectMetadataList;g=meta.k8s.io;v=v1";
20
21/// Possible errors when building a request.
22#[derive(Debug, Error)]
23pub enum Error {
24    /// Failed to build a request.
25    #[error("failed to build request: {0}")]
26    BuildRequest(#[source] http::Error),
27    /// Failed to serialize body.
28    #[error("failed to serialize body: {0}")]
29    SerializeBody(#[source] serde_json::Error),
30    /// Failed to validate request.
31    #[error("failed to validate request: {0}")]
32    Validation(String),
33}
34
35/// A Kubernetes request builder
36///
37/// Takes a base_path and supplies constructors for common operations
38/// The extra operations all return `http::Request` objects.
39#[derive(Debug, Clone)]
40pub struct Request {
41    /// The path component of a url
42    pub url_path: String,
43}
44
45impl Request {
46    /// New request with a resource's url path
47    pub fn new<S: Into<String>>(url_path: S) -> Self {
48        Self {
49            url_path: url_path.into(),
50        }
51    }
52}
53
54// -------------------------------------------------------
55
56/// Convenience methods found from API conventions
57impl Request {
58    /// List a collection of a resource
59    pub fn list(&self, lp: &ListParams) -> Result<http::Request<Vec<u8>>, Error> {
60        let target = format!("{}?", self.url_path);
61        let mut qp = form_urlencoded::Serializer::new(target);
62        lp.validate()?;
63        lp.populate_qp(&mut qp);
64        let urlstr = qp.finish();
65        let req = http::Request::get(urlstr);
66        req.body(vec![]).map_err(Error::BuildRequest)
67    }
68
69    /// Watch a resource at a given version
70    pub fn watch(&self, wp: &WatchParams, ver: &str) -> Result<http::Request<Vec<u8>>, Error> {
71        let target = format!("{}?", self.url_path);
72        let mut qp = form_urlencoded::Serializer::new(target);
73        wp.validate()?;
74        wp.populate_qp(&mut qp);
75        qp.append_pair("resourceVersion", ver);
76        let urlstr = qp.finish();
77        let req = http::Request::get(urlstr);
78        req.body(vec![]).map_err(Error::BuildRequest)
79    }
80
81    /// Get a single instance
82    pub fn get(&self, name: &str, gp: &GetParams) -> Result<http::Request<Vec<u8>>, Error> {
83        validate_name(name)?;
84        let urlstr = if let Some(rv) = &gp.resource_version {
85            let target = format!("{}/{}?", self.url_path, name);
86            form_urlencoded::Serializer::new(target)
87                .append_pair("resourceVersion", rv)
88                .finish()
89        } else {
90            let target = format!("{}/{}", self.url_path, name);
91            form_urlencoded::Serializer::new(target).finish()
92        };
93        let req = http::Request::get(urlstr);
94        req.body(vec![]).map_err(Error::BuildRequest)
95    }
96
97    /// Create an instance of a resource
98    pub fn create(&self, pp: &PostParams, data: Vec<u8>) -> Result<http::Request<Vec<u8>>, Error> {
99        pp.validate()?;
100        let target = format!("{}?", self.url_path);
101        let mut qp = form_urlencoded::Serializer::new(target);
102        pp.populate_qp(&mut qp);
103        let urlstr = qp.finish();
104        let req = http::Request::post(urlstr).header(http::header::CONTENT_TYPE, JSON_MIME);
105        req.body(data).map_err(Error::BuildRequest)
106    }
107
108    /// Delete an instance of a resource
109    pub fn delete(&self, name: &str, dp: &DeleteParams) -> Result<http::Request<Vec<u8>>, Error> {
110        validate_name(name)?;
111        let target = format!("{}/{}?", self.url_path, name);
112        let mut qp = form_urlencoded::Serializer::new(target);
113        let urlstr = qp.finish();
114        let body = serde_json::to_vec(&dp).map_err(Error::SerializeBody)?;
115        let req = http::Request::delete(urlstr).header(http::header::CONTENT_TYPE, JSON_MIME);
116        req.body(body).map_err(Error::BuildRequest)
117    }
118
119    /// Delete a collection of a resource
120    pub fn delete_collection(
121        &self,
122        dp: &DeleteParams,
123        lp: &ListParams,
124    ) -> Result<http::Request<Vec<u8>>, Error> {
125        let target = format!("{}?", self.url_path);
126        let mut qp = form_urlencoded::Serializer::new(target);
127        if let Some(fields) = &lp.field_selector {
128            qp.append_pair("fieldSelector", fields);
129        }
130        if let Some(labels) = &lp.label_selector {
131            qp.append_pair("labelSelector", labels);
132        }
133        let urlstr = qp.finish();
134
135        let data = if dp.is_default() {
136            vec![] // default serialize needs to be empty body
137        } else {
138            serde_json::to_vec(&dp).map_err(Error::SerializeBody)?
139        };
140
141        let req = http::Request::delete(urlstr).header(http::header::CONTENT_TYPE, JSON_MIME);
142        req.body(data).map_err(Error::BuildRequest)
143    }
144
145    /// Patch an instance of a resource
146    ///
147    /// Requires a serialized merge-patch+json at the moment.
148    pub fn patch<P: serde::Serialize>(
149        &self,
150        name: &str,
151        pp: &PatchParams,
152        patch: &Patch<P>,
153    ) -> Result<http::Request<Vec<u8>>, Error> {
154        validate_name(name)?;
155        pp.validate(patch)?;
156        let target = format!("{}/{}?", self.url_path, name);
157        let mut qp = form_urlencoded::Serializer::new(target);
158        pp.populate_qp(&mut qp);
159        let urlstr = qp.finish();
160
161        http::Request::patch(urlstr)
162            .header(http::header::ACCEPT, JSON_MIME)
163            .header(http::header::CONTENT_TYPE, patch.content_type())
164            .body(patch.serialize().map_err(Error::SerializeBody)?)
165            .map_err(Error::BuildRequest)
166    }
167
168    /// Replace an instance of a resource
169    ///
170    /// Requires `metadata.resourceVersion` set in data
171    pub fn replace(
172        &self,
173        name: &str,
174        pp: &PostParams,
175        data: Vec<u8>,
176    ) -> Result<http::Request<Vec<u8>>, Error> {
177        validate_name(name)?;
178        let target = format!("{}/{}?", self.url_path, name);
179        let mut qp = form_urlencoded::Serializer::new(target);
180        pp.populate_qp(&mut qp);
181        let urlstr = qp.finish();
182        let req = http::Request::put(urlstr).header(http::header::CONTENT_TYPE, JSON_MIME);
183        req.body(data).map_err(Error::BuildRequest)
184    }
185}
186
187/// Subresources
188impl Request {
189    /// Get an instance of the subresource
190    pub fn get_subresource(
191        &self,
192        subresource_name: &str,
193        name: &str,
194    ) -> Result<http::Request<Vec<u8>>, Error> {
195        validate_name(name)?;
196        let target = format!("{}/{}/{}", self.url_path, name, subresource_name);
197        let mut qp = form_urlencoded::Serializer::new(target);
198        let urlstr = qp.finish();
199        let req = http::Request::get(urlstr);
200        req.body(vec![]).map_err(Error::BuildRequest)
201    }
202
203    /// Create an instance of the subresource
204    pub fn create_subresource(
205        &self,
206        subresource_name: &str,
207        name: &str,
208        pp: &PostParams,
209        data: Vec<u8>,
210    ) -> Result<http::Request<Vec<u8>>, Error> {
211        validate_name(name)?;
212        let target = format!("{}/{}/{}?", self.url_path, name, subresource_name);
213        let mut qp = form_urlencoded::Serializer::new(target);
214        pp.populate_qp(&mut qp);
215        let urlstr = qp.finish();
216        let req = http::Request::post(urlstr).header(http::header::CONTENT_TYPE, JSON_MIME);
217        req.body(data).map_err(Error::BuildRequest)
218    }
219
220    /// Patch an instance of the subresource
221    pub fn patch_subresource<P: serde::Serialize>(
222        &self,
223        subresource_name: &str,
224        name: &str,
225        pp: &PatchParams,
226        patch: &Patch<P>,
227    ) -> Result<http::Request<Vec<u8>>, Error> {
228        validate_name(name)?;
229        pp.validate(patch)?;
230        let target = format!("{}/{}/{}?", self.url_path, name, subresource_name);
231        let mut qp = form_urlencoded::Serializer::new(target);
232        pp.populate_qp(&mut qp);
233        let urlstr = qp.finish();
234
235        http::Request::patch(urlstr)
236            .header(http::header::ACCEPT, JSON_MIME)
237            .header(http::header::CONTENT_TYPE, patch.content_type())
238            .body(patch.serialize().map_err(Error::SerializeBody)?)
239            .map_err(Error::BuildRequest)
240    }
241
242    /// Replace an instance of the subresource
243    pub fn replace_subresource(
244        &self,
245        subresource_name: &str,
246        name: &str,
247        pp: &PostParams,
248        data: Vec<u8>,
249    ) -> Result<http::Request<Vec<u8>>, Error> {
250        validate_name(name)?;
251        let target = format!("{}/{}/{}?", self.url_path, name, subresource_name);
252        let mut qp = form_urlencoded::Serializer::new(target);
253        pp.populate_qp(&mut qp);
254        let urlstr = qp.finish();
255        let req = http::Request::put(urlstr).header(http::header::CONTENT_TYPE, JSON_MIME);
256        req.body(data).map_err(Error::BuildRequest)
257    }
258}
259
260/// Metadata-only request implementations
261///
262/// Requests set an extended Accept header compromised of JSON media type and
263/// additional parameters that retrieve only necessary metadata from an object.
264impl Request {
265    /// Get a single metadata instance for a named resource
266    pub fn get_metadata(&self, name: &str, gp: &GetParams) -> Result<http::Request<Vec<u8>>, Error> {
267        validate_name(name)?;
268        let urlstr = if let Some(rv) = &gp.resource_version {
269            let target = format!("{}/{}?", self.url_path, name);
270            form_urlencoded::Serializer::new(target)
271                .append_pair("resourceVersion", rv)
272                .finish()
273        } else {
274            let target = format!("{}/{}", self.url_path, name);
275            form_urlencoded::Serializer::new(target).finish()
276        };
277        let req = http::Request::get(urlstr)
278            .header(http::header::ACCEPT, JSON_METADATA_MIME)
279            .header(http::header::CONTENT_TYPE, JSON_MIME);
280        req.body(vec![]).map_err(Error::BuildRequest)
281    }
282
283    /// List a collection of metadata of a resource
284    pub fn list_metadata(&self, lp: &ListParams) -> Result<http::Request<Vec<u8>>, Error> {
285        let target = format!("{}?", self.url_path);
286        let mut qp = form_urlencoded::Serializer::new(target);
287        lp.validate()?;
288        lp.populate_qp(&mut qp);
289        let urlstr = qp.finish();
290        let req = http::Request::get(urlstr)
291            .header(http::header::ACCEPT, JSON_METADATA_LIST_MIME)
292            .header(http::header::CONTENT_TYPE, JSON_MIME);
293
294        req.body(vec![]).map_err(Error::BuildRequest)
295    }
296
297    /// Watch metadata of a resource at a given version
298    pub fn watch_metadata(&self, wp: &WatchParams, ver: &str) -> Result<http::Request<Vec<u8>>, Error> {
299        let target = format!("{}?", self.url_path);
300        let mut qp = form_urlencoded::Serializer::new(target);
301        wp.validate()?;
302        wp.populate_qp(&mut qp);
303        qp.append_pair("resourceVersion", ver);
304
305        let urlstr = qp.finish();
306        http::Request::get(urlstr)
307            .header(http::header::ACCEPT, JSON_METADATA_MIME)
308            .header(http::header::CONTENT_TYPE, JSON_MIME)
309            .body(vec![])
310            .map_err(Error::BuildRequest)
311    }
312
313    /// Patch an instance of a resource and receive its metadata only
314    ///
315    /// Requires a serialized merge-patch+json at the moment
316    pub fn patch_metadata<P: serde::Serialize>(
317        &self,
318        name: &str,
319        pp: &PatchParams,
320        patch: &Patch<P>,
321    ) -> Result<http::Request<Vec<u8>>, Error> {
322        validate_name(name)?;
323        pp.validate(patch)?;
324        let target = format!("{}/{}?", self.url_path, name);
325        let mut qp = form_urlencoded::Serializer::new(target);
326        pp.populate_qp(&mut qp);
327        let urlstr = qp.finish();
328
329        http::Request::patch(urlstr)
330            .header(http::header::ACCEPT, JSON_METADATA_MIME)
331            .header(http::header::CONTENT_TYPE, patch.content_type())
332            .body(patch.serialize().map_err(Error::SerializeBody)?)
333            .map_err(Error::BuildRequest)
334    }
335}
336
337/// Names must not be empty as otherwise API server would interpret a `get` as `list`, or a `delete` as `delete_collection`
338fn validate_name(name: &str) -> Result<(), Error> {
339    if name.is_empty() {
340        return Err(Error::Validation("A non-empty name is required".into()));
341    }
342    Ok(())
343}
344
345/// Extensive tests for Request of k8s_openapi::Resource structs
346///
347/// Cheap sanity check to ensure type maps work as expected
348#[cfg(test)]
349mod test {
350    use crate::{
351        params::{GetParams, PostParams, VersionMatch, WatchParams},
352        request::{Error, Request},
353        resource::Resource,
354    };
355    use http::header;
356    use k8s::{
357        admissionregistration::v1 as adregv1, apps::v1 as appsv1, authorization::v1 as authv1,
358        autoscaling::v1 as autoscalingv1, batch::v1 as batchv1, core::v1 as corev1,
359        networking::v1 as networkingv1, rbac::v1 as rbacv1, storage::v1 as storagev1,
360    };
361    use k8s_openapi::api as k8s;
362
363    // NB: stable requires >= 1.17
364    use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1 as apiextsv1;
365
366    // TODO: fixturize these tests
367    #[test]
368    fn api_url_secret() {
369        let url = corev1::Secret::url_path(&(), Some("ns"));
370        let req = Request::new(url).create(&PostParams::default(), vec![]).unwrap();
371        assert_eq!(req.uri(), "/api/v1/namespaces/ns/secrets?");
372        assert_eq!(req.headers().get(header::CONTENT_TYPE).unwrap(), super::JSON_MIME);
373    }
374
375    #[test]
376    fn api_url_rs() {
377        let url = appsv1::ReplicaSet::url_path(&(), Some("ns"));
378        let req = Request::new(url).create(&PostParams::default(), vec![]).unwrap();
379        assert_eq!(req.uri(), "/apis/apps/v1/namespaces/ns/replicasets?");
380    }
381    #[test]
382    fn api_url_role() {
383        let url = rbacv1::Role::url_path(&(), Some("ns"));
384        let req = Request::new(url).create(&PostParams::default(), vec![]).unwrap();
385        assert_eq!(
386            req.uri(),
387            "/apis/rbac.authorization.k8s.io/v1/namespaces/ns/roles?"
388        );
389    }
390
391    #[test]
392    fn api_url_cj() {
393        let url = batchv1::CronJob::url_path(&(), Some("ns"));
394        let req = Request::new(url).create(&PostParams::default(), vec![]).unwrap();
395        assert_eq!(req.uri(), "/apis/batch/v1/namespaces/ns/cronjobs?");
396    }
397    #[test]
398    fn api_url_hpa() {
399        let url = autoscalingv1::HorizontalPodAutoscaler::url_path(&(), Some("ns"));
400        let req = Request::new(url).create(&PostParams::default(), vec![]).unwrap();
401        assert_eq!(
402            req.uri(),
403            "/apis/autoscaling/v1/namespaces/ns/horizontalpodautoscalers?"
404        );
405    }
406
407    #[test]
408    fn api_url_np() {
409        let url = networkingv1::NetworkPolicy::url_path(&(), Some("ns"));
410        let req = Request::new(url).create(&PostParams::default(), vec![]).unwrap();
411        assert_eq!(
412            req.uri(),
413            "/apis/networking.k8s.io/v1/namespaces/ns/networkpolicies?"
414        );
415    }
416    #[test]
417    fn api_url_ingress() {
418        let url = networkingv1::Ingress::url_path(&(), Some("ns"));
419        let req = Request::new(url).create(&PostParams::default(), vec![]).unwrap();
420        assert_eq!(req.uri(), "/apis/networking.k8s.io/v1/namespaces/ns/ingresses?");
421    }
422
423    #[test]
424    fn api_url_vattach() {
425        let url = storagev1::VolumeAttachment::url_path(&(), None);
426        let req = Request::new(url).create(&PostParams::default(), vec![]).unwrap();
427        assert_eq!(req.uri(), "/apis/storage.k8s.io/v1/volumeattachments?");
428    }
429
430    #[test]
431    fn api_url_admission() {
432        let url = adregv1::ValidatingWebhookConfiguration::url_path(&(), None);
433        let req = Request::new(url).create(&PostParams::default(), vec![]).unwrap();
434        assert_eq!(
435            req.uri(),
436            "/apis/admissionregistration.k8s.io/v1/validatingwebhookconfigurations?"
437        );
438    }
439
440    #[test]
441    fn api_auth_selfreview() {
442        //assert_eq!(r.group, "authorization.k8s.io");
443        //assert_eq!(r.kind, "SelfSubjectRulesReview");
444        let url = authv1::SelfSubjectRulesReview::url_path(&(), None);
445        let req = Request::new(url).create(&PostParams::default(), vec![]).unwrap();
446        assert_eq!(
447            req.uri(),
448            "/apis/authorization.k8s.io/v1/selfsubjectrulesreviews?"
449        );
450    }
451
452    #[test]
453    fn api_apiextsv1_crd() {
454        let url = apiextsv1::CustomResourceDefinition::url_path(&(), None);
455        let req = Request::new(url).create(&PostParams::default(), vec![]).unwrap();
456        assert_eq!(
457            req.uri(),
458            "/apis/apiextensions.k8s.io/v1/customresourcedefinitions?"
459        );
460    }
461
462    /// -----------------------------------------------------------------
463    /// Tests that the misc mappings are also sensible
464    use crate::params::{DeleteParams, ListParams, Patch, PatchParams};
465
466    #[test]
467    fn get_metadata_path() {
468        let url = appsv1::Deployment::url_path(&(), Some("ns"));
469        let req = Request::new(url)
470            .get_metadata("mydeploy", &GetParams::default())
471            .unwrap();
472        println!("{}", req.uri());
473        assert_eq!(req.uri(), "/apis/apps/v1/namespaces/ns/deployments/mydeploy");
474        assert_eq!(req.method(), "GET");
475        assert_eq!(req.headers().get(header::CONTENT_TYPE).unwrap(), super::JSON_MIME);
476        assert_eq!(
477            req.headers().get(header::ACCEPT).unwrap(),
478            super::JSON_METADATA_MIME
479        );
480    }
481
482    #[test]
483    fn get_path_with_rv() {
484        let url = appsv1::Deployment::url_path(&(), Some("ns"));
485        let req = Request::new(url).get("mydeploy", &GetParams::any()).unwrap();
486        assert_eq!(
487            req.uri(),
488            "/apis/apps/v1/namespaces/ns/deployments/mydeploy?&resourceVersion=0"
489        );
490    }
491
492    #[test]
493    fn get_meta_path_with_rv() {
494        let url = appsv1::Deployment::url_path(&(), Some("ns"));
495        let req = Request::new(url)
496            .get_metadata("mydeploy", &GetParams::at("665"))
497            .unwrap();
498        assert_eq!(
499            req.uri(),
500            "/apis/apps/v1/namespaces/ns/deployments/mydeploy?&resourceVersion=665"
501        );
502
503        assert_eq!(req.method(), "GET");
504        assert_eq!(req.headers().get(header::CONTENT_TYPE).unwrap(), super::JSON_MIME);
505        assert_eq!(
506            req.headers().get(header::ACCEPT).unwrap(),
507            super::JSON_METADATA_MIME
508        );
509    }
510
511    #[test]
512    fn get_empty_name() {
513        let url = appsv1::Deployment::url_path(&(), Some("ns"));
514        let req = Request::new(url).get("", &GetParams::any());
515        assert!(matches!(req, Err(Error::Validation(_))));
516    }
517
518    #[test]
519    fn list_path() {
520        let url = appsv1::Deployment::url_path(&(), Some("ns"));
521        let lp = ListParams::default();
522        let req = Request::new(url).list(&lp).unwrap();
523        assert_eq!(req.uri(), "/apis/apps/v1/namespaces/ns/deployments");
524    }
525    #[test]
526    fn list_metadata_path() {
527        let url = appsv1::Deployment::url_path(&(), Some("ns"));
528        let lp = ListParams::default().matching(VersionMatch::NotOlderThan).at("5");
529        let req = Request::new(url).list_metadata(&lp).unwrap();
530        assert_eq!(
531            req.uri(),
532            "/apis/apps/v1/namespaces/ns/deployments?&resourceVersion=5&resourceVersionMatch=NotOlderThan"
533        );
534        assert_eq!(req.headers().get(header::CONTENT_TYPE).unwrap(), super::JSON_MIME);
535        assert_eq!(
536            req.headers().get(header::ACCEPT).unwrap(),
537            super::JSON_METADATA_LIST_MIME
538        );
539    }
540    #[test]
541    fn watch_path() {
542        let url = corev1::Pod::url_path(&(), Some("ns"));
543        let wp = WatchParams::default();
544        let req = Request::new(url).watch(&wp, "0").unwrap();
545        assert_eq!(
546            req.uri(),
547            "/api/v1/namespaces/ns/pods?&watch=true&timeoutSeconds=290&allowWatchBookmarks=true&resourceVersion=0"
548        );
549    }
550
551    #[test]
552    fn watch_streaming_list() {
553        let url = corev1::Pod::url_path(&(), Some("ns"));
554        let wp = WatchParams::default().initial_events();
555        let req = Request::new(url).watch(&wp, "0").unwrap();
556        assert_eq!(
557            req.uri(),
558            "/api/v1/namespaces/ns/pods?&watch=true&timeoutSeconds=290&allowWatchBookmarks=true&sendInitialEvents=true&resourceVersionMatch=NotOlderThan&resourceVersion=0"
559        );
560    }
561
562    #[test]
563    fn watch_metadata_path() {
564        let url = corev1::Pod::url_path(&(), Some("ns"));
565        let wp = WatchParams::default();
566        let req = Request::new(url).watch_metadata(&wp, "0").unwrap();
567        assert_eq!(
568            req.uri(),
569            "/api/v1/namespaces/ns/pods?&watch=true&timeoutSeconds=290&allowWatchBookmarks=true&resourceVersion=0"
570        );
571        assert_eq!(req.headers().get(header::CONTENT_TYPE).unwrap(), super::JSON_MIME);
572        assert_eq!(
573            req.headers().get(header::ACCEPT).unwrap(),
574            super::JSON_METADATA_MIME
575        );
576    }
577    #[test]
578    fn replace_path() {
579        let url = appsv1::DaemonSet::url_path(&(), None);
580        let pp = PostParams {
581            dry_run: true,
582            ..Default::default()
583        };
584        let req = Request::new(url).replace("myds", &pp, vec![]).unwrap();
585        assert_eq!(req.uri(), "/apis/apps/v1/daemonsets/myds?&dryRun=All");
586        assert_eq!(req.headers().get(header::CONTENT_TYPE).unwrap(), super::JSON_MIME);
587    }
588
589    #[test]
590    fn delete_path() {
591        let url = appsv1::ReplicaSet::url_path(&(), Some("ns"));
592        let dp = DeleteParams::default();
593        let req = Request::new(url).delete("myrs", &dp).unwrap();
594        assert_eq!(req.uri(), "/apis/apps/v1/namespaces/ns/replicasets/myrs");
595        assert_eq!(req.method(), "DELETE");
596        assert_eq!(req.headers().get(header::CONTENT_TYPE).unwrap(), super::JSON_MIME);
597    }
598
599    #[test]
600    fn delete_collection_path() {
601        let url = appsv1::ReplicaSet::url_path(&(), Some("ns"));
602        let lp = ListParams::default().labels("app=myapp");
603        let dp = DeleteParams::default();
604        let req = Request::new(url).delete_collection(&dp, &lp).unwrap();
605        assert_eq!(
606            req.uri(),
607            "/apis/apps/v1/namespaces/ns/replicasets?&labelSelector=app%3Dmyapp"
608        );
609        assert_eq!(req.method(), "DELETE");
610        assert_eq!(req.headers().get(header::CONTENT_TYPE).unwrap(), super::JSON_MIME);
611    }
612
613    #[test]
614    fn namespace_path() {
615        let url = corev1::Namespace::url_path(&(), None);
616        let gp = ListParams::default();
617        let req = Request::new(url).list(&gp).unwrap();
618        assert_eq!(req.uri(), "/api/v1/namespaces")
619    }
620
621    // subresources with weird version accuracy
622    #[test]
623    fn patch_status_path() {
624        let url = corev1::Node::url_path(&(), None);
625        let pp = PatchParams::default();
626        let req = Request::new(url)
627            .patch_subresource("status", "mynode", &pp, &Patch::Merge(()))
628            .unwrap();
629        assert_eq!(req.uri(), "/api/v1/nodes/mynode/status?");
630        assert_eq!(
631            req.headers().get("Content-Type").unwrap().to_str().unwrap(),
632            Patch::Merge(()).content_type()
633        );
634        assert_eq!(req.method(), "PATCH");
635    }
636    #[test]
637    fn patch_pod_metadata() {
638        let url = corev1::Pod::url_path(&(), Some("ns"));
639        let pp = PatchParams::default();
640        let req = Request::new(url)
641            .patch_metadata("mypod", &pp, &Patch::Merge(()))
642            .unwrap();
643        assert_eq!(req.uri(), "/api/v1/namespaces/ns/pods/mypod?");
644        assert_eq!(
645            req.headers().get(header::CONTENT_TYPE).unwrap(),
646            Patch::Merge(()).content_type()
647        );
648        assert_eq!(
649            req.headers().get(header::ACCEPT).unwrap(),
650            super::JSON_METADATA_MIME
651        );
652        assert_eq!(req.method(), "PATCH");
653    }
654    #[test]
655    fn replace_status_path() {
656        let url = corev1::Node::url_path(&(), None);
657        let pp = PostParams::default();
658        let req = Request::new(url)
659            .replace_subresource("status", "mynode", &pp, vec![])
660            .unwrap();
661        assert_eq!(req.uri(), "/api/v1/nodes/mynode/status?");
662        assert_eq!(req.method(), "PUT");
663        assert_eq!(req.headers().get(header::CONTENT_TYPE).unwrap(), super::JSON_MIME);
664    }
665
666    #[test]
667    fn create_ingress() {
668        // NB: Ingress exists in extensions AND networking
669        let url = networkingv1::Ingress::url_path(&(), Some("ns"));
670        let pp = PostParams::default();
671        let req = Request::new(&url).create(&pp, vec![]).unwrap();
672
673        assert_eq!(req.uri(), "/apis/networking.k8s.io/v1/namespaces/ns/ingresses?");
674        let patch_params = PatchParams::default();
675        let req = Request::new(url)
676            .patch("baz", &patch_params, &Patch::Merge(()))
677            .unwrap();
678        assert_eq!(
679            req.uri(),
680            "/apis/networking.k8s.io/v1/namespaces/ns/ingresses/baz?"
681        );
682        assert_eq!(req.method(), "PATCH");
683    }
684
685    #[test]
686    fn replace_status() {
687        let url = apiextsv1::CustomResourceDefinition::url_path(&(), None);
688        let pp = PostParams::default();
689        let req = Request::new(url)
690            .replace_subresource("status", "mycrd.domain.io", &pp, vec![])
691            .unwrap();
692        assert_eq!(
693            req.uri(),
694            "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/mycrd.domain.io/status?"
695        );
696    }
697    #[test]
698    fn get_scale_path() {
699        let url = corev1::Node::url_path(&(), None);
700        let req = Request::new(url).get_subresource("scale", "mynode").unwrap();
701        assert_eq!(req.uri(), "/api/v1/nodes/mynode/scale");
702        assert_eq!(req.method(), "GET");
703    }
704    #[test]
705    fn patch_scale_path() {
706        let url = corev1::Node::url_path(&(), None);
707        let pp = PatchParams::default();
708        let req = Request::new(url)
709            .patch_subresource("scale", "mynode", &pp, &Patch::Merge(()))
710            .unwrap();
711        assert_eq!(req.uri(), "/api/v1/nodes/mynode/scale?");
712        assert_eq!(req.method(), "PATCH");
713    }
714    #[test]
715    fn replace_scale_path() {
716        let url = corev1::Node::url_path(&(), None);
717        let pp = PostParams::default();
718        let req = Request::new(url)
719            .replace_subresource("scale", "mynode", &pp, vec![])
720            .unwrap();
721        assert_eq!(req.uri(), "/api/v1/nodes/mynode/scale?");
722        assert_eq!(req.method(), "PUT");
723    }
724
725    #[test]
726    fn create_subresource_path() {
727        let url = corev1::ServiceAccount::url_path(&(), Some("ns"));
728        let pp = PostParams::default();
729        let data = vec![];
730        let req = Request::new(url)
731            .create_subresource("token", "sa", &pp, data)
732            .unwrap();
733        assert_eq!(req.uri(), "/api/v1/namespaces/ns/serviceaccounts/sa/token");
734    }
735
736    // TODO: reinstate if we get scoping in trait
737    //#[test]
738    //#[should_panic]
739    //fn all_resources_not_namespaceable() {
740    //    let _r = Request::<corev1::Node>::new(&(), Some("ns"));
741    //}
742
743    #[test]
744    fn list_pods_from_cache() {
745        let url = corev1::Pod::url_path(&(), Some("ns"));
746        let gp = ListParams::default().match_any();
747        let req = Request::new(url).list(&gp).unwrap();
748        assert_eq!(
749            req.uri().query().unwrap(),
750            "&resourceVersion=0&resourceVersionMatch=NotOlderThan"
751        );
752    }
753
754    #[test]
755    fn list_most_recent_pods() {
756        let url = corev1::Pod::url_path(&(), Some("ns"));
757        let gp = ListParams::default();
758        let req = Request::new(url).list(&gp).unwrap();
759        assert_eq!(
760            req.uri().query().unwrap(),
761            "" // No options are required
762        );
763    }
764
765    #[test]
766    fn list_invalid_resource_version_combination() {
767        let url = corev1::Pod::url_path(&(), Some("ns"));
768        let gp = ListParams::default().at("0").matching(VersionMatch::Exact);
769        let err = Request::new(url).list(&gp).unwrap_err();
770        assert!(format!("{err}").contains("non-zero resource_version is required when using an Exact match"));
771    }
772
773    #[test]
774    fn list_paged_any_semantic() {
775        let url = corev1::Pod::url_path(&(), Some("ns"));
776        let gp = ListParams::default().limit(50).match_any();
777        let req = Request::new(url).list(&gp).unwrap();
778        assert_eq!(req.uri().query().unwrap(), "&limit=50");
779    }
780
781    #[test]
782    fn list_paged_with_continue_any_semantic() {
783        let url = corev1::Pod::url_path(&(), Some("ns"));
784        let gp = ListParams::default().limit(50).continue_token("1234").match_any();
785        let req = Request::new(url).list(&gp).unwrap();
786        assert_eq!(req.uri().query().unwrap(), "&limit=50&continue=1234");
787    }
788
789    #[test]
790    fn list_paged_with_continue_starting_at() {
791        let url = corev1::Pod::url_path(&(), Some("ns"));
792        let gp = ListParams::default()
793            .limit(50)
794            .continue_token("1234")
795            .at("9999")
796            .matching(VersionMatch::Exact);
797        let req = Request::new(url).list(&gp).unwrap();
798        assert_eq!(req.uri().query().unwrap(), "&limit=50&continue=1234");
799    }
800
801    #[test]
802    fn list_exact_match() {
803        let url = corev1::Pod::url_path(&(), Some("ns"));
804        let gp = ListParams::default().at("500").matching(VersionMatch::Exact);
805        let req = Request::new(url).list(&gp).unwrap();
806        let query = req.uri().query().unwrap();
807        assert_eq!(query, "&resourceVersion=500&resourceVersionMatch=Exact");
808    }
809
810    #[test]
811    fn watch_params() {
812        let url = corev1::Pod::url_path(&(), Some("ns"));
813        let wp = WatchParams::default()
814            .disable_bookmarks()
815            .fields("metadata.name=pod=1")
816            .labels("app=web");
817        let req = Request::new(url).watch(&wp, "0").unwrap();
818        assert_eq!(
819            req.uri().query().unwrap(),
820            "&watch=true&timeoutSeconds=290&fieldSelector=metadata.name%3Dpod%3D1&labelSelector=app%3Dweb&resourceVersion=0"
821        );
822    }
823
824    #[test]
825    fn watch_timeout_error() {
826        let url = corev1::Pod::url_path(&(), Some("ns"));
827        let wp = WatchParams::default().timeout(100000);
828        let err = Request::new(url).watch(&wp, "").unwrap_err();
829        assert!(format!("{err}").contains("timeout must be < 295s"));
830    }
831}