use axum::{
extract::{Path, State},
http::StatusCode,
Json,
};
use planspec_core::Validator;
use serde_json::{json, Value};
use crate::AppState;
fn populate_resource_status(body: &mut Value, kind: &str) {
let now = chrono::Utc::now().to_rfc3339();
if body.get("status").is_none() {
body["status"] = json!({});
}
match kind {
"Goal" => {
if body["status"].get("phase").is_none() {
body["status"]["phase"] = json!("Pending");
}
if body["status"].get("conditions").is_none() {
body["status"]["conditions"] = json!([{
"type": "Accepted",
"status": "True",
"reason": "GoalCreated",
"message": "Goal has been accepted",
"lastTransitionTime": now
}]);
}
}
"Plan" => {
let node_count = body
.get("spec")
.and_then(|s| s.get("graph"))
.and_then(|g| g.get("nodes"))
.and_then(|n| n.as_array())
.map(|a| a.len())
.unwrap_or(0);
body["status"]["phase"] = json!("Ready");
body["status"]["nodeCount"] = json!(node_count);
body["status"]["conditions"] = json!([{
"type": "Valid",
"status": "True",
"reason": "SchemaValid",
"message": "Plan passed schema validation",
"lastTransitionTime": now
}]);
}
"Execution" => {
if body["status"].get("phase").is_none() {
body["status"]["phase"] = json!("Pending");
}
if body["status"].get("conditions").is_none() {
body["status"]["conditions"] = json!([{
"type": "Accepted",
"status": "True",
"reason": "ExecutionCreated",
"message": "Execution has been accepted and is pending",
"lastTransitionTime": now
}]);
}
}
"Capability" => {
if body["status"].get("phase").is_none() {
body["status"]["phase"] = json!("Available");
}
if body["status"].get("conditions").is_none() {
body["status"]["conditions"] = json!([{
"type": "Available",
"status": "True",
"reason": "CapabilityRegistered",
"message": "Capability is available for use",
"lastTransitionTime": now
}]);
}
}
"Binding" => {
if body["status"].get("phase").is_none() {
body["status"]["phase"] = json!("Unresolved");
}
if body["status"].get("conditions").is_none() {
body["status"]["conditions"] = json!([{
"type": "Resolved",
"status": "False",
"reason": "BindingCreated",
"message": "Binding created, resolution pending",
"lastTransitionTime": now
}]);
}
}
_ => {}
}
}
pub async fn apply(
State(state): State<AppState>,
Path(namespace): Path<String>,
Json(resources): Json<Vec<Value>>,
) -> Result<Json<Value>, (StatusCode, Json<Value>)> {
if resources.is_empty() {
return Ok(Json(json!({
"applied": [],
"errors": []
})));
}
let validator = Validator::new().map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"kind": "Status",
"status": "Failure",
"message": e.to_string(),
"code": 500
})),
)
})?;
let mut validation_errors = Vec::new();
for (i, resource) in resources.iter().enumerate() {
let kind = resource
.get("kind")
.and_then(|k| k.as_str())
.unwrap_or("Unknown");
let name = resource
.get("metadata")
.and_then(|m| m.get("name"))
.and_then(|n| n.as_str())
.unwrap_or("<unnamed>");
if let Err(errors) = validator.validate_json(resource) {
let error_messages: Vec<String> = errors.iter().map(|e| e.to_string()).collect();
validation_errors.push(json!({
"index": i,
"resource": format!("{}/{}", kind, name),
"reason": "Invalid",
"message": error_messages.join("; ")
}));
}
}
if !validation_errors.is_empty() {
return Err((
StatusCode::BAD_REQUEST,
Json(json!({
"kind": "Status",
"status": "Failure",
"message": "Validation failed",
"reason": "Invalid",
"details": {
"causes": validation_errors
},
"code": 400
})),
));
}
state
.store
.ensure_namespace(&namespace)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"kind": "Status",
"status": "Failure",
"message": e.to_string(),
"code": 500
})),
)
})?;
let mut applied = Vec::new();
let mut errors = Vec::new();
for mut resource in resources {
let kind = resource
.get("kind")
.and_then(|k| k.as_str())
.unwrap_or("Unknown")
.to_string();
let name = resource
.get("metadata")
.and_then(|m| m.get("name"))
.and_then(|n| n.as_str())
.unwrap_or("")
.to_string();
if let Some(metadata) = resource.get_mut("metadata").and_then(|m| m.as_object_mut()) {
metadata.insert("namespace".to_string(), Value::String(namespace.clone()));
}
populate_resource_status(&mut resource, &kind);
let existing = state.store.get(&namespace, &kind, &name).await;
match existing {
Ok(Some(_)) => {
match state
.store
.replace(&namespace, &kind, &name, resource, None)
.await
{
Ok((_stored, event)) => {
state.broadcaster.send(event);
applied.push(json!({
"kind": kind,
"name": name,
"namespace": namespace,
"action": "updated"
}));
}
Err(e) => {
errors.push(json!({
"resource": format!("{}/{}", kind, name),
"message": e.to_string()
}));
}
}
}
Ok(None) => {
match state.store.create(&namespace, &kind, &name, resource).await {
Ok((_stored, event)) => {
state.broadcaster.send(event);
applied.push(json!({
"kind": kind,
"name": name,
"namespace": namespace,
"action": "created"
}));
}
Err(e) => {
errors.push(json!({
"resource": format!("{}/{}", kind, name),
"message": e.to_string()
}));
}
}
}
Err(e) => {
errors.push(json!({
"resource": format!("{}/{}", kind, name),
"message": e.to_string()
}));
}
}
}
if !errors.is_empty() {
Ok(Json(json!({
"applied": applied,
"errors": errors
})))
} else {
Ok(Json(json!({
"applied": applied,
"errors": []
})))
}
}