properly handling patch updates
Some checks failed
CI / Clippy (push) Failing after 3m6s
CI / Rustfmt (push) Failing after 3m9s
CI / Cargo Audit & Deny (push) Successful in 5m2s
CI / Tests (push) Successful in 8m15s
CI / Security Blocking Checks (push) Successful in 10s
CI / Web Advisory Checks (push) Successful in 1m4s
CI / Web Blocking Checks (push) Failing after 4m52s
Publish Images And Chart / Resolve Publish Metadata (push) Successful in 2s
CI / Security Advisory Checks (push) Successful in 1m31s
Publish Images And Chart / Publish init-user (push) Failing after 30s
Publish Images And Chart / Publish init-packs (push) Failing after 1m41s
Publish Images And Chart / Publish migrations (push) Failing after 10s
Publish Images And Chart / Publish web (push) Failing after 11s
Publish Images And Chart / Publish sensor (push) Failing after 32s
Publish Images And Chart / Publish worker (push) Failing after 11s
Publish Images And Chart / Publish executor (push) Failing after 11s
Publish Images And Chart / Publish notifier (push) Failing after 9s
Publish Images And Chart / Publish api (push) Failing after 31s
Publish Images And Chart / Publish Helm Chart (push) Has been skipped

This commit is contained in:
2026-03-17 12:17:58 -05:00
parent 643023b6d5
commit f96861d417
136 changed files with 3782 additions and 1553 deletions

View File

