Implement k8shost deployment REST API

This commit is contained in:
centra 2026-04-01 00:14:40 +09:00
parent 2b7c3166d2
commit 23ec8b5edb
Signed by: centra
GPG key ID: 0C09689D20B25ACA
4 changed files with 693 additions and 96 deletions

View file

@ -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(),

View file

@ -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",
};

View file

@ -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}"

View file

@ -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