1use crate::client::{HTTPClient, encode_param};
2use crate::errors::UbiClientError;
3use crate::{make_json_request, make_request};
4use bytes::Bytes;
5use serde::Deserialize;
6use serde::Serialize;
7use std::sync::Arc;
8
9pub struct KCClient {
10 http_client: Arc<HTTPClient>,
11}
12
13#[derive(Debug, Serialize, Deserialize, Default)]
14pub struct QueryParams {
15 start_after: Option<String>,
16 page_size: Option<u32>,
17 order_column: Option<String>,
18}
19
20#[derive(Debug, Serialize, Deserialize, Default)]
21pub struct ReqClusterCreate {
22 pub version: String,
23 pub worker_size: String,
24 pub cp_nodes: u32,
25 pub worker_nodes: u32,
26}
27
28#[derive(Debug, Serialize, Deserialize, Default)]
29pub struct ResponseListKubernetesClusters {
30 pub count: u32,
31 pub items: Vec<ClusterItem>,
32}
33
34#[derive(Debug, Serialize, Deserialize, Default)]
35
36pub struct ClusterItem {
37 pub cp_node_count: u32,
38 pub cp_vms: Option<Vec<VirtualMachine>>,
39 pub display_state: String,
40 pub id: String,
41 pub location: String,
42 pub name: String,
43 pub node_size: String,
44 pub nodepools: Option<Vec<NodePool>>,
45 pub services_load_balancer_url: Option<String>,
46 pub version: String,
47}
48
49#[derive(Debug, Serialize, Deserialize, Default)]
50pub struct NodePool {
51 pub kubernetes_cluster_id: String,
52 pub id: String,
53 pub name: String,
54 pub node_count: u32,
55 pub node_size: String,
56 pub vms: Vec<VirtualMachine>,
57}
58
59#[derive(Debug, Serialize, Deserialize, Default)]
60pub struct VirtualMachine {
61 pub id: String,
62 pub ip4: String,
63 pub ip4_enabled: bool,
64 pub ip6: String,
65 pub location: String,
66 pub name: String,
67 pub size: String,
68 pub state: String,
69 pub storage_size_gib: u32,
70 pub unix_user: String,
71}
72
73impl KCClient {
74 pub fn new(http_client: Arc<HTTPClient>) -> Self {
75 KCClient { http_client }
76 }
77
78 pub async fn list_kubernetes_clusters(
85 &self,
86 project_id: &str,
87 query_params: Option<QueryParams>,
88 ) -> Result<ResponseListKubernetesClusters, UbiClientError> {
89 let url = &format!("project/{}/kubernetes-cluster", project_id);
90
91 let body_empty = serde_json::json!({});
92 make_json_request!(
93 self,
94 reqwest::Method::GET,
95 url,
96 body_empty,
97 query_params,
98 ResponseListKubernetesClusters
99 )
100 }
101
102 pub async fn create_kubernetes_cluster(
113 &self,
114 project_id: &str,
115 location: &str,
116 kubernetes_cluster_reference: &str,
117 payload: &ReqClusterCreate,
118 ) -> Result<ClusterItem, UbiClientError> {
119 let url = &format!(
120 "project/{}/location/{}/kubernetes-cluster/{}",
121 project_id, location, kubernetes_cluster_reference
122 );
123
124 let query_params = [(); 0]; make_json_request!(
126 self,
127 reqwest::Method::POST,
128 url,
129 payload,
130 query_params,
131 ClusterItem
132 )
133 }
134
135 pub async fn delete_kubernetes_cluster(
146 &self,
147 project_id: &str,
148 location: &str,
149 kubernetes_cluster_reference: &str,
150 ) -> Result<(), UbiClientError> {
151 let url = &format!(
152 "project/{}/location/{}/kubernetes-cluster/{}",
153 project_id, location, kubernetes_cluster_reference
154 );
155
156 let _response = make_request!(self, reqwest::Method::DELETE, url)?;
157 Ok(())
158 }
159
160 pub async fn download_kc_config(
168 &self,
169 project_id: &str,
170 location: &str,
171 kubernetes_cluster_reference: &str,
172 ) -> Result<Bytes, UbiClientError> {
173 let url = &format!(
174 "project/{}/location/{}/kubernetes-cluster/{}/kubeconfig",
175 encode_param(project_id),
176 location,
177 kubernetes_cluster_reference
178 );
179
180 let response: reqwest::Response = make_request!(self, reqwest::Method::GET, &url)?;
181 Ok(response.bytes().await?)
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188 use crate::client::HTTPClient;
189 use mockito::{Matcher, Server};
190 use std::sync::Arc;
191
192 fn create_test_client(server_url: &str) -> KCClient {
193 let reqwest_client = reqwest::Client::new();
194 let http_client = HTTPClient::new(server_url, reqwest_client, "v1");
195 KCClient::new(Arc::new(http_client))
196 }
197
198 #[tokio::test]
199 async fn test_list_kubernetes_clusters_success() {
200 let mut server = Server::new_async().await;
201 let mock_response = ResponseListKubernetesClusters {
202 count: 2,
203 items: vec![
204 ClusterItem {
205 cp_node_count: 3,
206 cp_vms: None,
207 display_state: "running".to_string(),
208 id: "cluster-1".to_string(),
209 location: "us-east".to_string(),
210 name: "test-cluster-1".to_string(),
211 node_size: "standard-2".to_string(),
212 nodepools: None,
213 services_load_balancer_url: Some("http://lb.example.com".to_string()),
214 version: "1.28.0".to_string(),
215 },
216 ClusterItem {
217 cp_node_count: 1,
218 cp_vms: None,
219 display_state: "creating".to_string(),
220 id: "cluster-2".to_string(),
221 location: "us-west".to_string(),
222 name: "test-cluster-2".to_string(),
223 node_size: "standard-1".to_string(),
224 nodepools: None,
225 services_load_balancer_url: None,
226 version: "1.27.0".to_string(),
227 },
228 ],
229 };
230
231 let mock = server
232 .mock("GET", "/v1/project/test-project/kubernetes-cluster")
233 .with_status(200)
234 .with_header("content-type", "application/json")
235 .with_body(serde_json::to_string(&mock_response).unwrap())
236 .create_async()
237 .await;
238
239 let client = create_test_client(&server.url());
240 let result = client
241 .list_kubernetes_clusters("test-project", None)
242 .await
243 .unwrap();
244
245 mock.assert_async().await;
246 assert_eq!(result.count, 2);
247 assert_eq!(result.items.len(), 2);
248 assert_eq!(result.items[0].name, "test-cluster-1");
249 assert_eq!(result.items[1].name, "test-cluster-2");
250 }
251
252 #[tokio::test]
253 async fn test_list_kubernetes_clusters_with_query_params() {
254 let mut server = Server::new_async().await;
255 let mock_response = ResponseListKubernetesClusters {
256 count: 1,
257 items: vec![ClusterItem {
258 cp_node_count: 3,
259 cp_vms: None,
260 display_state: "running".to_string(),
261 id: "cluster-1".to_string(),
262 location: "us-east".to_string(),
263 name: "test-cluster-1".to_string(),
264 node_size: "standard-2".to_string(),
265 nodepools: None,
266 services_load_balancer_url: Some("http://lb.example.com".to_string()),
267 version: "1.28.0".to_string(),
268 }],
269 };
270
271 let mock = server
272 .mock("GET", "/v1/project/test-project/kubernetes-cluster")
273 .match_query(Matcher::AllOf(vec![
274 Matcher::UrlEncoded("page_size".to_string(), "10".to_string()),
275 Matcher::UrlEncoded("order_column".to_string(), "name".to_string()),
276 ]))
277 .with_status(200)
278 .with_header("content-type", "application/json")
279 .with_body(serde_json::to_string(&mock_response).unwrap())
280 .create_async()
281 .await;
282
283 let client = create_test_client(&server.url());
284 let query_params = QueryParams {
285 start_after: None,
286 page_size: Some(10),
287 order_column: Some("name".to_string()),
288 };
289
290 let result = client
291 .list_kubernetes_clusters("test-project", Some(query_params))
292 .await
293 .unwrap();
294
295 mock.assert_async().await;
296 assert_eq!(result.count, 1);
297 assert_eq!(result.items.len(), 1);
298 }
299
300 #[tokio::test]
301 async fn test_create_kubernetes_cluster_success() {
302 let mut server = Server::new_async().await;
303 let mock_response = ClusterItem {
304 cp_node_count: 3,
305 cp_vms: None,
306 display_state: "creating".to_string(),
307 id: "new-cluster".to_string(),
308 location: "us-east".to_string(),
309 name: "new-test-cluster".to_string(),
310 node_size: "standard-2".to_string(),
311 nodepools: None,
312 services_load_balancer_url: None,
313 version: "1.28.0".to_string(),
314 };
315
316 let mock = server
317 .mock(
318 "POST",
319 "/v1/project/test-project/location/us-east/kubernetes-cluster/new-test-cluster",
320 )
321 .with_status(201)
322 .with_header("content-type", "application/json")
323 .with_body(serde_json::to_string(&mock_response).unwrap())
324 .create_async()
325 .await;
326
327 let client = create_test_client(&server.url());
328 let payload = ReqClusterCreate {
329 version: "1.28.0".to_string(),
330 worker_size: "standard-2".to_string(),
331 cp_nodes: 3,
332 worker_nodes: 2,
333 };
334
335 let result = client
336 .create_kubernetes_cluster("test-project", "us-east", "new-test-cluster", &payload)
337 .await
338 .unwrap();
339
340 mock.assert_async().await;
341 assert_eq!(result.name, "new-test-cluster");
342 assert_eq!(result.location, "us-east");
343 assert_eq!(result.version, "1.28.0");
344 assert_eq!(result.display_state, "creating");
345 }
346
347 #[tokio::test]
348 async fn test_delete_kubernetes_cluster_success() {
349 let mut server = Server::new_async().await;
350
351 let mock = server
352 .mock(
353 "DELETE",
354 "/v1/project/test-project/location/us-east/kubernetes-cluster/test-cluster",
355 )
356 .with_status(204)
357 .create_async()
358 .await;
359
360 let client = create_test_client(&server.url());
361 let result = client
362 .delete_kubernetes_cluster("test-project", "us-east", "test-cluster")
363 .await;
364
365 mock.assert_async().await;
366 assert!(result.is_ok());
367 }
368
369 #[tokio::test]
370 async fn test_download_kc_config_success() {
371 let mut server = Server::new_async().await;
372 let kubeconfig_content = r#"apiVersion: v1
373kind: Config
374clusters:
375- cluster:
376 server: https://test-cluster.example.com
377 name: test-cluster
378contexts:
379- context:
380 cluster: test-cluster
381 user: test-user
382 name: test-context
383current-context: test-context
384users:
385- name: test-user
386 user:
387 token: test-token"#;
388
389 let mock = server
390 .mock("GET", "/v1/project/test-project/location/us-east/kubernetes-cluster/test-cluster/kubeconfig")
391 .with_status(200)
392 .with_header("content-type", "application/yaml")
393 .with_body(kubeconfig_content)
394 .create_async()
395 .await;
396
397 let client = create_test_client(&server.url());
398 let result = client
399 .download_kc_config("test-project", "us-east", "test-cluster")
400 .await
401 .unwrap();
402
403 mock.assert_async().await;
404 let downloaded_content = String::from_utf8(result.to_vec()).unwrap();
405 assert!(downloaded_content.contains("apiVersion: v1"));
406 assert!(downloaded_content.contains("kind: Config"));
407 assert!(downloaded_content.contains("test-cluster"));
408 }
409
410 #[tokio::test]
411 async fn test_list_kubernetes_clusters_api_error() {
412 let mut server = Server::new_async().await;
413 let error_response = serde_json::json!({
414 "error": {
415 "type": "NotFound",
416 "message": "Project not found",
417 "details": "The specified project does not exist"
418 }
419 });
420
421 let mock = server
422 .mock("GET", "/v1/project/nonexistent-project/kubernetes-cluster")
423 .with_status(404)
424 .with_header("content-type", "application/json")
425 .with_body(error_response.to_string())
426 .create_async()
427 .await;
428
429 let client = create_test_client(&server.url());
430 let result = client
431 .list_kubernetes_clusters("nonexistent-project", None)
432 .await;
433
434 mock.assert_async().await;
435 assert!(result.is_err());
436
437 if let Err(UbiClientError::APIResponseError {
438 etype,
439 message,
440 details,
441 }) = result
442 {
443 assert_eq!(etype, "NotFound");
444 assert_eq!(message, "Project not found");
445 assert_eq!(
446 details,
447 Some("The specified project does not exist".to_string())
448 );
449 } else {
450 panic!("Expected APIResponseError");
451 }
452 }
453
454 #[tokio::test]
455 async fn test_create_kubernetes_cluster_validation_error() {
456 let mut server = Server::new_async().await;
457 let error_response = serde_json::json!({
458 "error": {
459 "type": "ValidationError",
460 "message": "Invalid cluster configuration",
461 "details": "Worker nodes count must be greater than 0"
462 }
463 });
464
465 let mock = server
466 .mock(
467 "POST",
468 "/v1/project/test-project/location/us-east/kubernetes-cluster/invalid-cluster",
469 )
470 .with_status(400)
471 .with_header("content-type", "application/json")
472 .with_body(error_response.to_string())
473 .create_async()
474 .await;
475
476 let client = create_test_client(&server.url());
477 let payload = ReqClusterCreate {
478 version: "1.28.0".to_string(),
479 worker_size: "standard-2".to_string(),
480 cp_nodes: 3,
481 worker_nodes: 0, };
483
484 let result = client
485 .create_kubernetes_cluster("test-project", "us-east", "invalid-cluster", &payload)
486 .await;
487
488 mock.assert_async().await;
489 assert!(result.is_err());
490
491 if let Err(UbiClientError::APIResponseError { etype, message, .. }) = result {
492 assert_eq!(etype, "ValidationError");
493 assert_eq!(message, "Invalid cluster configuration");
494 } else {
495 panic!("Expected APIResponseError");
496 }
497 }
498
499 #[tokio::test]
500 async fn test_delete_kubernetes_cluster_not_found() {
501 let mut server = Server::new_async().await;
502 let error_response = serde_json::json!({
503 "error": {
504 "type": "NotFound",
505 "message": "Kubernetes cluster not found",
506 "details": null
507 }
508 });
509
510 let mock = server
511 .mock(
512 "DELETE",
513 "/v1/project/test-project/location/us-east/kubernetes-cluster/nonexistent-cluster",
514 )
515 .with_status(404)
516 .with_header("content-type", "application/json")
517 .with_body(error_response.to_string())
518 .create_async()
519 .await;
520
521 let client = create_test_client(&server.url());
522 let result = client
523 .delete_kubernetes_cluster("test-project", "us-east", "nonexistent-cluster")
524 .await;
525
526 mock.assert_async().await;
527 assert!(result.is_err());
528
529 if let Err(UbiClientError::APIResponseError { etype, message, .. }) = result {
530 assert_eq!(etype, "NotFound");
531 assert_eq!(message, "Kubernetes cluster not found");
532 } else {
533 panic!("Expected APIResponseError");
534 }
535 }
536
537 #[tokio::test]
538 async fn test_encode_param_in_urls() {
539 let mut server = Server::new_async().await;
540 let mock_response = ResponseListKubernetesClusters {
541 count: 0,
542 items: vec![],
543 };
544
545 let project_id = "test-project";
547
548 let mock = server
549 .mock(
550 "GET",
551 format!("/v1/project/{}/kubernetes-cluster", project_id).as_str(),
552 )
553 .with_status(200)
554 .with_header("content-type", "application/json")
555 .with_body(serde_json::to_string(&mock_response).unwrap())
556 .create_async()
557 .await;
558
559 let client = create_test_client(&server.url());
560 let result = client
561 .list_kubernetes_clusters(project_id, None)
562 .await
563 .unwrap();
564
565 mock.assert_async().await;
566 assert_eq!(result.count, 0);
567 }
568
569 #[tokio::test]
570 async fn test_download_kc_config_with_encoded_params() {
571 let mut server = Server::new_async().await;
572 let kubeconfig_content = "test-config";
573
574 let project_id_with_special_chars = "test-project@domain.com";
576
577 let mock = server
578 .mock("GET", "/v1/project/test%2Dproject%40domain%2Ecom/location/us-east/kubernetes-cluster/test-cluster/kubeconfig")
579 .with_status(200)
580 .with_header("content-type", "application/yaml")
581 .with_body(kubeconfig_content)
582 .create_async()
583 .await;
584
585 let client = create_test_client(&server.url());
586 let result = client
587 .download_kc_config(project_id_with_special_chars, "us-east", "test-cluster")
588 .await
589 .unwrap();
590
591 mock.assert_async().await;
592 let downloaded_content = String::from_utf8(result.to_vec()).unwrap();
593 assert_eq!(downloaded_content, "test-config");
594 }
595
596 #[test]
597 fn test_query_params_default() {
598 let params = QueryParams::default();
599 assert!(params.start_after.is_none());
600 assert!(params.page_size.is_none());
601 assert!(params.order_column.is_none());
602 }
603
604 #[test]
605 fn test_req_cluster_create_default() {
606 let req = ReqClusterCreate::default();
607 assert_eq!(req.version, "");
608 assert_eq!(req.worker_size, "");
609 assert_eq!(req.cp_nodes, 0);
610 assert_eq!(req.worker_nodes, 0);
611 }
612
613 #[test]
614 fn test_cluster_item_serialization() {
615 let cluster = ClusterItem {
616 cp_node_count: 3,
617 cp_vms: None,
618 display_state: "running".to_string(),
619 id: "test-cluster".to_string(),
620 location: "us-east".to_string(),
621 name: "My Test Cluster".to_string(),
622 node_size: "standard-2".to_string(),
623 nodepools: None,
624 services_load_balancer_url: Some("http://lb.example.com".to_string()),
625 version: "1.28.0".to_string(),
626 };
627
628 let serialized = serde_json::to_string(&cluster).unwrap();
629 let deserialized: ClusterItem = serde_json::from_str(&serialized).unwrap();
630
631 assert_eq!(cluster.id, deserialized.id);
632 assert_eq!(cluster.name, deserialized.name);
633 assert_eq!(cluster.location, deserialized.location);
634 assert_eq!(cluster.version, deserialized.version);
635 }
636}