@@ -15,7 +15,7 @@ use attune_common::repositories::{
action::{ActionRepository, ActionSearchFilters, CreateActionInput, UpdateActionInput},
pack::PackRepository,
queue_stats::QueueStatsRepository,
Create, Delete, FindByRef, Update,
Create, Delete, FindByRef, Patch, Update,
};
use crate::{
@@ -24,7 +24,7 @@ use crate::{
dto::{
action::{
ActionResponse, ActionSummary, CreateActionRequest, QueueStatsResponse,
UpdateActionRequest,
RuntimeVersionConstraintPatch, UpdateActionRequest,
},
common::{PaginatedResponse, PaginationParams},
ApiResponse, SuccessResponse,
@@ -280,7 +280,10 @@ pub async fn update_action(
description: request.description,
entrypoint: request.entrypoint,
runtime: request.runtime,
runtime_version_constraint: request.runtime_version_constraint,
runtime_version_constraint: request.runtime_version_constraint.map(|patch| match patch {
RuntimeVersionConstraintPatch::Set(value) => Patch::Set(value),
RuntimeVersionConstraintPatch::Clear => Patch::Clear,
}),
param_schema: request.param_schema,
out_schema: request.out_schema,
parameter_delivery: None,

View File

@@ -36,15 +36,16 @@ use attune_common::repositories::{
ArtifactRepository, ArtifactSearchFilters, ArtifactVersionRepository, CreateArtifactInput,
CreateArtifactVersionInput, UpdateArtifactInput,
},
Create, Delete, FindById, FindByRef, Update,
Create, Delete, FindById, FindByRef, Patch, Update,
};
use crate::{
auth::middleware::RequireAuth,
dto::{
artifact::{
AllocateFileVersionByRefRequest, AppendProgressRequest, ArtifactQueryParams,
ArtifactResponse, ArtifactSummary, ArtifactVersionResponse, ArtifactVersionSummary,
AllocateFileVersionByRefRequest, AppendProgressRequest, ArtifactExecutionPatch,
ArtifactJsonPatch, ArtifactQueryParams, ArtifactResponse, ArtifactStringPatch,
ArtifactSummary, ArtifactVersionResponse, ArtifactVersionSummary,
CreateArtifactRequest, CreateFileVersionRequest, CreateVersionJsonRequest,
SetDataRequest, UpdateArtifactRequest,
},
@@ -257,12 +258,27 @@ pub async fn update_artifact(
visibility: request.visibility,
retention_policy: request.retention_policy,
retention_limit: request.retention_limit,
name: request.name,
description: request.description,
content_type: request.content_type,
name: request.name.map(|patch| match patch {
ArtifactStringPatch::Set(value) => Patch::Set(value),
ArtifactStringPatch::Clear => Patch::Clear,
}),
description: request.description.map(|patch| match patch {
ArtifactStringPatch::Set(value) => Patch::Set(value),
ArtifactStringPatch::Clear => Patch::Clear,
}),
content_type: request.content_type.map(|patch| match patch {
ArtifactStringPatch::Set(value) => Patch::Set(value),
ArtifactStringPatch::Clear => Patch::Clear,
}),
size_bytes: None, // Managed by version creation trigger
execution: request.execution.map(Some),
data: request.data,
execution: request.execution.map(|patch| match patch {
ArtifactExecutionPatch::Set(value) => Patch::Set(value),
ArtifactExecutionPatch::Clear => Patch::Clear,
}),
data: request.data.map(|patch| match patch {
ArtifactJsonPatch::Set(value) => Patch::Set(value),
ArtifactJsonPatch::Clear => Patch::Clear,
}),
};
let updated = ArtifactRepository::update(&state.db, id, input).await?;
@@ -1155,7 +1171,7 @@ pub async fn upload_version_by_ref(
description: None,
content_type: None,
size_bytes: None,
execution: execution_id.map(Some),
execution: execution_id.map(Patch::Set),
data: None,
};
ArtifactRepository::update(&state.db, existing.id, update_input).await?
@@ -1303,7 +1319,7 @@ pub async fn allocate_file_version_by_ref(
description: None,
content_type: None,
size_bytes: None,
execution: request.execution.map(Some),
execution: request.execution.map(Patch::Set),
data: None,
};
ArtifactRepository::update(&state.db, existing.id, update_input).await?

View File

@@ -13,6 +13,7 @@ pub mod keys;
pub mod packs;
pub mod permissions;
pub mod rules;
pub mod runtimes;
pub mod triggers;
pub mod webhooks;
pub mod workflows;
@@ -30,6 +31,7 @@ pub use keys::routes as key_routes;
pub use packs::routes as pack_routes;
pub use permissions::routes as permission_routes;
pub use rules::routes as rule_routes;
pub use runtimes::routes as runtime_routes;
pub use triggers::routes as trigger_routes;
pub use webhooks::routes as webhook_routes;
pub use workflows::routes as workflow_routes;

View File

@@ -16,7 +16,8 @@ use attune_common::mq::{MessageEnvelope, MessageType, PackRegisteredPayload};
use attune_common::rbac::{Action, AuthorizationContext, Resource};
use attune_common::repositories::{
pack::{CreatePackInput, UpdatePackInput},
Create, Delete, FindById, FindByRef, PackRepository, PackTestRepository, Pagination, Update,
Create, Delete, FindById, FindByRef, PackRepository, PackTestRepository, Pagination, Patch,
Update,
};
use attune_common::workflow::{PackWorkflowService, PackWorkflowServiceConfig};
@@ -29,8 +30,9 @@ use crate::{
BuildPackEnvsRequest, BuildPackEnvsResponse, CreatePackRequest, DownloadPacksRequest,
DownloadPacksResponse, GetPackDependenciesRequest, GetPackDependenciesResponse,
InstallPackRequest, PackInstallResponse, PackResponse, PackSummary,
PackWorkflowSyncResponse, PackWorkflowValidationResponse, RegisterPackRequest,
RegisterPacksRequest, RegisterPacksResponse, UpdatePackRequest, WorkflowSyncResult,
PackDescriptionPatch, PackWorkflowSyncResponse, PackWorkflowValidationResponse,
RegisterPackRequest, RegisterPacksRequest, RegisterPacksResponse, UpdatePackRequest,
WorkflowSyncResult,
},
ApiResponse, SuccessResponse,
},
@@ -258,7 +260,10 @@ pub async fn update_pack(
// Create update input
let update_input = UpdatePackInput {
label: request.label,
description: request.description,
description: request.description.map(|patch| match patch {
PackDescriptionPatch::Set(value) => Patch::Set(value),
PackDescriptionPatch::Clear => Patch::Clear,
}),
version: request.version,
conf_schema: request.conf_schema,
config: request.config,
@@ -876,7 +881,10 @@ async fn register_pack_internal(
// Update existing pack in place — preserves pack ID and all child entity IDs
let update_input = UpdatePackInput {
label: Some(label),
description: Some(description.unwrap_or_default()),
description: Some(match description {
Some(value) => Patch::Set(value),
None => Patch::Clear,
}),
version: Some(version.clone()),
conf_schema: Some(conf_schema),
config: None, // preserve user-set config

View File

@@ -0,0 +1,303 @@
//! Runtime management API routes
use axum::{
extract::{Path, Query, State},
http::StatusCode,
response::IntoResponse,
routing::get,
Json, Router,
};
use std::sync::Arc;
use validator::Validate;
use attune_common::repositories::{
pack::PackRepository,
runtime::{CreateRuntimeInput, RuntimeRepository, UpdateRuntimeInput},
Create, Delete, FindByRef, List, Patch, Update,
};
use crate::{
auth::middleware::RequireAuth,
dto::{
common::{PaginatedResponse, PaginationParams},
runtime::{
CreateRuntimeRequest, NullableJsonPatch, NullableStringPatch, RuntimeResponse,
RuntimeSummary, UpdateRuntimeRequest,
},
ApiResponse, SuccessResponse,
},
middleware::{ApiError, ApiResult},
state::AppState,
};
#[utoipa::path(
get,
path = "/api/v1/runtimes",
tag = "runtimes",
params(PaginationParams),
responses(
(status = 200, description = "List of runtimes", body = PaginatedResponse<RuntimeSummary>)
),
security(("bearer_auth" = []))
)]
pub async fn list_runtimes(
State(state): State<Arc<AppState>>,
RequireAuth(_user): RequireAuth,
Query(pagination): Query<PaginationParams>,
) -> ApiResult<impl IntoResponse> {
let all_runtimes = RuntimeRepository::list(&state.db).await?;
let total = all_runtimes.len() as u64;
let rows: Vec<_> = all_runtimes
.into_iter()
.skip(pagination.offset() as usize)
.take(pagination.limit() as usize)
.collect();
let response = PaginatedResponse::new(
rows.into_iter().map(RuntimeSummary::from).collect(),
&pagination,
total,
);
Ok((StatusCode::OK, Json(response)))
}
#[utoipa::path(
get,
path = "/api/v1/packs/{pack_ref}/runtimes",
tag = "runtimes",
params(
("pack_ref" = String, Path, description = "Pack reference identifier"),
PaginationParams
),
responses(
(status = 200, description = "List of runtimes for a pack", body = PaginatedResponse<RuntimeSummary>),
(status = 404, description = "Pack not found")
),
security(("bearer_auth" = []))
)]
pub async fn list_runtimes_by_pack(
State(state): State<Arc<AppState>>,
RequireAuth(_user): RequireAuth,
Path(pack_ref): Path<String>,
Query(pagination): Query<PaginationParams>,
) -> ApiResult<impl IntoResponse> {
let pack = PackRepository::find_by_ref(&state.db, &pack_ref)
.await?
.ok_or_else(|| ApiError::NotFound(format!("Pack '{}' not found", pack_ref)))?;
let all_runtimes = RuntimeRepository::find_by_pack(&state.db, pack.id).await?;
let total = all_runtimes.len() as u64;
let rows: Vec<_> = all_runtimes
.into_iter()
.skip(pagination.offset() as usize)
.take(pagination.limit() as usize)
.collect();
let response = PaginatedResponse::new(
rows.into_iter().map(RuntimeSummary::from).collect(),
&pagination,
total,
);
Ok((StatusCode::OK, Json(response)))
}
#[utoipa::path(
get,
path = "/api/v1/runtimes/{ref}",
tag = "runtimes",
params(("ref" = String, Path, description = "Runtime reference identifier")),
responses(
(status = 200, description = "Runtime details", body = ApiResponse<RuntimeResponse>),
(status = 404, description = "Runtime not found")
),
security(("bearer_auth" = []))
)]
pub async fn get_runtime(
State(state): State<Arc<AppState>>,
RequireAuth(_user): RequireAuth,
Path(runtime_ref): Path<String>,
) -> ApiResult<impl IntoResponse> {
let runtime = RuntimeRepository::find_by_ref(&state.db, &runtime_ref)
.await?
.ok_or_else(|| ApiError::NotFound(format!("Runtime '{}' not found", runtime_ref)))?;
Ok((
StatusCode::OK,
Json(ApiResponse::new(RuntimeResponse::from(runtime))),
))
}
#[utoipa::path(
post,
path = "/api/v1/runtimes",
tag = "runtimes",
request_body = CreateRuntimeRequest,
responses(
(status = 201, description = "Runtime created successfully", body = ApiResponse<RuntimeResponse>),
(status = 400, description = "Validation error"),
(status = 404, description = "Pack not found"),
(status = 409, description = "Runtime with same ref already exists")
),
security(("bearer_auth" = []))
)]
pub async fn create_runtime(
State(state): State<Arc<AppState>>,
RequireAuth(_user): RequireAuth,
Json(request): Json<CreateRuntimeRequest>,
) -> ApiResult<impl IntoResponse> {
request.validate()?;
if RuntimeRepository::find_by_ref(&state.db, &request.r#ref)
.await?
.is_some()
{
return Err(ApiError::Conflict(format!(
"Runtime with ref '{}' already exists",
request.r#ref
)));
}
let (pack_id, pack_ref) = if let Some(ref pack_ref_str) = request.pack_ref {
let pack = PackRepository::find_by_ref(&state.db, pack_ref_str)
.await?
.ok_or_else(|| ApiError::NotFound(format!("Pack '{}' not found", pack_ref_str)))?;
(Some(pack.id), Some(pack.r#ref))
} else {
(None, None)
};
let runtime = RuntimeRepository::create(
&state.db,
CreateRuntimeInput {
r#ref: request.r#ref,
pack: pack_id,
pack_ref,
description: request.description,
name: request.name,
distributions: request.distributions,
installation: request.installation,
execution_config: request.execution_config,
},
)
.await?;
Ok((
StatusCode::CREATED,
Json(ApiResponse::with_message(
RuntimeResponse::from(runtime),
"Runtime created successfully",
)),
))
}
#[utoipa::path(
put,
path = "/api/v1/runtimes/{ref}",
tag = "runtimes",
params(("ref" = String, Path, description = "Runtime reference identifier")),
request_body = UpdateRuntimeRequest,
responses(
(status = 200, description = "Runtime updated successfully", body = ApiResponse<RuntimeResponse>),
(status = 400, description = "Validation error"),
(status = 404, description = "Runtime not found")
),
security(("bearer_auth" = []))
)]
pub async fn update_runtime(
State(state): State<Arc<AppState>>,
RequireAuth(_user): RequireAuth,
Path(runtime_ref): Path<String>,
Json(request): Json<UpdateRuntimeRequest>,
) -> ApiResult<impl IntoResponse> {
request.validate()?;
let existing_runtime = RuntimeRepository::find_by_ref(&state.db, &runtime_ref)
.await?
.ok_or_else(|| ApiError::NotFound(format!("Runtime '{}' not found", runtime_ref)))?;
let runtime = RuntimeRepository::update(
&state.db,
existing_runtime.id,
UpdateRuntimeInput {
description: request.description.map(|patch| match patch {
NullableStringPatch::Set(value) => Patch::Set(value),
NullableStringPatch::Clear => Patch::Clear,
}),
name: request.name,
distributions: request.distributions,
installation: request.installation.map(|patch| match patch {
NullableJsonPatch::Set(value) => Patch::Set(value),
NullableJsonPatch::Clear => Patch::Clear,
}),
execution_config: request.execution_config,
},
)
.await?;
Ok((
StatusCode::OK,
Json(ApiResponse::with_message(
RuntimeResponse::from(runtime),
"Runtime updated successfully",
)),
))
}
#[utoipa::path(
delete,
path = "/api/v1/runtimes/{ref}",
tag = "runtimes",
params(("ref" = String, Path, description = "Runtime reference identifier")),
responses(
(status = 200, description = "Runtime deleted successfully", body = SuccessResponse),
(status = 404, description = "Runtime not found")
),
security(("bearer_auth" = []))
)]
pub async fn delete_runtime(
State(state): State<Arc<AppState>>,
RequireAuth(_user): RequireAuth,
Path(runtime_ref): Path<String>,
) -> ApiResult<impl IntoResponse> {
let runtime = RuntimeRepository::find_by_ref(&state.db, &runtime_ref)
.await?
.ok_or_else(|| ApiError::NotFound(format!("Runtime '{}' not found", runtime_ref)))?;
let deleted = RuntimeRepository::delete(&state.db, runtime.id).await?;
if !deleted {
return Err(ApiError::NotFound(format!(
"Runtime '{}' not found",
runtime_ref
)));
}
Ok((
StatusCode::OK,
Json(SuccessResponse::new(format!(
"Runtime '{}' deleted successfully",
runtime_ref
))),
))
}
pub fn routes() -> Router<Arc<AppState>> {
Router::new()
.route("/runtimes", get(list_runtimes).post(create_runtime))
.route(
"/runtimes/{ref}",
get(get_runtime).put(update_runtime).delete(delete_runtime),
)
.route("/packs/{pack_ref}/runtimes", get(list_runtimes_by_pack))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_runtime_routes_structure() {
let _router = routes();
}
}

