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}