artifacts!
This commit is contained in:
@@ -17,6 +17,7 @@ use attune_common::auth::jwt::{generate_execution_token, JwtConfig};
|
||||
use attune_common::error::{Error, Result};
|
||||
use attune_common::models::runtime::RuntimeExecutionConfig;
|
||||
use attune_common::models::{runtime::Runtime as RuntimeModel, Action, Execution, ExecutionStatus};
|
||||
use attune_common::repositories::artifact::{ArtifactRepository, ArtifactVersionRepository};
|
||||
use attune_common::repositories::execution::{ExecutionRepository, UpdateExecutionInput};
|
||||
use attune_common::repositories::runtime_version::RuntimeVersionRepository;
|
||||
use attune_common::repositories::{FindById, Update};
|
||||
@@ -42,6 +43,7 @@ pub struct ActionExecutor {
|
||||
max_stdout_bytes: usize,
|
||||
max_stderr_bytes: usize,
|
||||
packs_base_dir: PathBuf,
|
||||
artifacts_dir: PathBuf,
|
||||
api_url: String,
|
||||
jwt_config: JwtConfig,
|
||||
}
|
||||
@@ -67,6 +69,7 @@ impl ActionExecutor {
|
||||
max_stdout_bytes: usize,
|
||||
max_stderr_bytes: usize,
|
||||
packs_base_dir: PathBuf,
|
||||
artifacts_dir: PathBuf,
|
||||
api_url: String,
|
||||
jwt_config: JwtConfig,
|
||||
) -> Self {
|
||||
@@ -79,6 +82,7 @@ impl ActionExecutor {
|
||||
max_stdout_bytes,
|
||||
max_stderr_bytes,
|
||||
packs_base_dir,
|
||||
artifacts_dir,
|
||||
api_url,
|
||||
jwt_config,
|
||||
}
|
||||
@@ -142,6 +146,15 @@ impl ActionExecutor {
|
||||
// Don't fail the execution just because artifact storage failed
|
||||
}
|
||||
|
||||
// Finalize file-backed artifacts (stat files on disk and update size_bytes)
|
||||
if let Err(e) = self.finalize_file_artifacts(execution_id).await {
|
||||
warn!(
|
||||
"Failed to finalize file-backed artifacts for execution {}: {}",
|
||||
execution_id, e
|
||||
);
|
||||
// Don't fail the execution just because artifact finalization failed
|
||||
}
|
||||
|
||||
// Update execution with result
|
||||
let is_success = result.is_success();
|
||||
debug!(
|
||||
@@ -291,6 +304,10 @@ impl ActionExecutor {
|
||||
env.insert("ATTUNE_EXEC_ID".to_string(), execution.id.to_string());
|
||||
env.insert("ATTUNE_ACTION".to_string(), execution.action_ref.clone());
|
||||
env.insert("ATTUNE_API_URL".to_string(), self.api_url.clone());
|
||||
env.insert(
|
||||
"ATTUNE_ARTIFACTS_DIR".to_string(),
|
||||
self.artifacts_dir.to_string_lossy().to_string(),
|
||||
);
|
||||
|
||||
// Generate execution-scoped API token.
|
||||
// The identity that triggered the execution is derived from the `sub` claim
|
||||
@@ -657,6 +674,95 @@ impl ActionExecutor {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Finalize file-backed artifacts after execution completes.
|
||||
///
|
||||
/// Scans all artifact versions linked to this execution that have a `file_path`,
|
||||
/// stats each file on disk, and updates `size_bytes` on both the version row
|
||||
/// and the parent artifact row.
|
||||
async fn finalize_file_artifacts(&self, execution_id: i64) -> Result<()> {
|
||||
let versions =
|
||||
ArtifactVersionRepository::find_file_versions_by_execution(&self.pool, execution_id)
|
||||
.await?;
|
||||
|
||||
if versions.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!(
|
||||
"Finalizing {} file-backed artifact version(s) for execution {}",
|
||||
versions.len(),
|
||||
execution_id,
|
||||
);
|
||||
|
||||
// Track the latest version per artifact so we can update parent size_bytes
|
||||
let mut latest_size_per_artifact: HashMap<i64, (i32, i64)> = HashMap::new();
|
||||
|
||||
for ver in &versions {
|
||||
let file_path = match &ver.file_path {
|
||||
Some(fp) => fp,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let full_path = self.artifacts_dir.join(file_path);
|
||||
let size_bytes = match tokio::fs::metadata(&full_path).await {
|
||||
Ok(metadata) => metadata.len() as i64,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Could not stat artifact file '{}' for version {}: {}. Setting size_bytes=0.",
|
||||
full_path.display(),
|
||||
ver.id,
|
||||
e,
|
||||
);
|
||||
0
|
||||
}
|
||||
};
|
||||
|
||||
// Update the version row
|
||||
if let Err(e) =
|
||||
ArtifactVersionRepository::update_size_bytes(&self.pool, ver.id, size_bytes).await
|
||||
{
|
||||
warn!(
|
||||
"Failed to update size_bytes for artifact version {}: {}",
|
||||
ver.id, e,
|
||||
);
|
||||
}
|
||||
|
||||
// Track the highest version number per artifact for parent update
|
||||
let entry = latest_size_per_artifact
|
||||
.entry(ver.artifact)
|
||||
.or_insert((ver.version, size_bytes));
|
||||
if ver.version > entry.0 {
|
||||
*entry = (ver.version, size_bytes);
|
||||
}
|
||||
|
||||
debug!(
|
||||
"Finalized artifact version {} (artifact {}): file='{}', size={}",
|
||||
ver.id, ver.artifact, file_path, size_bytes,
|
||||
);
|
||||
}
|
||||
|
||||
// Update parent artifact size_bytes to reflect the latest version's size
|
||||
for (artifact_id, (_version, size_bytes)) in &latest_size_per_artifact {
|
||||
if let Err(e) =
|
||||
ArtifactRepository::update_size_bytes(&self.pool, *artifact_id, *size_bytes).await
|
||||
{
|
||||
warn!(
|
||||
"Failed to update size_bytes for artifact {}: {}",
|
||||
artifact_id, e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
"Finalized file-backed artifacts for execution {}: {} version(s), {} artifact(s)",
|
||||
execution_id,
|
||||
versions.len(),
|
||||
latest_size_per_artifact.len(),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle successful execution
|
||||
async fn handle_execution_success(
|
||||
&self,
|
||||
|
||||
@@ -136,7 +136,7 @@ impl WorkerService {
|
||||
// Initialize worker registration
|
||||
let registration = Arc::new(RwLock::new(WorkerRegistration::new(pool.clone(), &config)));
|
||||
|
||||
// Initialize artifact manager
|
||||
// Initialize artifact manager (legacy, for stdout/stderr log storage)
|
||||
let artifact_base_dir = std::path::PathBuf::from(
|
||||
config
|
||||
.worker
|
||||
@@ -148,6 +148,22 @@ impl WorkerService {
|
||||
let artifact_manager = ArtifactManager::new(artifact_base_dir);
|
||||
artifact_manager.initialize().await?;
|
||||
|
||||
// Initialize artifacts directory for file-backed artifact storage (shared volume).
|
||||
// Execution processes write artifact files here; the API serves them from the same path.
|
||||
let artifacts_dir = std::path::PathBuf::from(&config.artifacts_dir);
|
||||
if let Err(e) = tokio::fs::create_dir_all(&artifacts_dir).await {
|
||||
warn!(
|
||||
"Failed to create artifacts directory '{}': {}. File-backed artifacts may not work.",
|
||||
artifacts_dir.display(),
|
||||
e,
|
||||
);
|
||||
} else {
|
||||
info!(
|
||||
"Artifacts directory initialized at: {}",
|
||||
artifacts_dir.display()
|
||||
);
|
||||
}
|
||||
|
||||
let packs_base_dir = std::path::PathBuf::from(&config.packs_base_dir);
|
||||
let runtime_envs_dir = std::path::PathBuf::from(&config.runtime_envs_dir);
|
||||
|
||||
@@ -304,6 +320,7 @@ impl WorkerService {
|
||||
max_stdout_bytes,
|
||||
max_stderr_bytes,
|
||||
packs_base_dir.clone(),
|
||||
artifacts_dir,
|
||||
api_url,
|
||||
jwt_config,
|
||||
));
|
||||
|
||||
Reference in New Issue
Block a user