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