Implement k8shost deployment REST API
This commit is contained in:
parent
2b7c3166d2
commit
23ec8b5edb
4 changed files with 693 additions and 96 deletions
|
|
@ -45,6 +45,10 @@ struct Args {
|
|||
#[arg(long)]
|
||||
addr: Option<String>,
|
||||
|
||||
/// Listen address for HTTP REST server (e.g., "127.0.0.1:8085")
|
||||
#[arg(long)]
|
||||
http_addr: Option<String>,
|
||||
|
||||
/// Log level (e.g., "info", "debug", "trace")
|
||||
#[arg(long)]
|
||||
log_level: Option<String>,
|
||||
|
|
@ -112,7 +116,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
.addr
|
||||
.map(|s| s.parse().unwrap_or(loaded_config.server.addr))
|
||||
.unwrap_or(loaded_config.server.addr),
|
||||
http_addr: loaded_config.server.http_addr,
|
||||
http_addr: args
|
||||
.http_addr
|
||||
.map(|s| s.parse().unwrap_or(loaded_config.server.http_addr))
|
||||
.unwrap_or(loaded_config.server.http_addr),
|
||||
log_level: args.log_level.unwrap_or(loaded_config.server.log_level),
|
||||
},
|
||||
flaredb: config::FlareDbConfig {
|
||||
|
|
@ -277,7 +284,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
auth_service.clone(),
|
||||
));
|
||||
let node_service = Arc::new(NodeServiceImpl::new(storage.clone(), auth_service.clone()));
|
||||
let deployment_service = DeploymentServiceImpl::new(storage.clone(), auth_service.clone());
|
||||
let deployment_service = Arc::new(DeploymentServiceImpl::new(
|
||||
storage.clone(),
|
||||
auth_service.clone(),
|
||||
));
|
||||
|
||||
// Start scheduler in background with CreditService integration
|
||||
let scheduler = Arc::new(scheduler::Scheduler::new_with_credit_service(storage.clone()).await);
|
||||
|
|
@ -335,7 +345,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
make_interceptor(auth_service.clone()),
|
||||
))
|
||||
.add_service(tonic::codegen::InterceptedService::new(
|
||||
DeploymentServiceServer::new(deployment_service),
|
||||
DeploymentServiceServer::new(deployment_service.as_ref().clone()),
|
||||
make_interceptor(auth_service.clone()),
|
||||
))
|
||||
.serve(config.server.addr);
|
||||
|
|
@ -343,6 +353,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
// HTTP REST API server
|
||||
let http_addr = config.server.http_addr;
|
||||
let rest_state = rest::RestApiState {
|
||||
deployment_service: deployment_service.clone(),
|
||||
pod_service: pod_service.clone(),
|
||||
service_service: service_service.clone(),
|
||||
node_service: node_service.clone(),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
//! - DELETE /api/v1/pods/{namespace}/{name} - Delete pod
|
||||
//! - GET /api/v1/services - List services
|
||||
//! - POST /api/v1/services - Create service
|
||||
//! - GET /api/v1/deployments - List deployments
|
||||
//! - POST /api/v1/deployments - Create deployment
|
||||
//! - GET /api/v1/deployments/{namespace}/{name} - Get deployment
|
||||
//! - PUT /api/v1/deployments/{namespace}/{name} - Update deployment
|
||||
//! - DELETE /api/v1/deployments/{namespace}/{name} - Delete deployment
|
||||
//! - GET /api/v1/nodes - List nodes
|
||||
//! - GET /health - Health check
|
||||
|
||||
|
|
@ -18,21 +23,29 @@ use axum::{
|
|||
};
|
||||
use iam_service_auth::{resolve_tenant_ids_from_context, AuthService, TenantContext};
|
||||
use k8shost_proto::{
|
||||
node_service_server::NodeService, pod_service_server::PodService,
|
||||
service_service_server::ServiceService, Container, CreatePodRequest, CreateServiceRequest,
|
||||
DeletePodRequest, DeleteServiceRequest, ListNodesRequest, ListPodsRequest, ListServicesRequest,
|
||||
Node as ProtoNode, ObjectMeta, Pod as ProtoPod, PodSpec, Service as ProtoService, ServicePort,
|
||||
ServiceSpec,
|
||||
deployment_service_server::DeploymentService, node_service_server::NodeService,
|
||||
pod_service_server::PodService, service_service_server::ServiceService, Container,
|
||||
ContainerPort, CreateDeploymentRequest, CreatePodRequest, CreateServiceRequest,
|
||||
DeleteDeploymentRequest, DeletePodRequest, DeleteServiceRequest, Deployment as ProtoDeployment,
|
||||
DeploymentSpec, EnvVar, GetDeploymentRequest, LabelSelector, ListDeploymentsRequest,
|
||||
ListNodesRequest, ListPodsRequest, ListServicesRequest, Node as ProtoNode, ObjectMeta,
|
||||
Pod as ProtoPod, PodSpec, PodTemplateSpec, Service as ProtoService, ServicePort, ServiceSpec,
|
||||
UpdateDeploymentRequest,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tonic::{Code, Request};
|
||||
|
||||
use crate::services::{node::NodeServiceImpl, pod::PodServiceImpl, service::ServiceServiceImpl};
|
||||
use crate::services::{
|
||||
deployment::DeploymentServiceImpl, node::NodeServiceImpl, pod::PodServiceImpl,
|
||||
service::ServiceServiceImpl,
|
||||
};
|
||||
|
||||
/// REST API state
|
||||
#[derive(Clone)]
|
||||
pub struct RestApiState {
|
||||
pub deployment_service: Arc<DeploymentServiceImpl>,
|
||||
pub pod_service: Arc<PodServiceImpl>,
|
||||
pub service_service: Arc<ServiceServiceImpl>,
|
||||
pub node_service: Arc<NodeServiceImpl>,
|
||||
|
|
@ -103,7 +116,50 @@ pub struct CreateServiceRequestRest {
|
|||
pub service_type: Option<String>,
|
||||
pub port: i32,
|
||||
pub target_port: Option<i32>,
|
||||
pub selector: Option<std::collections::HashMap<String, String>>,
|
||||
pub selector: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
/// Deployment creation request
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CreateDeploymentRequestRest {
|
||||
pub name: String,
|
||||
pub namespace: Option<String>,
|
||||
pub replicas: Option<i32>,
|
||||
pub selector: HashMap<String, String>,
|
||||
pub template_labels: Option<HashMap<String, String>>,
|
||||
pub containers: Vec<DeploymentContainerRequestRest>,
|
||||
}
|
||||
|
||||
/// Deployment update request
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UpdateDeploymentRequestRest {
|
||||
pub replicas: Option<i32>,
|
||||
pub selector: Option<HashMap<String, String>>,
|
||||
pub template_labels: Option<HashMap<String, String>>,
|
||||
pub containers: Option<Vec<DeploymentContainerRequestRest>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct DeploymentContainerRequestRest {
|
||||
pub name: String,
|
||||
pub image: String,
|
||||
pub command: Option<Vec<String>>,
|
||||
pub args: Option<Vec<String>>,
|
||||
pub ports: Option<Vec<DeploymentContainerPortRequestRest>>,
|
||||
pub env: Option<Vec<DeploymentEnvVarRequestRest>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct DeploymentContainerPortRequestRest {
|
||||
pub name: Option<String>,
|
||||
pub container_port: i32,
|
||||
pub protocol: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct DeploymentEnvVarRequestRest {
|
||||
pub name: String,
|
||||
pub value: Option<String>,
|
||||
}
|
||||
|
||||
/// Query params for list operations
|
||||
|
|
@ -273,12 +329,169 @@ pub struct NodesResponse {
|
|||
pub nodes: Vec<NodeResponse>,
|
||||
}
|
||||
|
||||
/// Deployment response
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct DeploymentResponse {
|
||||
pub name: String,
|
||||
pub namespace: String,
|
||||
pub replicas: i32,
|
||||
pub ready_replicas: i32,
|
||||
pub available_replicas: i32,
|
||||
pub selector: HashMap<String, String>,
|
||||
pub template_labels: HashMap<String, String>,
|
||||
pub containers: Vec<DeploymentContainerResponse>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct DeploymentContainerResponse {
|
||||
pub name: String,
|
||||
pub image: String,
|
||||
pub command: Vec<String>,
|
||||
pub args: Vec<String>,
|
||||
pub ports: Vec<DeploymentContainerPortResponse>,
|
||||
pub env: Vec<DeploymentEnvVarResponse>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct DeploymentContainerPortResponse {
|
||||
pub name: Option<String>,
|
||||
pub container_port: i32,
|
||||
pub protocol: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct DeploymentEnvVarResponse {
|
||||
pub name: String,
|
||||
pub value: Option<String>,
|
||||
}
|
||||
|
||||
impl From<ProtoDeployment> for DeploymentResponse {
|
||||
fn from(deployment: ProtoDeployment) -> Self {
|
||||
let metadata = deployment.metadata.unwrap_or(ObjectMeta {
|
||||
name: String::new(),
|
||||
namespace: None,
|
||||
uid: None,
|
||||
resource_version: None,
|
||||
creation_timestamp: None,
|
||||
labels: HashMap::new(),
|
||||
annotations: HashMap::new(),
|
||||
org_id: None,
|
||||
project_id: None,
|
||||
});
|
||||
let DeploymentSpec {
|
||||
replicas,
|
||||
selector,
|
||||
template,
|
||||
} = deployment.spec.unwrap_or(DeploymentSpec {
|
||||
replicas: None,
|
||||
selector: None,
|
||||
template: None,
|
||||
});
|
||||
let template = template.unwrap_or(PodTemplateSpec {
|
||||
metadata: Some(ObjectMeta {
|
||||
name: String::new(),
|
||||
namespace: None,
|
||||
uid: None,
|
||||
resource_version: None,
|
||||
creation_timestamp: None,
|
||||
labels: HashMap::new(),
|
||||
annotations: HashMap::new(),
|
||||
org_id: None,
|
||||
project_id: None,
|
||||
}),
|
||||
spec: Some(PodSpec {
|
||||
containers: Vec::new(),
|
||||
restart_policy: None,
|
||||
node_name: None,
|
||||
}),
|
||||
});
|
||||
let template_metadata = template.metadata.unwrap_or(ObjectMeta {
|
||||
name: String::new(),
|
||||
namespace: None,
|
||||
uid: None,
|
||||
resource_version: None,
|
||||
creation_timestamp: None,
|
||||
labels: HashMap::new(),
|
||||
annotations: HashMap::new(),
|
||||
org_id: None,
|
||||
project_id: None,
|
||||
});
|
||||
let template_spec = template.spec.unwrap_or(PodSpec {
|
||||
containers: Vec::new(),
|
||||
restart_policy: None,
|
||||
node_name: None,
|
||||
});
|
||||
let status = deployment.status;
|
||||
|
||||
Self {
|
||||
name: metadata.name,
|
||||
namespace: metadata.namespace.unwrap_or_else(|| "default".to_string()),
|
||||
replicas: replicas.unwrap_or(1),
|
||||
ready_replicas: status
|
||||
.as_ref()
|
||||
.and_then(|status| status.ready_replicas)
|
||||
.unwrap_or(0),
|
||||
available_replicas: status
|
||||
.as_ref()
|
||||
.and_then(|status| status.available_replicas)
|
||||
.unwrap_or(0),
|
||||
selector: selector
|
||||
.map(|selector| selector.match_labels)
|
||||
.unwrap_or_default(),
|
||||
template_labels: template_metadata.labels,
|
||||
containers: template_spec
|
||||
.containers
|
||||
.into_iter()
|
||||
.map(|container| DeploymentContainerResponse {
|
||||
name: container.name,
|
||||
image: container.image,
|
||||
command: container.command,
|
||||
args: container.args,
|
||||
ports: container
|
||||
.ports
|
||||
.into_iter()
|
||||
.map(|port| DeploymentContainerPortResponse {
|
||||
name: port.name,
|
||||
container_port: port.container_port,
|
||||
protocol: port.protocol,
|
||||
})
|
||||
.collect(),
|
||||
env: container
|
||||
.env
|
||||
.into_iter()
|
||||
.map(|env| DeploymentEnvVarResponse {
|
||||
name: env.name,
|
||||
value: env.value,
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Deployments list response
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct DeploymentsResponse {
|
||||
pub deployments: Vec<DeploymentResponse>,
|
||||
}
|
||||
|
||||
/// Build the REST API router
|
||||
pub fn build_router(state: RestApiState) -> Router {
|
||||
Router::new()
|
||||
.route("/api/v1/pods", get(list_pods).post(create_pod))
|
||||
.route("/api/v1/pods/{namespace}/{name}", delete(delete_pod))
|
||||
.route("/api/v1/services", get(list_services).post(create_service))
|
||||
.route(
|
||||
"/api/v1/deployments",
|
||||
get(list_deployments).post(create_deployment),
|
||||
)
|
||||
.route(
|
||||
"/api/v1/deployments/{namespace}/{name}",
|
||||
get(get_deployment)
|
||||
.put(update_deployment)
|
||||
.delete(delete_deployment),
|
||||
)
|
||||
.route(
|
||||
"/api/v1/services/{namespace}/{name}",
|
||||
delete(delete_service),
|
||||
|
|
@ -311,13 +524,11 @@ async fn list_pods(
|
|||
});
|
||||
req.extensions_mut().insert(tenant);
|
||||
|
||||
let response = state.pod_service.list_pods(req).await.map_err(|e| {
|
||||
error_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"LIST_FAILED",
|
||||
&e.message(),
|
||||
)
|
||||
})?;
|
||||
let response = state
|
||||
.pod_service
|
||||
.list_pods(req)
|
||||
.await
|
||||
.map_err(map_tonic_status)?;
|
||||
|
||||
let pods: Vec<PodResponse> = response
|
||||
.into_inner()
|
||||
|
|
@ -368,13 +579,11 @@ async fn create_pod(
|
|||
});
|
||||
grpc_req.extensions_mut().insert(tenant);
|
||||
|
||||
let response = state.pod_service.create_pod(grpc_req).await.map_err(|e| {
|
||||
error_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"CREATE_FAILED",
|
||||
&e.message(),
|
||||
)
|
||||
})?;
|
||||
let response = state
|
||||
.pod_service
|
||||
.create_pod(grpc_req)
|
||||
.await
|
||||
.map_err(map_tonic_status)?;
|
||||
|
||||
let pod = response.into_inner().pod.ok_or_else(|| {
|
||||
error_response(
|
||||
|
|
@ -404,13 +613,11 @@ async fn delete_pod(
|
|||
});
|
||||
req.extensions_mut().insert(tenant);
|
||||
|
||||
state.pod_service.delete_pod(req).await.map_err(|e| {
|
||||
error_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"DELETE_FAILED",
|
||||
&e.message(),
|
||||
)
|
||||
})?;
|
||||
state
|
||||
.pod_service
|
||||
.delete_pod(req)
|
||||
.await
|
||||
.map_err(map_tonic_status)?;
|
||||
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
|
|
@ -436,13 +643,7 @@ async fn list_services(
|
|||
.service_service
|
||||
.list_services(req)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"LIST_FAILED",
|
||||
&e.message(),
|
||||
)
|
||||
})?;
|
||||
.map_err(map_tonic_status)?;
|
||||
|
||||
let services: Vec<ServiceResponse> = response
|
||||
.into_inner()
|
||||
|
|
@ -498,13 +699,7 @@ async fn create_service(
|
|||
.service_service
|
||||
.create_service(grpc_req)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"CREATE_FAILED",
|
||||
&e.message(),
|
||||
)
|
||||
})?;
|
||||
.map_err(map_tonic_status)?;
|
||||
|
||||
let service = response.into_inner().service.ok_or_else(|| {
|
||||
error_response(
|
||||
|
|
@ -538,13 +733,7 @@ async fn delete_service(
|
|||
.service_service
|
||||
.delete_service(req)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"DELETE_FAILED",
|
||||
&e.message(),
|
||||
)
|
||||
})?;
|
||||
.map_err(map_tonic_status)?;
|
||||
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
|
|
@ -563,13 +752,11 @@ async fn list_nodes(
|
|||
let mut req = Request::new(ListNodesRequest {});
|
||||
req.extensions_mut().insert(tenant);
|
||||
|
||||
let response = state.node_service.list_nodes(req).await.map_err(|e| {
|
||||
error_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"LIST_FAILED",
|
||||
&e.message(),
|
||||
)
|
||||
})?;
|
||||
let response = state
|
||||
.node_service
|
||||
.list_nodes(req)
|
||||
.await
|
||||
.map_err(map_tonic_status)?;
|
||||
|
||||
let nodes: Vec<NodeResponse> = response
|
||||
.into_inner()
|
||||
|
|
@ -581,6 +768,393 @@ async fn list_nodes(
|
|||
Ok(Json(SuccessResponse::new(NodesResponse { nodes })))
|
||||
}
|
||||
|
||||
/// GET /api/v1/deployments - List deployments
|
||||
async fn list_deployments(
|
||||
State(state): State<RestApiState>,
|
||||
Query(params): Query<ListParams>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Json<SuccessResponse<DeploymentsResponse>>, (StatusCode, Json<ErrorResponse>)> {
|
||||
let tenant = resolve_rest_tenant(&state, &headers).await?;
|
||||
let mut req = Request::new(ListDeploymentsRequest {
|
||||
namespace: params.namespace,
|
||||
});
|
||||
req.extensions_mut().insert(tenant);
|
||||
|
||||
let response = state
|
||||
.deployment_service
|
||||
.list_deployments(req)
|
||||
.await
|
||||
.map_err(map_tonic_status)?;
|
||||
|
||||
let deployments = response
|
||||
.into_inner()
|
||||
.items
|
||||
.into_iter()
|
||||
.map(DeploymentResponse::from)
|
||||
.collect();
|
||||
|
||||
Ok(Json(SuccessResponse::new(DeploymentsResponse {
|
||||
deployments,
|
||||
})))
|
||||
}
|
||||
|
||||
/// GET /api/v1/deployments/{namespace}/{name} - Get deployment
|
||||
async fn get_deployment(
|
||||
State(state): State<RestApiState>,
|
||||
Path((namespace, name)): Path<(String, String)>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Json<SuccessResponse<DeploymentResponse>>, (StatusCode, Json<ErrorResponse>)> {
|
||||
let tenant = resolve_rest_tenant(&state, &headers).await?;
|
||||
let mut req = Request::new(GetDeploymentRequest { namespace, name });
|
||||
req.extensions_mut().insert(tenant);
|
||||
|
||||
let response = state
|
||||
.deployment_service
|
||||
.get_deployment(req)
|
||||
.await
|
||||
.map_err(map_tonic_status)?;
|
||||
let deployment = response.into_inner().deployment.ok_or_else(|| {
|
||||
error_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"INTERNAL",
|
||||
"No deployment returned",
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(Json(SuccessResponse::new(DeploymentResponse::from(
|
||||
deployment,
|
||||
))))
|
||||
}
|
||||
|
||||
/// POST /api/v1/deployments - Create deployment
|
||||
async fn create_deployment(
|
||||
State(state): State<RestApiState>,
|
||||
headers: HeaderMap,
|
||||
Json(req): Json<CreateDeploymentRequestRest>,
|
||||
) -> Result<
|
||||
(StatusCode, Json<SuccessResponse<DeploymentResponse>>),
|
||||
(StatusCode, Json<ErrorResponse>),
|
||||
> {
|
||||
let tenant = resolve_rest_tenant(&state, &headers).await?;
|
||||
let mut grpc_req = Request::new(CreateDeploymentRequest {
|
||||
deployment: Some(build_proto_deployment(
|
||||
req.name,
|
||||
req.namespace,
|
||||
req.replicas,
|
||||
req.selector,
|
||||
req.template_labels,
|
||||
req.containers,
|
||||
)),
|
||||
});
|
||||
grpc_req.extensions_mut().insert(tenant);
|
||||
|
||||
let response = state
|
||||
.deployment_service
|
||||
.create_deployment(grpc_req)
|
||||
.await
|
||||
.map_err(map_tonic_status)?;
|
||||
let deployment = response.into_inner().deployment.ok_or_else(|| {
|
||||
error_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"INTERNAL",
|
||||
"No deployment returned",
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
Json(SuccessResponse::new(DeploymentResponse::from(deployment))),
|
||||
))
|
||||
}
|
||||
|
||||
/// PUT /api/v1/deployments/{namespace}/{name} - Update deployment
|
||||
async fn update_deployment(
|
||||
State(state): State<RestApiState>,
|
||||
Path((namespace, name)): Path<(String, String)>,
|
||||
headers: HeaderMap,
|
||||
Json(req): Json<UpdateDeploymentRequestRest>,
|
||||
) -> Result<Json<SuccessResponse<DeploymentResponse>>, (StatusCode, Json<ErrorResponse>)> {
|
||||
let tenant = resolve_rest_tenant(&state, &headers).await?;
|
||||
|
||||
let mut get_req = Request::new(GetDeploymentRequest {
|
||||
namespace: namespace.clone(),
|
||||
name: name.clone(),
|
||||
});
|
||||
get_req.extensions_mut().insert(tenant.clone());
|
||||
let existing = state
|
||||
.deployment_service
|
||||
.get_deployment(get_req)
|
||||
.await
|
||||
.map_err(map_tonic_status)?
|
||||
.into_inner()
|
||||
.deployment
|
||||
.ok_or_else(|| {
|
||||
error_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"INTERNAL",
|
||||
"No deployment returned",
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut deployment = existing;
|
||||
let spec = deployment.spec.get_or_insert(DeploymentSpec {
|
||||
replicas: None,
|
||||
selector: None,
|
||||
template: None,
|
||||
});
|
||||
|
||||
if let Some(replicas) = req.replicas {
|
||||
spec.replicas = Some(replicas);
|
||||
}
|
||||
if let Some(selector) = req.selector {
|
||||
spec.selector = Some(LabelSelector {
|
||||
match_labels: selector,
|
||||
});
|
||||
}
|
||||
|
||||
let template = spec.template.get_or_insert(PodTemplateSpec {
|
||||
metadata: Some(ObjectMeta {
|
||||
name: String::new(),
|
||||
namespace: Some(namespace.clone()),
|
||||
uid: None,
|
||||
resource_version: None,
|
||||
creation_timestamp: None,
|
||||
labels: HashMap::new(),
|
||||
annotations: HashMap::new(),
|
||||
org_id: None,
|
||||
project_id: None,
|
||||
}),
|
||||
spec: Some(PodSpec {
|
||||
containers: Vec::new(),
|
||||
restart_policy: None,
|
||||
node_name: None,
|
||||
}),
|
||||
});
|
||||
|
||||
if let Some(template_labels) = req.template_labels {
|
||||
template
|
||||
.metadata
|
||||
.get_or_insert_with(|| ObjectMeta {
|
||||
name: String::new(),
|
||||
namespace: Some(namespace.clone()),
|
||||
uid: None,
|
||||
resource_version: None,
|
||||
creation_timestamp: None,
|
||||
labels: HashMap::new(),
|
||||
annotations: HashMap::new(),
|
||||
org_id: None,
|
||||
project_id: None,
|
||||
})
|
||||
.labels = template_labels;
|
||||
}
|
||||
if let Some(containers) = req.containers {
|
||||
template
|
||||
.spec
|
||||
.get_or_insert(PodSpec {
|
||||
containers: Vec::new(),
|
||||
restart_policy: None,
|
||||
node_name: None,
|
||||
})
|
||||
.containers = containers
|
||||
.into_iter()
|
||||
.map(proto_container_from_rest)
|
||||
.collect();
|
||||
}
|
||||
|
||||
ensure_selector_labels_on_template(spec);
|
||||
|
||||
let metadata = deployment.metadata.get_or_insert(ObjectMeta {
|
||||
name: name.clone(),
|
||||
namespace: Some(namespace.clone()),
|
||||
uid: None,
|
||||
resource_version: None,
|
||||
creation_timestamp: None,
|
||||
labels: HashMap::new(),
|
||||
annotations: HashMap::new(),
|
||||
org_id: None,
|
||||
project_id: None,
|
||||
});
|
||||
metadata.name = name;
|
||||
metadata.namespace = Some(namespace);
|
||||
|
||||
let mut update_req = Request::new(UpdateDeploymentRequest {
|
||||
deployment: Some(deployment),
|
||||
});
|
||||
update_req.extensions_mut().insert(tenant);
|
||||
|
||||
let response = state
|
||||
.deployment_service
|
||||
.update_deployment(update_req)
|
||||
.await
|
||||
.map_err(map_tonic_status)?;
|
||||
let deployment = response.into_inner().deployment.ok_or_else(|| {
|
||||
error_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"INTERNAL",
|
||||
"No deployment returned",
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(Json(SuccessResponse::new(DeploymentResponse::from(
|
||||
deployment,
|
||||
))))
|
||||
}
|
||||
|
||||
/// DELETE /api/v1/deployments/{namespace}/{name} - Delete deployment
|
||||
async fn delete_deployment(
|
||||
State(state): State<RestApiState>,
|
||||
Path((namespace, name)): Path<(String, String)>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<(StatusCode, Json<SuccessResponse<serde_json::Value>>), (StatusCode, Json<ErrorResponse>)>
|
||||
{
|
||||
let tenant = resolve_rest_tenant(&state, &headers).await?;
|
||||
let mut req = Request::new(DeleteDeploymentRequest {
|
||||
namespace: namespace.clone(),
|
||||
name: name.clone(),
|
||||
});
|
||||
req.extensions_mut().insert(tenant);
|
||||
|
||||
state
|
||||
.deployment_service
|
||||
.delete_deployment(req)
|
||||
.await
|
||||
.map_err(map_tonic_status)?;
|
||||
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
Json(SuccessResponse::new(serde_json::json!({
|
||||
"name": name,
|
||||
"namespace": namespace,
|
||||
"deleted": true
|
||||
}))),
|
||||
))
|
||||
}
|
||||
|
||||
fn build_proto_deployment(
|
||||
name: String,
|
||||
namespace: Option<String>,
|
||||
replicas: Option<i32>,
|
||||
selector: HashMap<String, String>,
|
||||
template_labels: Option<HashMap<String, String>>,
|
||||
containers: Vec<DeploymentContainerRequestRest>,
|
||||
) -> ProtoDeployment {
|
||||
let namespace = namespace.unwrap_or_else(|| "default".to_string());
|
||||
let mut labels = template_labels.unwrap_or_else(|| selector.clone());
|
||||
for (key, value) in &selector {
|
||||
labels.entry(key.clone()).or_insert_with(|| value.clone());
|
||||
}
|
||||
|
||||
ProtoDeployment {
|
||||
metadata: Some(ObjectMeta {
|
||||
name,
|
||||
namespace: Some(namespace.clone()),
|
||||
uid: None,
|
||||
resource_version: None,
|
||||
creation_timestamp: None,
|
||||
labels: HashMap::new(),
|
||||
annotations: HashMap::new(),
|
||||
org_id: None,
|
||||
project_id: None,
|
||||
}),
|
||||
spec: Some(DeploymentSpec {
|
||||
replicas,
|
||||
selector: Some(LabelSelector {
|
||||
match_labels: selector,
|
||||
}),
|
||||
template: Some(PodTemplateSpec {
|
||||
metadata: Some(ObjectMeta {
|
||||
name: String::new(),
|
||||
namespace: Some(namespace),
|
||||
uid: None,
|
||||
resource_version: None,
|
||||
creation_timestamp: None,
|
||||
labels,
|
||||
annotations: HashMap::new(),
|
||||
org_id: None,
|
||||
project_id: None,
|
||||
}),
|
||||
spec: Some(PodSpec {
|
||||
containers: containers
|
||||
.into_iter()
|
||||
.map(proto_container_from_rest)
|
||||
.collect(),
|
||||
restart_policy: Some("Always".to_string()),
|
||||
node_name: None,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
status: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn proto_container_from_rest(container: DeploymentContainerRequestRest) -> Container {
|
||||
Container {
|
||||
name: container.name,
|
||||
image: container.image,
|
||||
command: container.command.unwrap_or_default(),
|
||||
args: container.args.unwrap_or_default(),
|
||||
ports: container
|
||||
.ports
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|port| ContainerPort {
|
||||
name: port.name,
|
||||
container_port: port.container_port,
|
||||
protocol: port.protocol,
|
||||
})
|
||||
.collect(),
|
||||
env: container
|
||||
.env
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|env| EnvVar {
|
||||
name: env.name,
|
||||
value: env.value,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_selector_labels_on_template(spec: &mut DeploymentSpec) {
|
||||
let selector = spec
|
||||
.selector
|
||||
.as_ref()
|
||||
.map(|selector| selector.match_labels.clone())
|
||||
.unwrap_or_default();
|
||||
let template = spec.template.get_or_insert(PodTemplateSpec {
|
||||
metadata: Some(ObjectMeta {
|
||||
name: String::new(),
|
||||
namespace: Some("default".to_string()),
|
||||
uid: None,
|
||||
resource_version: None,
|
||||
creation_timestamp: None,
|
||||
labels: HashMap::new(),
|
||||
annotations: HashMap::new(),
|
||||
org_id: None,
|
||||
project_id: None,
|
||||
}),
|
||||
spec: Some(PodSpec {
|
||||
containers: Vec::new(),
|
||||
restart_policy: None,
|
||||
node_name: None,
|
||||
}),
|
||||
});
|
||||
let metadata = template.metadata.get_or_insert_with(|| ObjectMeta {
|
||||
name: String::new(),
|
||||
namespace: Some("default".to_string()),
|
||||
uid: None,
|
||||
resource_version: None,
|
||||
creation_timestamp: None,
|
||||
labels: HashMap::new(),
|
||||
annotations: HashMap::new(),
|
||||
org_id: None,
|
||||
project_id: None,
|
||||
});
|
||||
for (key, value) in selector {
|
||||
metadata.labels.entry(key).or_insert(value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to create error response
|
||||
fn error_response(
|
||||
status: StatusCode,
|
||||
|
|
@ -608,17 +1182,22 @@ async fn resolve_rest_tenant(
|
|||
.auth_service
|
||||
.authenticate_headers(headers)
|
||||
.await
|
||||
.map_err(map_auth_status)?;
|
||||
resolve_tenant_ids_from_context(&tenant, "", "").map_err(map_auth_status)?;
|
||||
.map_err(map_tonic_status)?;
|
||||
resolve_tenant_ids_from_context(&tenant, "", "").map_err(map_tonic_status)?;
|
||||
Ok(tenant)
|
||||
}
|
||||
|
||||
fn map_auth_status(status: tonic::Status) -> (StatusCode, Json<ErrorResponse>) {
|
||||
fn map_tonic_status(status: tonic::Status) -> (StatusCode, Json<ErrorResponse>) {
|
||||
let status_code = match status.code() {
|
||||
Code::Unauthenticated => StatusCode::UNAUTHORIZED,
|
||||
Code::PermissionDenied => StatusCode::FORBIDDEN,
|
||||
Code::InvalidArgument => StatusCode::BAD_REQUEST,
|
||||
Code::NotFound => StatusCode::NOT_FOUND,
|
||||
Code::AlreadyExists => StatusCode::CONFLICT,
|
||||
Code::FailedPrecondition => StatusCode::PRECONDITION_FAILED,
|
||||
Code::ResourceExhausted => StatusCode::TOO_MANY_REQUESTS,
|
||||
Code::DeadlineExceeded => StatusCode::GATEWAY_TIMEOUT,
|
||||
Code::Unavailable => StatusCode::SERVICE_UNAVAILABLE,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
let code = match status.code() {
|
||||
|
|
@ -626,6 +1205,11 @@ fn map_auth_status(status: tonic::Status) -> (StatusCode, Json<ErrorResponse>) {
|
|||
Code::PermissionDenied => "FORBIDDEN",
|
||||
Code::InvalidArgument => "INVALID_ARGUMENT",
|
||||
Code::NotFound => "NOT_FOUND",
|
||||
Code::AlreadyExists => "ALREADY_EXISTS",
|
||||
Code::FailedPrecondition => "FAILED_PRECONDITION",
|
||||
Code::ResourceExhausted => "RESOURCE_EXHAUSTED",
|
||||
Code::DeadlineExceeded => "DEADLINE_EXCEEDED",
|
||||
Code::Unavailable => "UNAVAILABLE",
|
||||
_ => "INTERNAL",
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,12 @@ in
|
|||
description = "Port for k8shost gRPC API server";
|
||||
};
|
||||
|
||||
httpPort = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
default = 8085;
|
||||
description = "Port for k8shost HTTP REST API server";
|
||||
};
|
||||
|
||||
iamAddr = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
|
|
@ -126,6 +132,7 @@ in
|
|||
ExecStart = lib.concatStringsSep " " ([
|
||||
"${cfg.package}/bin/k8shost-server"
|
||||
"--addr 0.0.0.0:${toString cfg.port}"
|
||||
"--http-addr 127.0.0.1:${toString cfg.httpPort}"
|
||||
] ++ lib.optional (cfg.iamAddr != null) "--iam-server-addr ${cfg.iamAddr}"
|
||||
++ lib.optional (cfg.chainfireAddr != null) "--chainfire-endpoint ${cfg.chainfireAddr}"
|
||||
++ lib.optional (cfg.prismnetAddr != null) "--prismnet-server-addr ${cfg.prismnetAddr}"
|
||||
|
|
|
|||
|
|
@ -3463,13 +3463,14 @@ validate_fiberlb_flow() {
|
|||
validate_k8shost_flow() {
|
||||
log "Validating K8sHost node, pod, service, and controller integrations"
|
||||
|
||||
local iam_tunnel="" prism_tunnel="" dns_tunnel="" lb_tunnel="" k8s_tunnel=""
|
||||
local iam_tunnel="" prism_tunnel="" dns_tunnel="" lb_tunnel="" k8s_tunnel="" k8s_http_tunnel=""
|
||||
iam_tunnel="$(start_ssh_tunnel node01 15080 50080)"
|
||||
prism_tunnel="$(start_ssh_tunnel node01 15081 50081)"
|
||||
dns_tunnel="$(start_ssh_tunnel node01 15084 50084)"
|
||||
lb_tunnel="$(start_ssh_tunnel node01 15085 50085)"
|
||||
k8s_tunnel="$(start_ssh_tunnel node01 15087 50087)"
|
||||
trap 'stop_ssh_tunnel node01 "${k8s_tunnel}"; stop_ssh_tunnel node01 "${lb_tunnel}"; stop_ssh_tunnel node01 "${dns_tunnel}"; stop_ssh_tunnel node01 "${prism_tunnel}"; stop_ssh_tunnel node01 "${iam_tunnel}"' RETURN
|
||||
k8s_http_tunnel="$(start_ssh_tunnel node01 18087 8085)"
|
||||
trap 'stop_ssh_tunnel node01 "${k8s_http_tunnel}"; stop_ssh_tunnel node01 "${k8s_tunnel}"; stop_ssh_tunnel node01 "${lb_tunnel}"; stop_ssh_tunnel node01 "${dns_tunnel}"; stop_ssh_tunnel node01 "${prism_tunnel}"; stop_ssh_tunnel node01 "${iam_tunnel}"' RETURN
|
||||
|
||||
local org_id="default-org"
|
||||
local project_id="default-project"
|
||||
|
|
@ -3503,19 +3504,16 @@ validate_k8shost_flow() {
|
|||
127.0.0.1:15087 k8shost.NodeService/ListNodes \
|
||||
| jq -e --arg name "${node_name}" '.items | any(.metadata.name == $name)' >/dev/null
|
||||
|
||||
grpcurl -plaintext \
|
||||
-H "authorization: Bearer ${token}" \
|
||||
-import-path "${K8SHOST_PROTO_DIR}" \
|
||||
-proto "${K8SHOST_PROTO}" \
|
||||
-d "$(jq -cn --arg name "${deployment_name}" --arg org "${org_id}" --arg project "${project_id}" '{deployment:{metadata:{name:$name, namespace:"default", orgId:$org, projectId:$project}, spec:{replicas:2, selector:{matchLabels:{app:"k8shost-deployment-smoke", deployment:$name}}, template:{metadata:{name:"", namespace:"default", labels:{app:"k8shost-deployment-smoke", deployment:$name}}, spec:{containers:[{name:"backend", image:"smoke", ports:[{containerPort:8082, protocol:"TCP"}]}]}}}}}')" \
|
||||
127.0.0.1:15087 k8shost.DeploymentService/CreateDeployment >/dev/null
|
||||
grpcurl -plaintext \
|
||||
-H "authorization: Bearer ${token}" \
|
||||
-import-path "${K8SHOST_PROTO_DIR}" \
|
||||
-proto "${K8SHOST_PROTO}" \
|
||||
-d "$(jq -cn '{namespace:"default"}')" \
|
||||
127.0.0.1:15087 k8shost.DeploymentService/ListDeployments \
|
||||
| jq -e --arg name "${deployment_name}" '.items | any(.metadata.name == $name)' >/dev/null
|
||||
curl -fsS \
|
||||
-H "Authorization: Bearer ${token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(jq -cn --arg name "${deployment_name}" '{name:$name, namespace:"default", replicas:2, selector:{app:"k8shost-deployment-smoke", deployment:$name}, containers:[{name:"backend", image:"smoke", ports:[{container_port:8082, protocol:"TCP"}]}]}')" \
|
||||
http://127.0.0.1:18087/api/v1/deployments \
|
||||
| jq -e --arg name "${deployment_name}" '.data.name == $name and .data.replicas == 2' >/dev/null
|
||||
curl -fsS \
|
||||
-H "Authorization: Bearer ${token}" \
|
||||
http://127.0.0.1:18087/api/v1/deployments?namespace=default \
|
||||
| jq -e --arg name "${deployment_name}" '.data.deployments | any(.name == $name)' >/dev/null
|
||||
|
||||
deadline=$((SECONDS + HTTP_WAIT_TIMEOUT))
|
||||
while true; do
|
||||
|
|
@ -3537,19 +3535,17 @@ validate_k8shost_flow() {
|
|||
sleep 2
|
||||
done
|
||||
|
||||
local deployment_json
|
||||
deployment_json="$(grpcurl -plaintext \
|
||||
-H "authorization: Bearer ${token}" \
|
||||
-import-path "${K8SHOST_PROTO_DIR}" \
|
||||
-proto "${K8SHOST_PROTO}" \
|
||||
-d "$(jq -cn --arg ns "default" --arg name "${deployment_name}" '{namespace:$ns, name:$name}')" \
|
||||
127.0.0.1:15087 k8shost.DeploymentService/GetDeployment)"
|
||||
grpcurl -plaintext \
|
||||
-H "authorization: Bearer ${token}" \
|
||||
-import-path "${K8SHOST_PROTO_DIR}" \
|
||||
-proto "${K8SHOST_PROTO}" \
|
||||
-d "$(printf '%s' "${deployment_json}" | jq '.deployment.spec.replicas = 1 | {deployment:.deployment}')" \
|
||||
127.0.0.1:15087 k8shost.DeploymentService/UpdateDeployment >/dev/null
|
||||
curl -fsS \
|
||||
-H "Authorization: Bearer ${token}" \
|
||||
http://127.0.0.1:18087/api/v1/deployments/default/${deployment_name} \
|
||||
| jq -e --arg name "${deployment_name}" '.data.name == $name and .data.ready_replicas >= 0' >/dev/null
|
||||
curl -fsS \
|
||||
-X PUT \
|
||||
-H "Authorization: Bearer ${token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"replicas":1}' \
|
||||
http://127.0.0.1:18087/api/v1/deployments/default/${deployment_name} \
|
||||
| jq -e '.data.replicas == 1' >/dev/null
|
||||
|
||||
deadline=$((SECONDS + HTTP_WAIT_TIMEOUT))
|
||||
while true; do
|
||||
|
|
@ -3569,12 +3565,11 @@ validate_k8shost_flow() {
|
|||
sleep 2
|
||||
done
|
||||
|
||||
grpcurl -plaintext \
|
||||
-H "authorization: Bearer ${token}" \
|
||||
-import-path "${K8SHOST_PROTO_DIR}" \
|
||||
-proto "${K8SHOST_PROTO}" \
|
||||
-d "$(jq -cn --arg ns "default" --arg name "${deployment_name}" '{namespace:$ns, name:$name}')" \
|
||||
127.0.0.1:15087 k8shost.DeploymentService/DeleteDeployment >/dev/null
|
||||
curl -fsS \
|
||||
-X DELETE \
|
||||
-H "Authorization: Bearer ${token}" \
|
||||
http://127.0.0.1:18087/api/v1/deployments/default/${deployment_name} \
|
||||
| jq -e '.data.deleted == true' >/dev/null
|
||||
|
||||
deadline=$((SECONDS + HTTP_WAIT_TIMEOUT))
|
||||
while true; do
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue