use crate::client::{HTTPClient, encode_param};
use crate::errors::UbiClientError;
use crate::{make_json_request, make_request};
use bytes::Bytes;
use serde::Deserialize;
use serde::Serialize;
use std::sync::Arc;
pub struct KCClient {
http_client: Arc<HTTPClient>,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct QueryParams {
start_after: Option<String>,
page_size: Option<u32>,
order_column: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct ReqClusterCreate {
pub version: String,
pub worker_size: String,
pub cp_nodes: u32,
pub worker_nodes: u32,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct ResponseListKubernetesClusters {
pub count: u32,
pub items: Vec<ClusterItem>,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct ClusterItem {
pub cp_node_count: u32,
pub cp_vms: Option<Vec<VirtualMachine>>,
pub display_state: String,
pub id: String,
pub location: String,
pub name: String,
pub node_size: String,
pub nodepools: Option<Vec<NodePool>>,
pub services_load_balancer_url: Option<String>,
pub version: String,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct NodePool {
pub kubernetes_cluster_id: String,
pub id: String,
pub name: String,
pub node_count: u32,
pub node_size: String,
pub vms: Vec<VirtualMachine>,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct VirtualMachine {
pub id: String,
pub ip4: String,
pub ip4_enabled: bool,
pub ip6: String,
pub location: String,
pub name: String,
pub size: String,
pub state: String,
pub storage_size_gib: u32,
pub unix_user: String,
}
impl KCClient {
pub fn new(http_client: Arc<HTTPClient>) -> Self {
KCClient { http_client }
}
pub async fn list_kubernetes_clusters(
&self,
project_id: &str,
query_params: Option<QueryParams>,
) -> Result<ResponseListKubernetesClusters, UbiClientError> {
let url = &format!("project/{}/kubernetes-cluster", project_id);
let body_empty = serde_json::json!({});
make_json_request!(
self,
reqwest::Method::GET,
url,
body_empty,
query_params,
ResponseListKubernetesClusters
)
}
pub async fn create_kubernetes_cluster(
&self,
project_id: &str,
location: &str,
kubernetes_cluster_reference: &str,
payload: &ReqClusterCreate,
) -> Result<ClusterItem, UbiClientError> {
let url = &format!(
"project/{}/location/{}/kubernetes-cluster/{}",
project_id, location, kubernetes_cluster_reference
);
let query_params = [(); 0]; make_json_request!(
self,
reqwest::Method::POST,
url,
payload,
query_params,
ClusterItem
)
}
pub async fn delete_kubernetes_cluster(
&self,
project_id: &str,
location: &str,
kubernetes_cluster_reference: &str,
) -> Result<(), UbiClientError> {
let url = &format!(
"project/{}/location/{}/kubernetes-cluster/{}",
project_id, location, kubernetes_cluster_reference
);
let _response = make_request!(self, reqwest::Method::DELETE, url)?;
Ok(())
}
pub async fn download_kc_config(
&self,
project_id: &str,
location: &str,
kubernetes_cluster_reference: &str,
) -> Result<Bytes, UbiClientError> {
let url = &format!(
"project/{}/location/{}/kubernetes-cluster/{}/kubeconfig",
encode_param(project_id),
location,
kubernetes_cluster_reference
);
let response: reqwest::Response = make_request!(self, reqwest::Method::GET, &url)?;
Ok(response.bytes().await?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::HTTPClient;
use mockito::{Matcher, Server};
use std::sync::Arc;
fn create_test_client(server_url: &str) -> KCClient {
let reqwest_client = reqwest::Client::new();
let http_client = HTTPClient::new(server_url, reqwest_client, "v1");
KCClient::new(Arc::new(http_client))
}
#[tokio::test]
async fn test_list_kubernetes_clusters_success() {
let mut server = Server::new_async().await;
let mock_response = ResponseListKubernetesClusters {
count: 2,
items: vec![
ClusterItem {
cp_node_count: 3,
cp_vms: None,
display_state: "running".to_string(),
id: "cluster-1".to_string(),
location: "us-east".to_string(),
name: "test-cluster-1".to_string(),
node_size: "standard-2".to_string(),
nodepools: None,
services_load_balancer_url: Some("http://lb.example.com".to_string()),
version: "1.28.0".to_string(),
},
ClusterItem {
cp_node_count: 1,
cp_vms: None,
display_state: "creating".to_string(),
id: "cluster-2".to_string(),
location: "us-west".to_string(),
name: "test-cluster-2".to_string(),
node_size: "standard-1".to_string(),
nodepools: None,
services_load_balancer_url: None,
version: "1.27.0".to_string(),
},
],
};
let mock = server
.mock("GET", "/v1/project/test-project/kubernetes-cluster")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(serde_json::to_string(&mock_response).unwrap())
.create_async()
.await;
let client = create_test_client(&server.url());
let result = client
.list_kubernetes_clusters("test-project", None)
.await
.unwrap();
mock.assert_async().await;
assert_eq!(result.count, 2);
assert_eq!(result.items.len(), 2);
assert_eq!(result.items[0].name, "test-cluster-1");
assert_eq!(result.items[1].name, "test-cluster-2");
}
#[tokio::test]
async fn test_list_kubernetes_clusters_with_query_params() {
let mut server = Server::new_async().await;
let mock_response = ResponseListKubernetesClusters {
count: 1,
items: vec![ClusterItem {
cp_node_count: 3,
cp_vms: None,
display_state: "running".to_string(),
id: "cluster-1".to_string(),
location: "us-east".to_string(),
name: "test-cluster-1".to_string(),
node_size: "standard-2".to_string(),
nodepools: None,
services_load_balancer_url: Some("http://lb.example.com".to_string()),
version: "1.28.0".to_string(),
}],
};
let mock = server
.mock("GET", "/v1/project/test-project/kubernetes-cluster")
.match_query(Matcher::AllOf(vec![
Matcher::UrlEncoded("page_size".to_string(), "10".to_string()),
Matcher::UrlEncoded("order_column".to_string(), "name".to_string()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(serde_json::to_string(&mock_response).unwrap())
.create_async()
.await;
let client = create_test_client(&server.url());
let query_params = QueryParams {
start_after: None,
page_size: Some(10),
order_column: Some("name".to_string()),
};
let result = client
.list_kubernetes_clusters("test-project", Some(query_params))
.await
.unwrap();
mock.assert_async().await;
assert_eq!(result.count, 1);
assert_eq!(result.items.len(), 1);
}
#[tokio::test]
async fn test_create_kubernetes_cluster_success() {
let mut server = Server::new_async().await;
let mock_response = ClusterItem {
cp_node_count: 3,
cp_vms: None,
display_state: "creating".to_string(),
id: "new-cluster".to_string(),
location: "us-east".to_string(),
name: "new-test-cluster".to_string(),
node_size: "standard-2".to_string(),
nodepools: None,
services_load_balancer_url: None,
version: "1.28.0".to_string(),
};
let mock = server
.mock(
"POST",
"/v1/project/test-project/location/us-east/kubernetes-cluster/new-test-cluster",
)
.with_status(201)
.with_header("content-type", "application/json")
.with_body(serde_json::to_string(&mock_response).unwrap())
.create_async()
.await;
let client = create_test_client(&server.url());
let payload = ReqClusterCreate {
version: "1.28.0".to_string(),
worker_size: "standard-2".to_string(),
cp_nodes: 3,
worker_nodes: 2,
};
let result = client
.create_kubernetes_cluster("test-project", "us-east", "new-test-cluster", &payload)
.await
.unwrap();
mock.assert_async().await;
assert_eq!(result.name, "new-test-cluster");
assert_eq!(result.location, "us-east");
assert_eq!(result.version, "1.28.0");
assert_eq!(result.display_state, "creating");
}
#[tokio::test]
async fn test_delete_kubernetes_cluster_success() {
let mut server = Server::new_async().await;
let mock = server
.mock(
"DELETE",
"/v1/project/test-project/location/us-east/kubernetes-cluster/test-cluster",
)
.with_status(204)
.create_async()
.await;
let client = create_test_client(&server.url());
let result = client
.delete_kubernetes_cluster("test-project", "us-east", "test-cluster")
.await;
mock.assert_async().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_download_kc_config_success() {
let mut server = Server::new_async().await;
let kubeconfig_content = r#"apiVersion: v1
kind: Config
clusters:
- cluster:
server: https://test-cluster.example.com
name: test-cluster
contexts:
- context:
cluster: test-cluster
user: test-user
name: test-context
current-context: test-context
users:
- name: test-user
user:
token: test-token"#;
let mock = server
.mock("GET", "/v1/project/test-project/location/us-east/kubernetes-cluster/test-cluster/kubeconfig")
.with_status(200)
.with_header("content-type", "application/yaml")
.with_body(kubeconfig_content)
.create_async()
.await;
let client = create_test_client(&server.url());
let result = client
.download_kc_config("test-project", "us-east", "test-cluster")
.await
.unwrap();
mock.assert_async().await;
let downloaded_content = String::from_utf8(result.to_vec()).unwrap();
assert!(downloaded_content.contains("apiVersion: v1"));
assert!(downloaded_content.contains("kind: Config"));
assert!(downloaded_content.contains("test-cluster"));
}
#[tokio::test]
async fn test_list_kubernetes_clusters_api_error() {
let mut server = Server::new_async().await;
let error_response = serde_json::json!({
"error": {
"type": "NotFound",
"message": "Project not found",
"details": "The specified project does not exist"
}
});
let mock = server
.mock("GET", "/v1/project/nonexistent-project/kubernetes-cluster")
.with_status(404)
.with_header("content-type", "application/json")
.with_body(error_response.to_string())
.create_async()
.await;
let client = create_test_client(&server.url());
let result = client
.list_kubernetes_clusters("nonexistent-project", None)
.await;
mock.assert_async().await;
assert!(result.is_err());
if let Err(UbiClientError::APIResponseError {
etype,
message,
details,
}) = result
{
assert_eq!(etype, "NotFound");
assert_eq!(message, "Project not found");
assert_eq!(
details,
Some("The specified project does not exist".to_string())
);
} else {
panic!("Expected APIResponseError");
}
}
#[tokio::test]
async fn test_create_kubernetes_cluster_validation_error() {
let mut server = Server::new_async().await;
let error_response = serde_json::json!({
"error": {
"type": "ValidationError",
"message": "Invalid cluster configuration",
"details": "Worker nodes count must be greater than 0"
}
});
let mock = server
.mock(
"POST",
"/v1/project/test-project/location/us-east/kubernetes-cluster/invalid-cluster",
)
.with_status(400)
.with_header("content-type", "application/json")
.with_body(error_response.to_string())
.create_async()
.await;
let client = create_test_client(&server.url());
let payload = ReqClusterCreate {
version: "1.28.0".to_string(),
worker_size: "standard-2".to_string(),
cp_nodes: 3,
worker_nodes: 0, };
let result = client
.create_kubernetes_cluster("test-project", "us-east", "invalid-cluster", &payload)
.await;
mock.assert_async().await;
assert!(result.is_err());
if let Err(UbiClientError::APIResponseError { etype, message, .. }) = result {
assert_eq!(etype, "ValidationError");
assert_eq!(message, "Invalid cluster configuration");
} else {
panic!("Expected APIResponseError");
}
}
#[tokio::test]
async fn test_delete_kubernetes_cluster_not_found() {
let mut server = Server::new_async().await;
let error_response = serde_json::json!({
"error": {
"type": "NotFound",
"message": "Kubernetes cluster not found",
"details": null
}
});
let mock = server
.mock(
"DELETE",
"/v1/project/test-project/location/us-east/kubernetes-cluster/nonexistent-cluster",
)
.with_status(404)
.with_header("content-type", "application/json")
.with_body(error_response.to_string())
.create_async()
.await;
let client = create_test_client(&server.url());
let result = client
.delete_kubernetes_cluster("test-project", "us-east", "nonexistent-cluster")
.await;
mock.assert_async().await;
assert!(result.is_err());
if let Err(UbiClientError::APIResponseError { etype, message, .. }) = result {
assert_eq!(etype, "NotFound");
assert_eq!(message, "Kubernetes cluster not found");
} else {
panic!("Expected APIResponseError");
}
}
#[tokio::test]
async fn test_encode_param_in_urls() {
let mut server = Server::new_async().await;
let mock_response = ResponseListKubernetesClusters {
count: 0,
items: vec![],
};
let project_id = "test-project";
let mock = server
.mock(
"GET",
format!("/v1/project/{}/kubernetes-cluster", project_id).as_str(),
)
.with_status(200)
.with_header("content-type", "application/json")
.with_body(serde_json::to_string(&mock_response).unwrap())
.create_async()
.await;
let client = create_test_client(&server.url());
let result = client
.list_kubernetes_clusters(project_id, None)
.await
.unwrap();
mock.assert_async().await;
assert_eq!(result.count, 0);
}
#[tokio::test]
async fn test_download_kc_config_with_encoded_params() {
let mut server = Server::new_async().await;
let kubeconfig_content = "test-config";
let project_id_with_special_chars = "test-project@domain.com";
let mock = server
.mock("GET", "/v1/project/test%2Dproject%40domain%2Ecom/location/us-east/kubernetes-cluster/test-cluster/kubeconfig")
.with_status(200)
.with_header("content-type", "application/yaml")
.with_body(kubeconfig_content)
.create_async()
.await;
let client = create_test_client(&server.url());
let result = client
.download_kc_config(project_id_with_special_chars, "us-east", "test-cluster")
.await
.unwrap();
mock.assert_async().await;
let downloaded_content = String::from_utf8(result.to_vec()).unwrap();
assert_eq!(downloaded_content, "test-config");
}
#[test]
fn test_query_params_default() {
let params = QueryParams::default();
assert!(params.start_after.is_none());
assert!(params.page_size.is_none());
assert!(params.order_column.is_none());
}
#[test]
fn test_req_cluster_create_default() {
let req = ReqClusterCreate::default();
assert_eq!(req.version, "");
assert_eq!(req.worker_size, "");
assert_eq!(req.cp_nodes, 0);
assert_eq!(req.worker_nodes, 0);
}
#[test]
fn test_cluster_item_serialization() {
let cluster = ClusterItem {
cp_node_count: 3,
cp_vms: None,
display_state: "running".to_string(),
id: "test-cluster".to_string(),
location: "us-east".to_string(),
name: "My Test Cluster".to_string(),
node_size: "standard-2".to_string(),
nodepools: None,
services_load_balancer_url: Some("http://lb.example.com".to_string()),
version: "1.28.0".to_string(),
};
let serialized = serde_json::to_string(&cluster).unwrap();
let deserialized: ClusterItem = serde_json::from_str(&serialized).unwrap();
assert_eq!(cluster.id, deserialized.id);
assert_eq!(cluster.name, deserialized.name);
assert_eq!(cluster.location, deserialized.location);
assert_eq!(cluster.version, deserialized.version);
}
}