Skip to main content

kube_core/discovery/
v2.rs

1//! Types for the Aggregated Discovery API (apidiscovery.k8s.io/v2)
2//!
3//! These types are not part of the Kubernetes OpenAPI spec, so they are defined here
4//! rather than in k8s-openapi. They mirror the types from k8s.io/api/apidiscovery/v2.
5//!
6//! The Aggregated Discovery API is available since Kubernetes 1.26 (beta) and stable in 1.30+.
7
8use k8s_openapi::apimachinery::pkg::apis::meta::v1::{ListMeta, ObjectMeta};
9use serde::{Deserialize, Serialize};
10
11/// Content negotiation Accept header for Aggregated Discovery API v2
12pub const ACCEPT_AGGREGATED_DISCOVERY_V2: &str = "application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList,application/json;g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList,application/json";
13
14/// APIGroupDiscoveryList is a resource containing a list of APIGroupDiscovery.
15/// This is one of the types that can be returned from the /api and /apis endpoint
16/// and contains an aggregated list of API resources (built-ins, Custom Resource Definitions, resources from aggregated servers)
17/// that a cluster supports.
18#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
19#[serde(rename_all = "camelCase")]
20pub struct APIGroupDiscoveryList {
21    /// Standard list metadata
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    pub metadata: Option<ListMeta>,
24
25    /// items is the list of groups for discovery.
26    /// The groups are listed in priority order.
27    #[serde(default)]
28    pub items: Vec<APIGroupDiscovery>,
29}
30
31/// APIGroupDiscovery holds information about which resources are being served for all version of the API Group.
32/// It contains a list of APIVersionDiscovery that holds a list of APIResourceDiscovery types served for a version.
33/// Versions are in descending order of preference, with the first version being the preferred entry.
34#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
35#[serde(rename_all = "camelCase")]
36pub struct APIGroupDiscovery {
37    /// Standard object's metadata.
38    /// The only field populated will be name. It will be the name of the API group.
39    #[serde(default, skip_serializing_if = "Option::is_none")]
40    pub metadata: Option<ObjectMeta>,
41
42    /// versions are the versions supported in this group.
43    /// They are sorted in descending order of preference,
44    /// with the preferred version being the first entry.
45    #[serde(default)]
46    pub versions: Vec<APIVersionDiscovery>,
47}
48
49/// APIVersionDiscovery holds a list of APIResourceDiscovery types that are served for a particular version within an API Group.
50#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
51#[serde(rename_all = "camelCase")]
52pub struct APIVersionDiscovery {
53    /// version is the name of the version within a group version.
54    #[serde(default, skip_serializing_if = "Option::is_none")]
55    pub version: Option<String>,
56
57    /// resources is a list of APIResourceDiscovery objects for the corresponding group version.
58    #[serde(default)]
59    pub resources: Vec<APIResourceDiscovery>,
60
61    /// freshness marks whether a group version's discovery document is up to date.
62    /// "Current" indicates the discovery document was recently refreshed.
63    /// "Stale" indicates the discovery document could not be retrieved and
64    /// the returned discovery document may be significantly out of date.
65    /// Clients that require the latest version of the discovery information
66    /// should not use the aggregated document.
67    #[serde(default, skip_serializing_if = "Option::is_none")]
68    pub freshness: Option<String>,
69}
70
71/// APIResourceDiscovery provides information about an API resource for discovery.
72#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
73#[serde(rename_all = "camelCase")]
74pub struct APIResourceDiscovery {
75    /// resource is the plural name of the resource.
76    /// This is used in the URL path and is the unique identifier for this resource across all versions in the API group.
77    #[serde(default, skip_serializing_if = "Option::is_none")]
78    pub resource: Option<String>,
79
80    /// responseKind describes the group, version, and kind of the serialization schema for the object type this endpoint typically returns.
81    #[serde(default, skip_serializing_if = "Option::is_none")]
82    pub response_kind: Option<GroupVersionKind>,
83
84    /// scope indicates the scope of a resource, either "Cluster" or "Namespaced".
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    pub scope: Option<String>,
87
88    /// singularResource is the singular name of the resource.
89    /// This allows clients to handle plural and singular opaquely.
90    #[serde(default, skip_serializing_if = "Option::is_none")]
91    pub singular_resource: Option<String>,
92
93    /// verbs is a list of supported API operation types (this includes but is not limited to get, list, watch, create, update, patch, delete, deletecollection, and proxy).
94    #[serde(default)]
95    pub verbs: Vec<String>,
96
97    /// shortNames is a list of suggested short names of the resource.
98    #[serde(default)]
99    pub short_names: Vec<String>,
100
101    /// categories is a list of the grouped resources this resource belongs to (e.g. 'all').
102    /// Clients may use this to simplify acting on multiple resource types at once.
103    #[serde(default)]
104    pub categories: Vec<String>,
105
106    /// subresources is a list of subresources provided by this resource.
107    /// Subresources are located at /api/v1/namespaces/{namespace}/{resource}/{name}/{subresource}
108    #[serde(default)]
109    pub subresources: Vec<APISubresourceDiscovery>,
110}
111
112/// APISubresourceDiscovery provides information about an API subresource for discovery.
113#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
114#[serde(rename_all = "camelCase")]
115pub struct APISubresourceDiscovery {
116    /// subresource is the name of the subresource.
117    /// This is used in the URL path and is the unique identifier for this resource across all versions.
118    #[serde(default, skip_serializing_if = "Option::is_none")]
119    pub subresource: Option<String>,
120
121    /// responseKind describes the group, version, and kind of the serialization schema for the object type this endpoint typically returns.
122    #[serde(default, skip_serializing_if = "Option::is_none")]
123    pub response_kind: Option<GroupVersionKind>,
124
125    /// acceptedTypes describes the kinds that this endpoint accepts.
126    /// Subresources may accept the parent's kind (for update, patch) or its own kind (for create).
127    #[serde(default)]
128    pub accepted_types: Vec<GroupVersionKind>,
129
130    /// verbs is a list of supported API operation types (this includes but is not limited to get, list, watch, create, update, patch, delete).
131    #[serde(default)]
132    pub verbs: Vec<String>,
133}
134
135/// GroupVersionKind unambiguously identifies a kind.
136/// This is a local copy for use in discovery types.
137#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
138#[serde(rename_all = "camelCase")]
139pub struct GroupVersionKind {
140    /// group is the group of the resource.
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub group: Option<String>,
143
144    /// version is the version of the resource.
145    #[serde(default, skip_serializing_if = "Option::is_none")]
146    pub version: Option<String>,
147
148    /// kind is the kind of the resource.
149    #[serde(default, skip_serializing_if = "Option::is_none")]
150    pub kind: Option<String>,
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn deserialize_api_group_discovery_list() {
159        // Sample response similar to what Kubernetes returns from /apis with aggregated discovery
160        let json = r#"{
161            "kind": "APIGroupDiscoveryList",
162            "apiVersion": "apidiscovery.k8s.io/v2",
163            "metadata": {},
164            "items": [
165                {
166                    "metadata": {
167                        "name": "apps"
168                    },
169                    "versions": [
170                        {
171                            "version": "v1",
172                            "freshness": "Current",
173                            "resources": [
174                                {
175                                    "resource": "deployments",
176                                    "responseKind": {
177                                        "group": "apps",
178                                        "version": "v1",
179                                        "kind": "Deployment"
180                                    },
181                                    "scope": "Namespaced",
182                                    "singularResource": "deployment",
183                                    "verbs": ["create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"],
184                                    "shortNames": ["deploy"],
185                                    "categories": ["all"],
186                                    "subresources": [
187                                        {
188                                            "subresource": "status",
189                                            "responseKind": {
190                                                "group": "apps",
191                                                "version": "v1",
192                                                "kind": "Deployment"
193                                            },
194                                            "verbs": ["get", "patch", "update"]
195                                        },
196                                        {
197                                            "subresource": "scale",
198                                            "responseKind": {
199                                                "group": "autoscaling",
200                                                "version": "v1",
201                                                "kind": "Scale"
202                                            },
203                                            "verbs": ["get", "patch", "update"]
204                                        }
205                                    ]
206                                }
207                            ]
208                        }
209                    ]
210                }
211            ]
212        }"#;
213
214        let result: APIGroupDiscoveryList = serde_json::from_str(json).unwrap();
215
216        assert_eq!(result.items.len(), 1);
217
218        let apps_group = &result.items[0];
219        assert_eq!(
220            apps_group.metadata.as_ref().and_then(|m| m.name.as_ref()),
221            Some(&"apps".to_string())
222        );
223
224        assert_eq!(apps_group.versions.len(), 1);
225        let v1 = &apps_group.versions[0];
226        assert_eq!(v1.version, Some("v1".to_string()));
227        assert_eq!(v1.freshness, Some("Current".to_string()));
228
229        assert_eq!(v1.resources.len(), 1);
230        let deployments = &v1.resources[0];
231        assert_eq!(deployments.resource, Some("deployments".to_string()));
232        assert_eq!(deployments.scope, Some("Namespaced".to_string()));
233        assert_eq!(deployments.singular_resource, Some("deployment".to_string()));
234        assert_eq!(deployments.short_names, vec!["deploy"]);
235        assert_eq!(deployments.categories, vec!["all"]);
236        assert!(deployments.verbs.contains(&"create".to_string()));
237        assert!(deployments.verbs.contains(&"watch".to_string()));
238
239        let response_kind = deployments.response_kind.as_ref().unwrap();
240        assert_eq!(response_kind.group, Some("apps".to_string()));
241        assert_eq!(response_kind.version, Some("v1".to_string()));
242        assert_eq!(response_kind.kind, Some("Deployment".to_string()));
243
244        assert_eq!(deployments.subresources.len(), 2);
245        let status_subresource = &deployments.subresources[0];
246        assert_eq!(status_subresource.subresource, Some("status".to_string()));
247    }
248
249    #[test]
250    fn deserialize_core_api_discovery() {
251        // Sample response from /api with aggregated discovery (core group)
252        let json = r#"{
253            "kind": "APIGroupDiscoveryList",
254            "apiVersion": "apidiscovery.k8s.io/v2",
255            "metadata": {},
256            "items": [
257                {
258                    "metadata": {
259                        "name": ""
260                    },
261                    "versions": [
262                        {
263                            "version": "v1",
264                            "freshness": "Current",
265                            "resources": [
266                                {
267                                    "resource": "pods",
268                                    "responseKind": {
269                                        "group": "",
270                                        "version": "v1",
271                                        "kind": "Pod"
272                                    },
273                                    "scope": "Namespaced",
274                                    "singularResource": "pod",
275                                    "verbs": ["create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"],
276                                    "shortNames": ["po"],
277                                    "categories": ["all"]
278                                },
279                                {
280                                    "resource": "namespaces",
281                                    "responseKind": {
282                                        "group": "",
283                                        "version": "v1",
284                                        "kind": "Namespace"
285                                    },
286                                    "scope": "Cluster",
287                                    "singularResource": "namespace",
288                                    "verbs": ["create", "delete", "get", "list", "patch", "update", "watch"],
289                                    "shortNames": ["ns"]
290                                }
291                            ]
292                        }
293                    ]
294                }
295            ]
296        }"#;
297
298        let result: APIGroupDiscoveryList = serde_json::from_str(json).unwrap();
299
300        assert_eq!(result.items.len(), 1);
301        let core_group = &result.items[0];
302
303        // Core group has empty name
304        assert_eq!(
305            core_group.metadata.as_ref().and_then(|m| m.name.as_ref()),
306            Some(&"".to_string())
307        );
308
309        let v1 = &core_group.versions[0];
310        assert_eq!(v1.resources.len(), 2);
311
312        // Check pods (namespaced)
313        let pods = &v1.resources[0];
314        assert_eq!(pods.resource, Some("pods".to_string()));
315        assert_eq!(pods.scope, Some("Namespaced".to_string()));
316
317        // Check namespaces (cluster-scoped)
318        let namespaces = &v1.resources[1];
319        assert_eq!(namespaces.resource, Some("namespaces".to_string()));
320        assert_eq!(namespaces.scope, Some("Cluster".to_string()));
321    }
322
323    #[test]
324    fn serialize_roundtrip() {
325        let original = APIGroupDiscoveryList {
326            metadata: None,
327            items: vec![APIGroupDiscovery {
328                metadata: Some(ObjectMeta {
329                    name: Some("test".to_string()),
330                    ..Default::default()
331                }),
332                versions: vec![APIVersionDiscovery {
333                    version: Some("v1".to_string()),
334                    freshness: Some("Current".to_string()),
335                    resources: vec![],
336                }],
337            }],
338        };
339
340        let json = serde_json::to_string(&original).unwrap();
341        let deserialized: APIGroupDiscoveryList = serde_json::from_str(&json).unwrap();
342
343        assert_eq!(original, deserialized);
344    }
345}