brokkr-broker::api::v1::stacks
Rust
Structs
Section titled “Structs”struct CreateDeploymentObjectRequest
Section titled “struct CreateDeploymentObjectRequest”pub
Derives: Debug, Deserialize, ToSchema
Wire DTO for creating a deployment object via the public API.
Distinct from brokkr_models::models::deployment_objects::NewDeploymentObject,
which carries server-derived fields (e.g. yaml_checksum).
Fields
Section titled “Fields”| Name | Type | Description |
|---|---|---|
yaml_content |
String |
YAML content of the deployment. |
is_deletion_marker |
bool |
Optional. Defaults to false. |
struct CreateDeploymentObjectQuery
Section titled “struct CreateDeploymentObjectQuery”pub
Derives: Debug, Default, Deserialize, utoipa::IntoParams
Query parameters for the deployment-object create endpoint. Used by the raw-YAML body path (Content-Type: application/yaml), where the flag cannot ride in the body (BROKKR-T-0194).
Fields
Section titled “Fields”| Name | Type | Description |
|---|---|---|
deletion_marker |
Option<bool> |
Marks the submission as a deletion marker. Only consulted on the raw-YAML path; on the JSON path the body field wins. |
struct TemplateInstantiationRequest
Section titled “struct TemplateInstantiationRequest”pub
Derives: Debug, Deserialize, Serialize, ToSchema
Fields
Section titled “Fields”| Name | Type | Description |
|---|---|---|
template_id |
Uuid |
|
parameters |
serde_json::Value |
struct TelemetryHistoryQuery
Section titled “struct TelemetryHistoryQuery”pub
Derives: Debug, serde::Deserialize, utoipa::IntoParams
Fields
Section titled “Fields”| Name | Type | Description |
|---|---|---|
since |
Option<chrono::DateTime<chrono::Utc>> |
Earliest created_at to include (ISO-8601). Defaults to now - retention_ceiling_seconds. Values older than the ceiling are silently clamped: only the retained window can be returned. |
limit |
Option<i64> |
Maximum rows to return. Defaults to 500; capped at 5000. |
struct RetentionInfo
Section titled “struct RetentionInfo”pub
Derives: Debug, serde::Serialize, utoipa::ToSchema
Fields
Section titled “Fields”| Name | Type | Description |
|---|---|---|
retention_ceiling_seconds |
u64 |
Hard upper bound on retention. Never exceeds 21600 (6h). |
effective_retention_seconds |
u64 |
Effective retention window for the stack. <= ceiling. |
oldest_available_ts |
Option<chrono::DateTime<chrono::Utc>> |
Server-side timestamp of the oldest row currently retained for this stack, or null when no rows exist in the window. |
long_term_sink_hint |
&'static str |
Recommended sink for long-term centralisation. Brokkr is NOT a log warehouse — see project_log_retention_stance. |
struct K8sEventHistoryResponse
Section titled “struct K8sEventHistoryResponse”pub
Derives: Debug, serde::Serialize, utoipa::ToSchema
Fields
Section titled “Fields”| Name | Type | Description |
|---|---|---|
retention |
RetentionInfo |
|
events |
Vec<AgentK8sEvent> |
struct PodLogHistoryResponse
Section titled “struct PodLogHistoryResponse”pub
Derives: Debug, serde::Serialize, utoipa::ToSchema
Fields
Section titled “Fields”| Name | Type | Description |
|---|---|---|
retention |
RetentionInfo |
|
lines |
Vec<AgentPodLog> |
Functions
Section titled “Functions”fn routes
Section titled “fn routes”pub
fn routes() -> Router<DAL>Source
pub fn routes() -> Router<DAL> { info!("Setting up stack routes"); Router::new() .route("/stacks", get(list_stacks).post(create_stack)) .route( "/stacks/:id", get(get_stack).put(update_stack).delete(delete_stack), ) .route( "/stacks/:id/deployment-objects", get(list_deployment_objects).post(create_deployment_object), ) .route( "/stacks/:id/deployment-objects/from-template", post(instantiate_template), ) .route("/stacks/:id/labels", get(list_labels).post(add_label)) .route("/stacks/:id/labels/:label", delete(remove_label)) .route( "/stacks/:id/annotations", get(list_annotations).post(add_annotation), ) .route("/stacks/:id/annotations/:key", delete(remove_annotation)) // WS-10: short-lived telemetry history (BROKKR-I-0019). Bounded by // the 6h retention ceiling (project_log_retention_stance). .route("/stacks/:id/events", get(list_telemetry_events)) .route("/stacks/:id/logs", get(list_telemetry_logs))}fn list_deployment_objects
Section titled “fn list_deployment_objects”pub
async fn list_deployment_objects( State(dal): State<DAL>, Extension(auth_payload): Extension<AuthPayload>, Path(stack_id): Path<Uuid>,) -> Result<Json<Vec<DeploymentObject>>, ApiError>Source
pub async fn list_deployment_objects( State(dal): State<DAL>, Extension(auth_payload): Extension<AuthPayload>, Path(stack_id): Path<Uuid>,) -> Result<Json<Vec<DeploymentObject>>, ApiError> { fetch_owned_stack(&dal, &auth_payload, stack_id).await?; let objects = dal .deployment_objects() .list_for_stack(stack_id) .map_err(|_| ApiError::internal("failed to fetch deployment objects"))?; metrics::set_deployment_objects_total(objects.len() as i64); Ok(Json(objects))}fn create_deployment_object
Section titled “fn create_deployment_object”pub
async fn create_deployment_object( State(dal): State<DAL>, Extension(auth_payload): Extension<AuthPayload>, Extension(ws_registry): Extension<Arc<ConnectionRegistry>>, Path(stack_id): Path<Uuid>, Query(query): Query<CreateDeploymentObjectQuery>, headers: HeaderMap, body: Bytes,) -> Result<(StatusCode, Json<DeploymentObject>), ApiError>Source
pub async fn create_deployment_object( State(dal): State<DAL>, Extension(auth_payload): Extension<AuthPayload>, Extension(ws_registry): Extension<Arc<ConnectionRegistry>>, Path(stack_id): Path<Uuid>, Query(query): Query<CreateDeploymentObjectQuery>, headers: HeaderMap, // Body-consuming extractor must come last. body: Bytes,) -> Result<(StatusCode, Json<DeploymentObject>), ApiError> { let stack = fetch_owned_stack(&dal, &auth_payload, stack_id).await?; let (yaml_content, is_deletion_marker) = resolve_create_body(&headers, &query, &body)?; validate_manifest_yaml(&yaml_content, is_deletion_marker)?; let new_object = NewDeploymentObject::new(stack_id, yaml_content, is_deletion_marker) .map_err(|e| ApiError::bad_request("invalid_deployment_object", e))?; let object = dal .deployment_objects() .create(&new_object) .map_err(|_| ApiError::internal("failed to create deployment object"))?;
// Post-commit: notify every connected agent that targets this stack so // they can reconcile the new deployment object immediately rather than // waiting for the next REST polling tick. push_stack_changed_to_targets(&ws_registry, &dal, &stack);
Ok((StatusCode::CREATED, Json(object)))}fn list_labels
Section titled “fn list_labels”pub
async fn list_labels( State(dal): State<DAL>, Extension(auth_payload): Extension<AuthPayload>, Path(stack_id): Path<Uuid>,) -> Result<Json<Vec<StackLabel>>, ApiError>Source
pub async fn list_labels( State(dal): State<DAL>, Extension(auth_payload): Extension<AuthPayload>, Path(stack_id): Path<Uuid>,) -> Result<Json<Vec<StackLabel>>, ApiError> { if !is_authorized_for_stack(&dal, &auth_payload, stack_id).await? { return Err(ApiError::forbidden( "stack_not_accessible", "not authorized to access this stack", )); } let labels = dal .stack_labels() .list_for_stack(stack_id) .map_err(|_| ApiError::internal("failed to fetch stack labels"))?; Ok(Json(labels))}fn add_label
Section titled “fn add_label”pub
async fn add_label( State(dal): State<DAL>, Extension(auth_payload): Extension<AuthPayload>, Path(stack_id): Path<Uuid>, Json(label): Json<String>,) -> Result<(StatusCode, Json<StackLabel>), ApiError>Source
pub async fn add_label( State(dal): State<DAL>, Extension(auth_payload): Extension<AuthPayload>, Path(stack_id): Path<Uuid>, Json(label): Json<String>,) -> Result<(StatusCode, Json<StackLabel>), ApiError> { fetch_owned_stack(&dal, &auth_payload, stack_id).await?; let new_label = NewStackLabel::new(stack_id, label) .map_err(|e| ApiError::bad_request("invalid_label", e))?; // Route the DB error through the standard conversion so a re-added label // (UNIQUE (stack_id, label)) surfaces as 409 unique_violation rather than a // blanket 500 — idempotent callers like `apply` rely on the 409 to treat a // label that already exists as a no-op. let label = dal .stack_labels() .create(&new_label) .map_err(|e| ApiError::from_diesel(e, "failed to add stack label"))?; Ok((StatusCode::CREATED, Json(label)))}fn remove_label
Section titled “fn remove_label”pub
async fn remove_label( State(dal): State<DAL>, Extension(auth_payload): Extension<AuthPayload>, Path((stack_id, label)): Path<(Uuid, String)>,) -> Result<StatusCode, ApiError>Source
pub async fn remove_label( State(dal): State<DAL>, Extension(auth_payload): Extension<AuthPayload>, Path((stack_id, label)): Path<(Uuid, String)>,) -> Result<StatusCode, ApiError> { fetch_owned_stack(&dal, &auth_payload, stack_id).await?; let deleted = dal .stack_labels() .delete_by_stack_and_label(stack_id, &label) .map_err(|_| ApiError::internal("failed to remove stack label"))?; if deleted > 0 { Ok(StatusCode::NO_CONTENT) } else { Err(ApiError::not_found( "stack_label_not_found", "label not found", )) }}fn list_annotations
Section titled “fn list_annotations”pub
async fn list_annotations( State(dal): State<DAL>, Extension(auth_payload): Extension<AuthPayload>, Path(stack_id): Path<Uuid>,) -> Result<Json<Vec<StackAnnotation>>, ApiError>Source
pub async fn list_annotations( State(dal): State<DAL>, Extension(auth_payload): Extension<AuthPayload>, Path(stack_id): Path<Uuid>,) -> Result<Json<Vec<StackAnnotation>>, ApiError> { if !is_authorized_for_stack(&dal, &auth_payload, stack_id).await? { return Err(ApiError::forbidden( "stack_not_accessible", "not authorized to access this stack", )); } let annotations = dal .stack_annotations() .list_for_stack(stack_id) .map_err(|_| ApiError::internal("failed to fetch stack annotations"))?; Ok(Json(annotations))}fn add_annotation
Section titled “fn add_annotation”pub
async fn add_annotation( State(dal): State<DAL>, Extension(auth_payload): Extension<AuthPayload>, Path(stack_id): Path<Uuid>, Json(new_annotation): Json<NewStackAnnotation>,) -> Result<(StatusCode, Json<StackAnnotation>), ApiError>Source
pub async fn add_annotation( State(dal): State<DAL>, Extension(auth_payload): Extension<AuthPayload>, Path(stack_id): Path<Uuid>, Json(new_annotation): Json<NewStackAnnotation>,) -> Result<(StatusCode, Json<StackAnnotation>), ApiError> { fetch_owned_stack(&dal, &auth_payload, stack_id).await?; if new_annotation.stack_id != stack_id { return Err(ApiError::bad_request( "stack_id_mismatch", "stack ID mismatch", )); } let annotation = dal .stack_annotations() .create(&new_annotation) .map_err(|e| ApiError::from_diesel(e, "failed to add stack annotation"))?; Ok((StatusCode::CREATED, Json(annotation)))}fn remove_annotation
Section titled “fn remove_annotation”pub
async fn remove_annotation( State(dal): State<DAL>, Extension(auth_payload): Extension<AuthPayload>, Path((stack_id, key)): Path<(Uuid, String)>,) -> Result<StatusCode, ApiError>Source
pub async fn remove_annotation( State(dal): State<DAL>, Extension(auth_payload): Extension<AuthPayload>, Path((stack_id, key)): Path<(Uuid, String)>,) -> Result<StatusCode, ApiError> { fetch_owned_stack(&dal, &auth_payload, stack_id).await?; let deleted = dal .stack_annotations() .delete_by_stack_and_key(stack_id, &key) .map_err(|_| ApiError::internal("failed to remove stack annotation"))?; if deleted > 0 { Ok(StatusCode::NO_CONTENT) } else { Err(ApiError::not_found( "stack_annotation_not_found", "annotation not found", )) }}fn list_telemetry_events
Section titled “fn list_telemetry_events”pub
async fn list_telemetry_events( State(dal): State<DAL>, Extension(auth): Extension<AuthPayload>, Path(stack_id): Path<Uuid>, axum::extract::Query(q): axum::extract::Query<TelemetryHistoryQuery>,) -> Result<Json<K8sEventHistoryResponse>, ApiError>Source
pub async fn list_telemetry_events( State(dal): State<DAL>, Extension(auth): Extension<AuthPayload>, Path(stack_id): Path<Uuid>, axum::extract::Query(q): axum::extract::Query<TelemetryHistoryQuery>,) -> Result<Json<K8sEventHistoryResponse>, ApiError> { fetch_owned_stack(&dal, &auth, stack_id).await?; let since = clamp_since(q.since); let limit = clamp_limit(q.limit); let events = dal .agent_k8s_events() .list_for_stack(stack_id, since, limit) .map_err(|e| { error!("failed to list k8s events for stack {}: {:?}", stack_id, e); ApiError::internal("failed to list telemetry events") })?; let oldest = events.last().map(|e| e.created_at); Ok(Json(K8sEventHistoryResponse { retention: retention_info(oldest), events, }))}fn list_telemetry_logs
Section titled “fn list_telemetry_logs”pub
async fn list_telemetry_logs( State(dal): State<DAL>, Extension(auth): Extension<AuthPayload>, Path(stack_id): Path<Uuid>, axum::extract::Query(q): axum::extract::Query<TelemetryHistoryQuery>,) -> Result<Json<PodLogHistoryResponse>, ApiError>Source
pub async fn list_telemetry_logs( State(dal): State<DAL>, Extension(auth): Extension<AuthPayload>, Path(stack_id): Path<Uuid>, axum::extract::Query(q): axum::extract::Query<TelemetryHistoryQuery>,) -> Result<Json<PodLogHistoryResponse>, ApiError> { fetch_owned_stack(&dal, &auth, stack_id).await?; let since = clamp_since(q.since); let limit = clamp_limit(q.limit); let lines = dal .agent_pod_logs() .list_for_stack(stack_id, since, limit) .map_err(|e| { error!("failed to list pod logs for stack {}: {:?}", stack_id, e); ApiError::internal("failed to list telemetry logs") })?; let oldest = lines.last().map(|l| l.created_at); Ok(Json(PodLogHistoryResponse { retention: retention_info(oldest), lines, }))}