working out the worker/execution interface
This commit is contained in:
@@ -17,6 +17,10 @@ pub struct CreateExecutionRequest {
|
||||
/// Execution parameters/configuration
|
||||
#[schema(value_type = Object, example = json!({"channel": "#alerts", "message": "Manual test"}))]
|
||||
pub parameters: Option<JsonValue>,
|
||||
|
||||
/// Environment variables for this execution
|
||||
#[schema(value_type = Object, example = json!({"DEBUG": "true", "LOG_LEVEL": "info"}))]
|
||||
pub env_vars: Option<JsonValue>,
|
||||
}
|
||||
|
||||
/// Response DTO for execution information
|
||||
|
||||
@@ -336,10 +336,455 @@ pub struct PackWorkflowValidationResponse {
|
||||
pub errors: std::collections::HashMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
/// Request DTO for downloading packs
|
||||
#[derive(Debug, Clone, Deserialize, Validate, ToSchema)]
|
||||
pub struct DownloadPacksRequest {
|
||||
/// List of pack sources (git URLs, HTTP URLs, or registry refs)
|
||||
#[validate(length(min = 1))]
|
||||
#[schema(example = json!(["https://github.com/attune/pack-slack.git", "aws@2.0.0"]))]
|
||||
pub packs: Vec<String>,
|
||||
|
||||
/// Destination directory for downloaded packs
|
||||
#[validate(length(min = 1))]
|
||||
#[schema(example = "/tmp/attune-packs")]
|
||||
pub destination_dir: String,
|
||||
|
||||
/// Pack registry URL for resolving references
|
||||
#[schema(example = "https://registry.attune.io/index.json")]
|
||||
pub registry_url: Option<String>,
|
||||
|
||||
/// Git reference (branch, tag, or commit) for git sources
|
||||
#[schema(example = "v1.0.0")]
|
||||
pub ref_spec: Option<String>,
|
||||
|
||||
/// Download timeout in seconds
|
||||
#[serde(default = "default_download_timeout")]
|
||||
#[schema(example = 300)]
|
||||
pub timeout: u64,
|
||||
|
||||
/// Verify SSL certificates
|
||||
#[serde(default = "default_true")]
|
||||
#[schema(example = true)]
|
||||
pub verify_ssl: bool,
|
||||
}
|
||||
|
||||
/// Response DTO for download packs operation
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct DownloadPacksResponse {
|
||||
/// Successfully downloaded packs
|
||||
pub downloaded_packs: Vec<DownloadedPack>,
|
||||
/// Failed pack downloads
|
||||
pub failed_packs: Vec<FailedPack>,
|
||||
/// Total number of packs requested
|
||||
pub total_count: usize,
|
||||
/// Number of successful downloads
|
||||
pub success_count: usize,
|
||||
/// Number of failed downloads
|
||||
pub failure_count: usize,
|
||||
}
|
||||
|
||||
/// Information about a downloaded pack
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct DownloadedPack {
|
||||
/// Original source
|
||||
pub source: String,
|
||||
/// Source type (git, http, registry)
|
||||
pub source_type: String,
|
||||
/// Local path to downloaded pack
|
||||
pub pack_path: String,
|
||||
/// Pack reference from pack.yaml
|
||||
pub pack_ref: String,
|
||||
/// Pack version from pack.yaml
|
||||
pub pack_version: String,
|
||||
/// Git commit hash (for git sources)
|
||||
pub git_commit: Option<String>,
|
||||
/// Directory checksum
|
||||
pub checksum: Option<String>,
|
||||
}
|
||||
|
||||
/// Information about a failed pack download
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct FailedPack {
|
||||
/// Pack source that failed
|
||||
pub source: String,
|
||||
/// Error message
|
||||
pub error: String,
|
||||
}
|
||||
|
||||
/// Request DTO for getting pack dependencies
|
||||
#[derive(Debug, Clone, Deserialize, Validate, ToSchema)]
|
||||
pub struct GetPackDependenciesRequest {
|
||||
/// List of pack directory paths to analyze
|
||||
#[validate(length(min = 1))]
|
||||
#[schema(example = json!(["/tmp/attune-packs/slack"]))]
|
||||
pub pack_paths: Vec<String>,
|
||||
|
||||
/// Skip pack.yaml validation
|
||||
#[serde(default)]
|
||||
#[schema(example = false)]
|
||||
pub skip_validation: bool,
|
||||
}
|
||||
|
||||
/// Response DTO for get pack dependencies operation
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct GetPackDependenciesResponse {
|
||||
/// All dependencies found
|
||||
pub dependencies: Vec<PackDependency>,
|
||||
/// Runtime requirements by pack
|
||||
pub runtime_requirements: std::collections::HashMap<String, RuntimeRequirements>,
|
||||
/// Dependencies not yet installed
|
||||
pub missing_dependencies: Vec<PackDependency>,
|
||||
/// Packs that were analyzed
|
||||
pub analyzed_packs: Vec<AnalyzedPack>,
|
||||
/// Errors encountered during analysis
|
||||
pub errors: Vec<DependencyError>,
|
||||
}
|
||||
|
||||
/// Pack dependency information
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct PackDependency {
|
||||
/// Pack reference
|
||||
pub pack_ref: String,
|
||||
/// Version specification
|
||||
pub version_spec: String,
|
||||
/// Pack that requires this dependency
|
||||
pub required_by: String,
|
||||
/// Whether dependency is already installed
|
||||
pub already_installed: bool,
|
||||
}
|
||||
|
||||
/// Runtime requirements for a pack
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct RuntimeRequirements {
|
||||
/// Pack reference
|
||||
pub pack_ref: String,
|
||||
/// Python requirements
|
||||
pub python: Option<PythonRequirements>,
|
||||
/// Node.js requirements
|
||||
pub nodejs: Option<NodeJsRequirements>,
|
||||
}
|
||||
|
||||
/// Python runtime requirements
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct PythonRequirements {
|
||||
/// Python version requirement
|
||||
pub version: Option<String>,
|
||||
/// Path to requirements.txt
|
||||
pub requirements_file: Option<String>,
|
||||
}
|
||||
|
||||
/// Node.js runtime requirements
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct NodeJsRequirements {
|
||||
/// Node.js version requirement
|
||||
pub version: Option<String>,
|
||||
/// Path to package.json
|
||||
pub package_file: Option<String>,
|
||||
}
|
||||
|
||||
/// Information about an analyzed pack
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct AnalyzedPack {
|
||||
/// Pack reference
|
||||
pub pack_ref: String,
|
||||
/// Pack directory path
|
||||
pub pack_path: String,
|
||||
/// Whether pack has dependencies
|
||||
pub has_dependencies: bool,
|
||||
/// Number of dependencies
|
||||
pub dependency_count: usize,
|
||||
}
|
||||
|
||||
/// Dependency analysis error
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct DependencyError {
|
||||
/// Pack path where error occurred
|
||||
pub pack_path: String,
|
||||
/// Error message
|
||||
pub error: String,
|
||||
}
|
||||
|
||||
/// Request DTO for building pack environments
|
||||
#[derive(Debug, Clone, Deserialize, Validate, ToSchema)]
|
||||
pub struct BuildPackEnvsRequest {
|
||||
/// List of pack directory paths
|
||||
#[validate(length(min = 1))]
|
||||
#[schema(example = json!(["/tmp/attune-packs/slack"]))]
|
||||
pub pack_paths: Vec<String>,
|
||||
|
||||
/// Base directory for permanent pack storage
|
||||
#[schema(example = "/opt/attune/packs")]
|
||||
pub packs_base_dir: Option<String>,
|
||||
|
||||
/// Python version to use
|
||||
#[serde(default = "default_python_version")]
|
||||
#[schema(example = "3.11")]
|
||||
pub python_version: String,
|
||||
|
||||
/// Node.js version to use
|
||||
#[serde(default = "default_nodejs_version")]
|
||||
#[schema(example = "20")]
|
||||
pub nodejs_version: String,
|
||||
|
||||
/// Skip building Python environments
|
||||
#[serde(default)]
|
||||
#[schema(example = false)]
|
||||
pub skip_python: bool,
|
||||
|
||||
/// Skip building Node.js environments
|
||||
#[serde(default)]
|
||||
#[schema(example = false)]
|
||||
pub skip_nodejs: bool,
|
||||
|
||||
/// Force rebuild of existing environments
|
||||
#[serde(default)]
|
||||
#[schema(example = false)]
|
||||
pub force_rebuild: bool,
|
||||
|
||||
/// Timeout in seconds for building each environment
|
||||
#[serde(default = "default_build_timeout")]
|
||||
#[schema(example = 600)]
|
||||
pub timeout: u64,
|
||||
}
|
||||
|
||||
/// Response DTO for build pack environments operation
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct BuildPackEnvsResponse {
|
||||
/// Successfully built environments
|
||||
pub built_environments: Vec<BuiltEnvironment>,
|
||||
/// Failed environment builds
|
||||
pub failed_environments: Vec<FailedEnvironment>,
|
||||
/// Summary statistics
|
||||
pub summary: BuildSummary,
|
||||
}
|
||||
|
||||
/// Information about a built environment
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct BuiltEnvironment {
|
||||
/// Pack reference
|
||||
pub pack_ref: String,
|
||||
/// Pack directory path
|
||||
pub pack_path: String,
|
||||
/// Built environments
|
||||
pub environments: Environments,
|
||||
/// Build duration in milliseconds
|
||||
pub duration_ms: u64,
|
||||
}
|
||||
|
||||
/// Environment details
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct Environments {
|
||||
/// Python environment
|
||||
pub python: Option<PythonEnvironment>,
|
||||
/// Node.js environment
|
||||
pub nodejs: Option<NodeJsEnvironment>,
|
||||
}
|
||||
|
||||
/// Python environment details
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct PythonEnvironment {
|
||||
/// Path to virtualenv
|
||||
pub virtualenv_path: String,
|
||||
/// Whether requirements were installed
|
||||
pub requirements_installed: bool,
|
||||
/// Number of packages installed
|
||||
pub package_count: usize,
|
||||
/// Python version used
|
||||
pub python_version: String,
|
||||
}
|
||||
|
||||
/// Node.js environment details
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct NodeJsEnvironment {
|
||||
/// Path to node_modules
|
||||
pub node_modules_path: String,
|
||||
/// Whether dependencies were installed
|
||||
pub dependencies_installed: bool,
|
||||
/// Number of packages installed
|
||||
pub package_count: usize,
|
||||
/// Node.js version used
|
||||
pub nodejs_version: String,
|
||||
}
|
||||
|
||||
/// Failed environment build
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct FailedEnvironment {
|
||||
/// Pack reference
|
||||
pub pack_ref: String,
|
||||
/// Pack directory path
|
||||
pub pack_path: String,
|
||||
/// Runtime that failed
|
||||
pub runtime: String,
|
||||
/// Error message
|
||||
pub error: String,
|
||||
}
|
||||
|
||||
/// Build summary statistics
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct BuildSummary {
|
||||
/// Total packs processed
|
||||
pub total_packs: usize,
|
||||
/// Successfully built
|
||||
pub success_count: usize,
|
||||
/// Failed builds
|
||||
pub failure_count: usize,
|
||||
/// Python environments built
|
||||
pub python_envs_built: usize,
|
||||
/// Node.js environments built
|
||||
pub nodejs_envs_built: usize,
|
||||
/// Total duration in milliseconds
|
||||
pub total_duration_ms: u64,
|
||||
}
|
||||
|
||||
/// Request DTO for registering multiple packs
|
||||
#[derive(Debug, Clone, Deserialize, Validate, ToSchema)]
|
||||
pub struct RegisterPacksRequest {
|
||||
/// List of pack directory paths to register
|
||||
#[validate(length(min = 1))]
|
||||
#[schema(example = json!(["/tmp/attune-packs/slack"]))]
|
||||
pub pack_paths: Vec<String>,
|
||||
|
||||
/// Base directory for permanent storage
|
||||
#[schema(example = "/opt/attune/packs")]
|
||||
pub packs_base_dir: Option<String>,
|
||||
|
||||
/// Skip schema validation
|
||||
#[serde(default)]
|
||||
#[schema(example = false)]
|
||||
pub skip_validation: bool,
|
||||
|
||||
/// Skip running pack tests
|
||||
#[serde(default)]
|
||||
#[schema(example = false)]
|
||||
pub skip_tests: bool,
|
||||
|
||||
/// Force registration (replace if exists)
|
||||
#[serde(default)]
|
||||
#[schema(example = false)]
|
||||
pub force: bool,
|
||||
}
|
||||
|
||||
/// Response DTO for register packs operation
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct RegisterPacksResponse {
|
||||
/// Successfully registered packs
|
||||
pub registered_packs: Vec<RegisteredPack>,
|
||||
/// Failed pack registrations
|
||||
pub failed_packs: Vec<FailedPackRegistration>,
|
||||
/// Summary statistics
|
||||
pub summary: RegistrationSummary,
|
||||
}
|
||||
|
||||
/// Information about a registered pack
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct RegisteredPack {
|
||||
/// Pack reference
|
||||
pub pack_ref: String,
|
||||
/// Pack database ID
|
||||
pub pack_id: i64,
|
||||
/// Pack version
|
||||
pub pack_version: String,
|
||||
/// Permanent storage path
|
||||
pub storage_path: String,
|
||||
/// Registered components by type
|
||||
pub components_registered: ComponentCounts,
|
||||
/// Test results
|
||||
pub test_result: Option<TestResult>,
|
||||
/// Validation results
|
||||
pub validation_results: ValidationResults,
|
||||
}
|
||||
|
||||
/// Component counts
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct ComponentCounts {
|
||||
/// Number of actions
|
||||
pub actions: usize,
|
||||
/// Number of sensors
|
||||
pub sensors: usize,
|
||||
/// Number of triggers
|
||||
pub triggers: usize,
|
||||
/// Number of rules
|
||||
pub rules: usize,
|
||||
/// Number of workflows
|
||||
pub workflows: usize,
|
||||
/// Number of policies
|
||||
pub policies: usize,
|
||||
}
|
||||
|
||||
/// Test result
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct TestResult {
|
||||
/// Test status
|
||||
pub status: String,
|
||||
/// Total number of tests
|
||||
pub total_tests: usize,
|
||||
/// Number passed
|
||||
pub passed: usize,
|
||||
/// Number failed
|
||||
pub failed: usize,
|
||||
}
|
||||
|
||||
/// Validation results
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct ValidationResults {
|
||||
/// Whether validation passed
|
||||
pub valid: bool,
|
||||
/// Validation errors
|
||||
pub errors: Vec<String>,
|
||||
}
|
||||
|
||||
/// Failed pack registration
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct FailedPackRegistration {
|
||||
/// Pack reference
|
||||
pub pack_ref: String,
|
||||
/// Pack path
|
||||
pub pack_path: String,
|
||||
/// Error message
|
||||
pub error: String,
|
||||
/// Error stage
|
||||
pub error_stage: String,
|
||||
}
|
||||
|
||||
/// Registration summary
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct RegistrationSummary {
|
||||
/// Total packs processed
|
||||
pub total_packs: usize,
|
||||
/// Successfully registered
|
||||
pub success_count: usize,
|
||||
/// Failed registrations
|
||||
pub failure_count: usize,
|
||||
/// Total components registered
|
||||
pub total_components: usize,
|
||||
/// Duration in milliseconds
|
||||
pub duration_ms: u64,
|
||||
}
|
||||
|
||||
fn default_empty_object() -> JsonValue {
|
||||
serde_json::json!({})
|
||||
}
|
||||
|
||||
fn default_download_timeout() -> u64 {
|
||||
300
|
||||
}
|
||||
|
||||
fn default_build_timeout() -> u64 {
|
||||
600
|
||||
}
|
||||
|
||||
fn default_python_version() -> String {
|
||||
"3.11".to_string()
|
||||
}
|
||||
|
||||
fn default_nodejs_version() -> String {
|
||||
"20".to_string()
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -69,6 +69,10 @@ pub async fn create_execution(
|
||||
.parameters
|
||||
.as_ref()
|
||||
.and_then(|p| serde_json::from_value(p.clone()).ok()),
|
||||
env_vars: request
|
||||
.env_vars
|
||||
.as_ref()
|
||||
.and_then(|e| serde_json::from_value(e.clone()).ok()),
|
||||
parent: None,
|
||||
enforcement: None,
|
||||
executor: None,
|
||||
|
||||
@@ -23,9 +23,11 @@ use crate::{
|
||||
dto::{
|
||||
common::{PaginatedResponse, PaginationParams},
|
||||
pack::{
|
||||
CreatePackRequest, InstallPackRequest, PackInstallResponse, PackResponse, PackSummary,
|
||||
BuildPackEnvsRequest, BuildPackEnvsResponse, CreatePackRequest, DownloadPacksRequest,
|
||||
DownloadPacksResponse, GetPackDependenciesRequest, GetPackDependenciesResponse,
|
||||
InstallPackRequest, PackInstallResponse, PackResponse, PackSummary,
|
||||
PackWorkflowSyncResponse, PackWorkflowValidationResponse, RegisterPackRequest,
|
||||
UpdatePackRequest, WorkflowSyncResult,
|
||||
RegisterPacksRequest, RegisterPacksResponse, UpdatePackRequest, WorkflowSyncResult,
|
||||
},
|
||||
ApiResponse, SuccessResponse,
|
||||
},
|
||||
@@ -307,7 +309,7 @@ async fn execute_and_store_pack_tests(
|
||||
pack_version: &str,
|
||||
trigger_type: &str,
|
||||
) -> Result<attune_common::models::pack_test::PackTestResult, ApiError> {
|
||||
use attune_worker::{TestConfig, TestExecutor};
|
||||
use attune_common::test_executor::{TestConfig, TestExecutor};
|
||||
use serde_yaml_ng;
|
||||
|
||||
// Load pack.yaml from filesystem
|
||||
@@ -1036,7 +1038,7 @@ pub async fn test_pack(
|
||||
RequireAuth(_user): RequireAuth,
|
||||
Path(pack_ref): Path<String>,
|
||||
) -> ApiResult<impl IntoResponse> {
|
||||
use attune_worker::{TestConfig, TestExecutor};
|
||||
use attune_common::test_executor::{TestConfig, TestExecutor};
|
||||
use serde_yaml_ng;
|
||||
|
||||
// Get pack from database
|
||||
@@ -1202,11 +1204,547 @@ pub async fn get_pack_latest_test(
|
||||
/// Note: Nested resource routes (e.g., /packs/:ref/actions) are defined
|
||||
/// in their respective modules (actions.rs, triggers.rs, rules.rs) to avoid
|
||||
/// route conflicts and maintain proper separation of concerns.
|
||||
/// Download packs from various sources
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/packs/download",
|
||||
tag = "packs",
|
||||
request_body = DownloadPacksRequest,
|
||||
responses(
|
||||
(status = 200, description = "Packs downloaded", body = ApiResponse<DownloadPacksResponse>),
|
||||
(status = 400, description = "Invalid request"),
|
||||
),
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn download_packs(
|
||||
State(state): State<Arc<AppState>>,
|
||||
RequireAuth(_user): RequireAuth,
|
||||
Json(request): Json<DownloadPacksRequest>,
|
||||
) -> ApiResult<Json<ApiResponse<DownloadPacksResponse>>> {
|
||||
use attune_common::pack_registry::PackInstaller;
|
||||
|
||||
// Create temp directory
|
||||
let temp_dir = std::env::temp_dir().join("attune-pack-downloads");
|
||||
std::fs::create_dir_all(&temp_dir)
|
||||
.map_err(|e| ApiError::InternalServerError(format!("Failed to create temp dir: {}", e)))?;
|
||||
|
||||
// Create installer
|
||||
let registry_config = if state.config.pack_registry.enabled {
|
||||
Some(state.config.pack_registry.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let installer = PackInstaller::new(&temp_dir, registry_config)
|
||||
.await
|
||||
.map_err(|e| ApiError::InternalServerError(format!("Failed to create installer: {}", e)))?;
|
||||
|
||||
let mut downloaded = Vec::new();
|
||||
let mut failed = Vec::new();
|
||||
|
||||
for source in &request.packs {
|
||||
let pack_source = detect_pack_source(source, request.ref_spec.as_deref())?;
|
||||
let source_type_str = get_source_type(&pack_source).to_string();
|
||||
|
||||
match installer.install(pack_source).await {
|
||||
Ok(installed) => {
|
||||
// Read pack.yaml
|
||||
let pack_yaml_path = installed.path.join("pack.yaml");
|
||||
if let Ok(content) = std::fs::read_to_string(&pack_yaml_path) {
|
||||
if let Ok(yaml) = serde_yaml_ng::from_str::<serde_yaml_ng::Value>(&content) {
|
||||
let pack_ref = yaml
|
||||
.get("ref")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
let pack_version = yaml
|
||||
.get("version")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("0.0.0")
|
||||
.to_string();
|
||||
|
||||
downloaded.push(crate::dto::pack::DownloadedPack {
|
||||
source: source.clone(),
|
||||
source_type: source_type_str.clone(),
|
||||
pack_path: installed.path.to_string_lossy().to_string(),
|
||||
pack_ref,
|
||||
pack_version,
|
||||
git_commit: None,
|
||||
checksum: installed.checksum,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
failed.push(crate::dto::pack::FailedPack {
|
||||
source: source.clone(),
|
||||
error: e.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let response = DownloadPacksResponse {
|
||||
success_count: downloaded.len(),
|
||||
failure_count: failed.len(),
|
||||
total_count: request.packs.len(),
|
||||
downloaded_packs: downloaded,
|
||||
failed_packs: failed,
|
||||
};
|
||||
|
||||
Ok(Json(ApiResponse::new(response)))
|
||||
}
|
||||
|
||||
/// Get pack dependencies
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/packs/dependencies",
|
||||
tag = "packs",
|
||||
request_body = GetPackDependenciesRequest,
|
||||
responses(
|
||||
(status = 200, description = "Dependencies analyzed", body = ApiResponse<GetPackDependenciesResponse>),
|
||||
(status = 400, description = "Invalid request"),
|
||||
),
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn get_pack_dependencies(
|
||||
State(state): State<Arc<AppState>>,
|
||||
RequireAuth(_user): RequireAuth,
|
||||
Json(request): Json<GetPackDependenciesRequest>,
|
||||
) -> ApiResult<Json<ApiResponse<GetPackDependenciesResponse>>> {
|
||||
use attune_common::repositories::List;
|
||||
|
||||
let mut dependencies = Vec::new();
|
||||
let mut runtime_requirements = std::collections::HashMap::new();
|
||||
let mut analyzed_packs = Vec::new();
|
||||
let mut errors = Vec::new();
|
||||
|
||||
// Get installed packs
|
||||
let installed_packs_list = PackRepository::list(&state.db).await?;
|
||||
let installed_refs: std::collections::HashSet<String> =
|
||||
installed_packs_list.into_iter().map(|p| p.r#ref).collect();
|
||||
|
||||
for pack_path in &request.pack_paths {
|
||||
let pack_yaml_path = std::path::Path::new(pack_path).join("pack.yaml");
|
||||
|
||||
if !pack_yaml_path.exists() {
|
||||
errors.push(crate::dto::pack::DependencyError {
|
||||
pack_path: pack_path.clone(),
|
||||
error: "pack.yaml not found".to_string(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
let content = match std::fs::read_to_string(&pack_yaml_path) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
errors.push(crate::dto::pack::DependencyError {
|
||||
pack_path: pack_path.clone(),
|
||||
error: format!("Failed to read pack.yaml: {}", e),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let yaml: serde_yaml_ng::Value = match serde_yaml_ng::from_str(&content) {
|
||||
Ok(y) => y,
|
||||
Err(e) => {
|
||||
errors.push(crate::dto::pack::DependencyError {
|
||||
pack_path: pack_path.clone(),
|
||||
error: format!("Failed to parse pack.yaml: {}", e),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let pack_ref = yaml
|
||||
.get("ref")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
|
||||
// Extract dependencies
|
||||
let mut dep_count = 0;
|
||||
if let Some(deps) = yaml.get("dependencies").and_then(|d| d.as_sequence()) {
|
||||
for dep in deps {
|
||||
if let Some(dep_str) = dep.as_str() {
|
||||
let parts: Vec<&str> = dep_str.splitn(2, '@').collect();
|
||||
let dep_ref = parts[0].to_string();
|
||||
let version_spec = parts.get(1).unwrap_or(&"*").to_string();
|
||||
let already_installed = installed_refs.contains(&dep_ref);
|
||||
|
||||
dependencies.push(crate::dto::pack::PackDependency {
|
||||
pack_ref: dep_ref.clone(),
|
||||
version_spec: version_spec.clone(),
|
||||
required_by: pack_ref.clone(),
|
||||
already_installed,
|
||||
});
|
||||
dep_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract runtime requirements
|
||||
let mut runtime_req = crate::dto::pack::RuntimeRequirements {
|
||||
pack_ref: pack_ref.clone(),
|
||||
python: None,
|
||||
nodejs: None,
|
||||
};
|
||||
|
||||
if let Some(python_ver) = yaml.get("python").and_then(|v| v.as_str()) {
|
||||
let req_file = std::path::Path::new(pack_path).join("requirements.txt");
|
||||
runtime_req.python = Some(crate::dto::pack::PythonRequirements {
|
||||
version: Some(python_ver.to_string()),
|
||||
requirements_file: if req_file.exists() {
|
||||
Some(req_file.to_string_lossy().to_string())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(nodejs_ver) = yaml.get("nodejs").and_then(|v| v.as_str()) {
|
||||
let pkg_file = std::path::Path::new(pack_path).join("package.json");
|
||||
runtime_req.nodejs = Some(crate::dto::pack::NodeJsRequirements {
|
||||
version: Some(nodejs_ver.to_string()),
|
||||
package_file: if pkg_file.exists() {
|
||||
Some(pkg_file.to_string_lossy().to_string())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if runtime_req.python.is_some() || runtime_req.nodejs.is_some() {
|
||||
runtime_requirements.insert(pack_ref.clone(), runtime_req);
|
||||
}
|
||||
|
||||
analyzed_packs.push(crate::dto::pack::AnalyzedPack {
|
||||
pack_ref: pack_ref.clone(),
|
||||
pack_path: pack_path.clone(),
|
||||
has_dependencies: dep_count > 0,
|
||||
dependency_count: dep_count,
|
||||
});
|
||||
}
|
||||
|
||||
let missing_dependencies: Vec<_> = dependencies
|
||||
.iter()
|
||||
.filter(|d| !d.already_installed)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
let response = GetPackDependenciesResponse {
|
||||
dependencies,
|
||||
runtime_requirements,
|
||||
missing_dependencies,
|
||||
analyzed_packs,
|
||||
errors,
|
||||
};
|
||||
|
||||
Ok(Json(ApiResponse::new(response)))
|
||||
}
|
||||
|
||||
/// Build pack environments
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/packs/build-envs",
|
||||
tag = "packs",
|
||||
request_body = BuildPackEnvsRequest,
|
||||
responses(
|
||||
(status = 200, description = "Environments built", body = ApiResponse<BuildPackEnvsResponse>),
|
||||
(status = 400, description = "Invalid request"),
|
||||
),
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn build_pack_envs(
|
||||
State(_state): State<Arc<AppState>>,
|
||||
RequireAuth(_user): RequireAuth,
|
||||
Json(request): Json<BuildPackEnvsRequest>,
|
||||
) -> ApiResult<Json<ApiResponse<BuildPackEnvsResponse>>> {
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
let mut built_environments = Vec::new();
|
||||
let mut failed_environments = Vec::new();
|
||||
let mut python_envs_built = 0;
|
||||
let mut nodejs_envs_built = 0;
|
||||
|
||||
for pack_path in &request.pack_paths {
|
||||
let pack_path_obj = Path::new(pack_path);
|
||||
let pack_start = std::time::Instant::now();
|
||||
|
||||
// Read pack.yaml to get pack_ref and runtime requirements
|
||||
let pack_yaml_path = pack_path_obj.join("pack.yaml");
|
||||
if !pack_yaml_path.exists() {
|
||||
failed_environments.push(crate::dto::pack::FailedEnvironment {
|
||||
pack_ref: "unknown".to_string(),
|
||||
pack_path: pack_path.clone(),
|
||||
runtime: "unknown".to_string(),
|
||||
error: "pack.yaml not found".to_string(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
let content = match std::fs::read_to_string(&pack_yaml_path) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
failed_environments.push(crate::dto::pack::FailedEnvironment {
|
||||
pack_ref: "unknown".to_string(),
|
||||
pack_path: pack_path.clone(),
|
||||
runtime: "unknown".to_string(),
|
||||
error: format!("Failed to read pack.yaml: {}", e),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let yaml: serde_yaml_ng::Value = match serde_yaml_ng::from_str(&content) {
|
||||
Ok(y) => y,
|
||||
Err(e) => {
|
||||
failed_environments.push(crate::dto::pack::FailedEnvironment {
|
||||
pack_ref: "unknown".to_string(),
|
||||
pack_path: pack_path.clone(),
|
||||
runtime: "unknown".to_string(),
|
||||
error: format!("Failed to parse pack.yaml: {}", e),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let pack_ref = yaml
|
||||
.get("ref")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
|
||||
let mut python_env = None;
|
||||
let mut nodejs_env = None;
|
||||
let mut has_error = false;
|
||||
|
||||
// Check for Python environment
|
||||
if !request.skip_python {
|
||||
if let Some(_python_ver) = yaml.get("python").and_then(|v| v.as_str()) {
|
||||
let requirements_file = pack_path_obj.join("requirements.txt");
|
||||
|
||||
if requirements_file.exists() {
|
||||
// Check if Python is available
|
||||
match Command::new("python3").arg("--version").output() {
|
||||
Ok(output) if output.status.success() => {
|
||||
let version_str = String::from_utf8_lossy(&output.stdout);
|
||||
let venv_path = pack_path_obj.join("venv");
|
||||
|
||||
// Check if venv exists or if force_rebuild is set
|
||||
if !venv_path.exists() || request.force_rebuild {
|
||||
tracing::info!(
|
||||
pack_ref = %pack_ref,
|
||||
"Python environment would be built here in production"
|
||||
);
|
||||
}
|
||||
|
||||
// Report environment status (detection mode)
|
||||
python_env = Some(crate::dto::pack::PythonEnvironment {
|
||||
virtualenv_path: venv_path.to_string_lossy().to_string(),
|
||||
requirements_installed: venv_path.exists(),
|
||||
package_count: 0, // Would count from pip freeze in production
|
||||
python_version: version_str.trim().to_string(),
|
||||
});
|
||||
python_envs_built += 1;
|
||||
}
|
||||
_ => {
|
||||
failed_environments.push(crate::dto::pack::FailedEnvironment {
|
||||
pack_ref: pack_ref.clone(),
|
||||
pack_path: pack_path.clone(),
|
||||
runtime: "python".to_string(),
|
||||
error: "Python 3 not available in system".to_string(),
|
||||
});
|
||||
has_error = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Node.js environment
|
||||
if !has_error && !request.skip_nodejs {
|
||||
if let Some(_nodejs_ver) = yaml.get("nodejs").and_then(|v| v.as_str()) {
|
||||
let package_file = pack_path_obj.join("package.json");
|
||||
|
||||
if package_file.exists() {
|
||||
// Check if Node.js is available
|
||||
match Command::new("node").arg("--version").output() {
|
||||
Ok(output) if output.status.success() => {
|
||||
let version_str = String::from_utf8_lossy(&output.stdout);
|
||||
let node_modules = pack_path_obj.join("node_modules");
|
||||
|
||||
// Check if node_modules exists or if force_rebuild is set
|
||||
if !node_modules.exists() || request.force_rebuild {
|
||||
tracing::info!(
|
||||
pack_ref = %pack_ref,
|
||||
"Node.js environment would be built here in production"
|
||||
);
|
||||
}
|
||||
|
||||
// Report environment status (detection mode)
|
||||
nodejs_env = Some(crate::dto::pack::NodeJsEnvironment {
|
||||
node_modules_path: node_modules.to_string_lossy().to_string(),
|
||||
dependencies_installed: node_modules.exists(),
|
||||
package_count: 0, // Would count from package.json in production
|
||||
nodejs_version: version_str.trim().to_string(),
|
||||
});
|
||||
nodejs_envs_built += 1;
|
||||
}
|
||||
_ => {
|
||||
failed_environments.push(crate::dto::pack::FailedEnvironment {
|
||||
pack_ref: pack_ref.clone(),
|
||||
pack_path: pack_path.clone(),
|
||||
runtime: "nodejs".to_string(),
|
||||
error: "Node.js not available in system".to_string(),
|
||||
});
|
||||
has_error = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !has_error && (python_env.is_some() || nodejs_env.is_some()) {
|
||||
built_environments.push(crate::dto::pack::BuiltEnvironment {
|
||||
pack_ref,
|
||||
pack_path: pack_path.clone(),
|
||||
environments: crate::dto::pack::Environments {
|
||||
python: python_env,
|
||||
nodejs: nodejs_env,
|
||||
},
|
||||
duration_ms: pack_start.elapsed().as_millis() as u64,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let success_count = built_environments.len();
|
||||
let failure_count = failed_environments.len();
|
||||
|
||||
let response = BuildPackEnvsResponse {
|
||||
built_environments,
|
||||
failed_environments,
|
||||
summary: crate::dto::pack::BuildSummary {
|
||||
total_packs: request.pack_paths.len(),
|
||||
success_count,
|
||||
failure_count,
|
||||
python_envs_built,
|
||||
nodejs_envs_built,
|
||||
total_duration_ms: start.elapsed().as_millis() as u64,
|
||||
},
|
||||
};
|
||||
|
||||
Ok(Json(ApiResponse::new(response)))
|
||||
}
|
||||
|
||||
/// Register multiple packs
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/packs/register-batch",
|
||||
tag = "packs",
|
||||
request_body = RegisterPacksRequest,
|
||||
responses(
|
||||
(status = 200, description = "Packs registered", body = ApiResponse<RegisterPacksResponse>),
|
||||
(status = 400, description = "Invalid request"),
|
||||
),
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn register_packs_batch(
|
||||
State(state): State<Arc<AppState>>,
|
||||
RequireAuth(user): RequireAuth,
|
||||
Json(request): Json<RegisterPacksRequest>,
|
||||
) -> ApiResult<Json<ApiResponse<RegisterPacksResponse>>> {
|
||||
let start = std::time::Instant::now();
|
||||
let mut registered = Vec::new();
|
||||
let mut failed = Vec::new();
|
||||
let total_components = 0;
|
||||
|
||||
for pack_path in &request.pack_paths {
|
||||
// Call the existing register_pack_internal function
|
||||
let register_req = crate::dto::pack::RegisterPackRequest {
|
||||
path: pack_path.clone(),
|
||||
force: request.force,
|
||||
skip_tests: request.skip_tests,
|
||||
};
|
||||
|
||||
match register_pack_internal(
|
||||
state.clone(),
|
||||
user.claims.sub.clone(),
|
||||
register_req.path.clone(),
|
||||
register_req.force,
|
||||
register_req.skip_tests,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(pack_id) => {
|
||||
// Fetch pack details
|
||||
if let Ok(Some(pack)) = PackRepository::find_by_id(&state.db, pack_id).await {
|
||||
// Count components (simplified)
|
||||
registered.push(crate::dto::pack::RegisteredPack {
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
pack_id,
|
||||
pack_version: pack.version.clone(),
|
||||
storage_path: format!("{}/{}", state.config.packs_base_dir, pack.r#ref),
|
||||
components_registered: crate::dto::pack::ComponentCounts {
|
||||
actions: 0,
|
||||
sensors: 0,
|
||||
triggers: 0,
|
||||
rules: 0,
|
||||
workflows: 0,
|
||||
policies: 0,
|
||||
},
|
||||
test_result: None,
|
||||
validation_results: crate::dto::pack::ValidationResults {
|
||||
valid: true,
|
||||
errors: Vec::new(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
failed.push(crate::dto::pack::FailedPackRegistration {
|
||||
pack_ref: "unknown".to_string(),
|
||||
pack_path: pack_path.clone(),
|
||||
error: e.to_string(),
|
||||
error_stage: "registration".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let response = RegisterPacksResponse {
|
||||
registered_packs: registered.clone(),
|
||||
failed_packs: failed.clone(),
|
||||
summary: crate::dto::pack::RegistrationSummary {
|
||||
total_packs: request.pack_paths.len(),
|
||||
success_count: registered.len(),
|
||||
failure_count: failed.len(),
|
||||
total_components,
|
||||
duration_ms: start.elapsed().as_millis() as u64,
|
||||
},
|
||||
};
|
||||
|
||||
Ok(Json(ApiResponse::new(response)))
|
||||
}
|
||||
|
||||
pub fn routes() -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
.route("/packs", get(list_packs).post(create_pack))
|
||||
.route("/packs/register", axum::routing::post(register_pack))
|
||||
.route(
|
||||
"/packs/register-batch",
|
||||
axum::routing::post(register_packs_batch),
|
||||
)
|
||||
.route("/packs/install", axum::routing::post(install_pack))
|
||||
.route("/packs/download", axum::routing::post(download_packs))
|
||||
.route(
|
||||
"/packs/dependencies",
|
||||
axum::routing::post(get_pack_dependencies),
|
||||
)
|
||||
.route("/packs/build-envs", axum::routing::post(build_pack_envs))
|
||||
.route(
|
||||
"/packs/{ref}",
|
||||
get(get_pack).put(update_pack).delete(delete_pack),
|
||||
|
||||
Reference in New Issue
Block a user