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}