View File

@@ -17,7 +17,7 @@ use attune_common::repositories::{
CreateSensorInput, CreateTriggerInput, SensorRepository, SensorSearchFilters,
TriggerRepository, TriggerSearchFilters, UpdateSensorInput, UpdateTriggerInput,
},
Create, Delete, FindByRef, Update,
Create, Delete, FindByRef, Patch, Update,
};
use crate::{
@@ -25,8 +25,9 @@ use crate::{
dto::{
common::{PaginatedResponse, PaginationParams},
trigger::{
CreateSensorRequest, CreateTriggerRequest, SensorResponse, SensorSummary,
TriggerResponse, TriggerSummary, UpdateSensorRequest, UpdateTriggerRequest,
CreateSensorRequest, CreateTriggerRequest, SensorJsonPatch, SensorResponse,
SensorSummary, TriggerJsonPatch, TriggerResponse, TriggerStringPatch,
TriggerSummary, UpdateSensorRequest, UpdateTriggerRequest,
},
ApiResponse, SuccessResponse,
},
@@ -274,10 +275,19 @@ pub async fn update_trigger(
// Create update input
let update_input = UpdateTriggerInput {
label: request.label,
description: request.description,
description: request.description.map(|patch| match patch {
TriggerStringPatch::Set(value) => Patch::Set(value),
TriggerStringPatch::Clear => Patch::Clear,
}),
enabled: request.enabled,
param_schema: request.param_schema,
out_schema: request.out_schema,
param_schema: request.param_schema.map(|patch| match patch {
TriggerJsonPatch::Set(value) => Patch::Set(value),
TriggerJsonPatch::Clear => Patch::Clear,
}),
out_schema: request.out_schema.map(|patch| match patch {
TriggerJsonPatch::Set(value) => Patch::Set(value),
TriggerJsonPatch::Clear => Patch::Clear,
}),
};
let trigger = TriggerRepository::update(&state.db, existing_trigger.id, update_input).await?;
@@ -722,7 +732,10 @@ pub async fn update_sensor(
trigger: None,
trigger_ref: None,
enabled: request.enabled,
param_schema: request.param_schema,
param_schema: request.param_schema.map(|patch| match patch {
SensorJsonPatch::Set(value) => Patch::Set(value),
SensorJsonPatch::Clear => Patch::Clear,
}),
config: None,
};