apt-ostree/src/ostree_commit_manager.rs

497 lines
No EOL
17 KiB
Rust

//! OSTree Commit Management for APT-OSTree
//!
//! This module implements OSTree commit management for package layering,
//! providing atomic operations, rollback support, and commit history tracking.
use std::path::{Path, PathBuf};
use tracing::{info, warn, debug};
use serde::{Serialize, Deserialize};
use chrono::{DateTime, Utc};
use crate::error::{AptOstreeError, AptOstreeResult};
use crate::apt_ostree_integration::DebPackageMetadata;
/// OSTree commit metadata
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OstreeCommitMetadata {
pub commit_id: String,
pub parent_commit: Option<String>,
pub timestamp: DateTime<Utc>,
pub subject: String,
pub body: String,
pub author: String,
pub packages_added: Vec<String>,
pub packages_removed: Vec<String>,
pub packages_modified: Vec<String>,
pub layer_level: usize,
pub deployment_type: DeploymentType,
pub checksum: String,
}
/// Deployment type
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DeploymentType {
Base,
PackageLayer,
SystemUpdate,
Rollback,
Custom,
}
/// OSTree commit manager
pub struct OstreeCommitManager {
repo_path: PathBuf,
branch_name: String,
current_commit: Option<String>,
commit_history: Vec<OstreeCommitMetadata>,
layer_counter: usize,
}
/// Commit creation options
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommitOptions {
pub subject: String,
pub body: Option<String>,
pub author: Option<String>,
pub layer_level: Option<usize>,
pub deployment_type: DeploymentType,
pub dry_run: bool,
}
/// Commit result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommitResult {
pub success: bool,
pub commit_id: Option<String>,
pub parent_commit: Option<String>,
pub metadata: Option<OstreeCommitMetadata>,
pub error_message: Option<String>,
}
impl Default for CommitOptions {
fn default() -> Self {
Self {
subject: "Package layer update".to_string(),
body: None,
author: Some("apt-ostree <apt-ostree@example.com>".to_string()),
layer_level: None,
deployment_type: DeploymentType::PackageLayer,
dry_run: false,
}
}
}
impl OstreeCommitManager {
/// Create a new OSTree commit manager
pub fn new(repo_path: PathBuf, branch_name: String) -> AptOstreeResult<Self> {
info!("Creating OSTree commit manager for branch: {} at {}", branch_name, repo_path.display());
// Ensure repository exists
if !repo_path.exists() {
return Err(AptOstreeError::OstreeError(
format!("OSTree repository not found: {}", repo_path.display())
));
}
Ok(Self {
repo_path,
branch_name,
current_commit: None,
commit_history: Vec::new(),
layer_counter: 0,
})
}
/// Initialize commit manager
pub async fn initialize(&mut self) -> AptOstreeResult<()> {
info!("Initializing OSTree commit manager");
// Get current commit
self.current_commit = self.get_current_commit().await?;
// Load commit history
self.load_commit_history().await?;
// Initialize layer counter
self.layer_counter = self.get_next_layer_level();
info!("OSTree commit manager initialized. Current commit: {:?}, Layer counter: {}",
self.current_commit, self.layer_counter);
Ok(())
}
/// Get current commit
pub async fn get_current_commit(&self) -> AptOstreeResult<Option<String>> {
let output = std::process::Command::new("ostree")
.args(&["rev-parse", &self.branch_name])
.current_dir(&self.repo_path)
.output();
match output {
Ok(output) => {
if output.status.success() {
let commit_id = String::from_utf8_lossy(&output.stdout).trim().to_string();
Ok(Some(commit_id))
} else {
warn!("No current commit found for branch: {}", self.branch_name);
Ok(None)
}
}
Err(e) => {
warn!("Failed to get current commit: {}", e);
Ok(None)
}
}
}
/// Load commit history
async fn load_commit_history(&mut self) -> AptOstreeResult<()> {
debug!("Loading commit history");
if let Some(current_commit) = &self.current_commit {
let output = std::process::Command::new("ostree")
.args(&["log", current_commit])
.current_dir(&self.repo_path)
.output();
if let Ok(output) = output {
if output.status.success() {
self.parse_commit_log(&output.stdout)?;
}
}
}
info!("Loaded {} commits from history", self.commit_history.len());
Ok(())
}
/// Parse commit log
fn parse_commit_log(&mut self, log_output: &[u8]) -> AptOstreeResult<()> {
let log_text = String::from_utf8_lossy(log_output);
let lines: Vec<&str> = log_text.lines().collect();
let mut current_commit: Option<OstreeCommitMetadata> = None;
for line in lines {
if line.starts_with("commit ") {
// Save previous commit if exists
if let Some(commit) = current_commit.take() {
self.commit_history.push(commit);
}
// Start new commit
let commit_id = line[7..].trim();
current_commit = Some(OstreeCommitMetadata {
commit_id: commit_id.to_string(),
parent_commit: None,
timestamp: Utc::now(),
subject: String::new(),
body: String::new(),
author: String::new(),
packages_added: Vec::new(),
packages_removed: Vec::new(),
packages_modified: Vec::new(),
layer_level: 0,
deployment_type: DeploymentType::Custom,
checksum: String::new(),
});
} else if let Some(ref mut commit) = current_commit {
if line.starts_with("Subject: ") {
commit.subject = line[9..].trim().to_string();
} else if line.starts_with("Author: ") {
commit.author = line[8..].trim().to_string();
} else if line.starts_with("Date: ") {
// Parse date if needed
} else if !line.is_empty() && !line.starts_with(" ") {
// Body content
commit.body.push_str(line);
commit.body.push('\n');
}
}
}
// Save last commit
if let Some(commit) = current_commit {
self.commit_history.push(commit);
}
Ok(())
}
/// Create a new commit with package changes
pub async fn create_package_commit(
&mut self,
packages_added: &[DebPackageMetadata],
packages_removed: &[String],
options: CommitOptions,
) -> AptOstreeResult<CommitResult> {
info!("Creating package commit with {} added, {} removed packages",
packages_added.len(), packages_removed.len());
if options.dry_run {
info!("DRY RUN: Would create commit with subject: {}", options.subject);
return Ok(CommitResult {
success: true,
commit_id: None,
parent_commit: self.current_commit.clone(),
metadata: None,
error_message: Some("Dry run mode".to_string()),
});
}
// Prepare commit metadata
let layer_level = options.layer_level.unwrap_or_else(|| {
self.layer_counter += 1;
self.layer_counter
});
let packages_added_names: Vec<String> = packages_added.iter()
.map(|pkg| pkg.name.clone())
.collect();
let metadata = OstreeCommitMetadata {
commit_id: String::new(), // Will be set after commit
parent_commit: self.current_commit.clone(),
timestamp: Utc::now(),
subject: options.subject,
body: options.body.unwrap_or_default(),
author: options.author.unwrap_or_else(|| "apt-ostree <apt-ostree@example.com>".to_string()),
packages_added: packages_added_names,
packages_removed: packages_removed.to_vec(),
packages_modified: Vec::new(),
layer_level,
deployment_type: options.deployment_type,
checksum: String::new(),
};
// Create OSTree commit
let commit_id = self.create_ostree_commit(&metadata).await?;
// Update metadata with commit ID
let mut final_metadata = metadata.clone();
final_metadata.commit_id = commit_id.clone();
// Add to history
self.commit_history.push(final_metadata.clone());
// Update current commit
self.current_commit = Some(commit_id.clone());
info!("Created package commit: {} (layer: {})", commit_id, layer_level);
Ok(CommitResult {
success: true,
commit_id: Some(commit_id),
parent_commit: metadata.parent_commit,
metadata: Some(final_metadata),
error_message: None,
})
}
/// Create OSTree commit
pub async fn create_ostree_commit(&self, metadata: &OstreeCommitMetadata) -> AptOstreeResult<String> {
debug!("Creating OSTree commit with subject: {}", metadata.subject);
// Prepare commit message
let commit_message = self.format_commit_message(metadata);
// Create temporary commit message file
let temp_dir = std::env::temp_dir();
let message_file = temp_dir.join(format!("apt-ostree-commit-{}.msg", chrono::Utc::now().timestamp()));
std::fs::write(&message_file, commit_message)?;
// Build ostree commit command
let mut cmd = std::process::Command::new("/usr/bin/ostree");
cmd.args(&["commit", "--branch", &self.branch_name]);
if let Some(parent) = &metadata.parent_commit {
cmd.args(&["--parent", parent]);
}
cmd.args(&["--body-file", message_file.to_str().unwrap()]);
cmd.current_dir(&self.repo_path);
// Execute commit
let output = cmd.output()
.map_err(|e| AptOstreeError::OstreeError(format!("Failed to create OSTree commit: {}", e)))?;
// Clean up message file
let _ = std::fs::remove_file(&message_file);
if !output.status.success() {
let error_msg = String::from_utf8_lossy(&output.stderr);
return Err(AptOstreeError::OstreeError(
format!("OSTree commit failed: {}", error_msg)
));
}
// Get commit ID from output
let commit_id = String::from_utf8_lossy(&output.stdout).trim().to_string();
Ok(commit_id)
}
/// Format commit message
fn format_commit_message(&self, metadata: &OstreeCommitMetadata) -> String {
let mut message = format!("{}\n\n", metadata.subject);
if !metadata.body.is_empty() {
message.push_str(&metadata.body);
message.push_str("\n\n");
}
message.push_str("Package Changes:\n");
if !metadata.packages_added.is_empty() {
message.push_str("Added:\n");
for package in &metadata.packages_added {
message.push_str(&format!(" + {}\n", package));
}
message.push('\n');
}
if !metadata.packages_removed.is_empty() {
message.push_str("Removed:\n");
for package in &metadata.packages_removed {
message.push_str(&format!(" - {}\n", package));
}
message.push('\n');
}
if !metadata.packages_modified.is_empty() {
message.push_str("Modified:\n");
for package in &metadata.packages_modified {
message.push_str(&format!(" ~ {}\n", package));
}
message.push('\n');
}
message.push_str(&format!("Layer Level: {}\n", metadata.layer_level));
message.push_str(&format!("Deployment Type: {:?}\n", metadata.deployment_type));
message.push_str(&format!("Timestamp: {}\n", metadata.timestamp));
message.push_str(&format!("Author: {}\n", metadata.author));
message
}
/// Rollback to previous commit
pub async fn rollback_to_commit(&mut self, commit_id: &str) -> AptOstreeResult<CommitResult> {
info!("Rolling back to commit: {}", commit_id);
// Verify commit exists
if !self.commit_exists(commit_id).await? {
return Err(AptOstreeError::OstreeError(
format!("Commit not found: {}", commit_id)
));
}
// Create rollback commit
let options = CommitOptions {
subject: format!("Rollback to commit {}", commit_id),
body: Some(format!("Rolling back from {} to {}",
self.current_commit.as_deref().unwrap_or("none"), commit_id)),
author: Some("apt-ostree <apt-ostree@example.com>".to_string()),
layer_level: Some(self.layer_counter + 1),
deployment_type: DeploymentType::Rollback,
dry_run: false,
};
let rollback_metadata = OstreeCommitMetadata {
commit_id: String::new(),
parent_commit: self.current_commit.clone(),
timestamp: Utc::now(),
subject: options.subject.clone(),
body: options.body.clone().unwrap_or_default(),
author: options.author.clone().unwrap_or_default(),
packages_added: Vec::new(),
packages_removed: Vec::new(),
packages_modified: Vec::new(),
layer_level: options.layer_level.unwrap_or(0),
deployment_type: DeploymentType::Rollback,
checksum: String::new(),
};
// Create rollback commit
let new_commit_id = self.create_ostree_commit(&rollback_metadata).await?;
// Update current commit
self.current_commit = Some(new_commit_id.clone());
// Add to history
let parent_commit = rollback_metadata.parent_commit.clone();
let mut final_metadata = rollback_metadata;
final_metadata.commit_id = new_commit_id.clone();
self.commit_history.push(final_metadata.clone());
info!("Rollback completed to commit: {}", new_commit_id);
Ok(CommitResult {
success: true,
commit_id: Some(new_commit_id),
parent_commit,
metadata: Some(final_metadata),
error_message: None,
})
}
/// Check if commit exists
async fn commit_exists(&self, commit_id: &str) -> AptOstreeResult<bool> {
let output = std::process::Command::new("/usr/bin/ostree")
.args(&["show", commit_id])
.current_dir(&self.repo_path)
.output();
match output {
Ok(output) => Ok(output.status.success()),
Err(_) => Ok(false),
}
}
/// Get commit history
pub fn get_commit_history(&self) -> &[OstreeCommitMetadata] {
&self.commit_history
}
/// Get next layer level
fn get_next_layer_level(&self) -> usize {
self.commit_history.iter()
.map(|commit| commit.layer_level)
.max()
.unwrap_or(0) + 1
}
/// Get commits by layer level
pub fn get_commits_by_layer(&self, layer_level: usize) -> Vec<&OstreeCommitMetadata> {
self.commit_history.iter()
.filter(|commit| commit.layer_level == layer_level)
.collect()
}
/// Get commits by deployment type
pub fn get_commits_by_type(&self, deployment_type: &DeploymentType) -> Vec<&OstreeCommitMetadata> {
self.commit_history.iter()
.filter(|commit| std::mem::discriminant(&commit.deployment_type) == std::mem::discriminant(deployment_type))
.collect()
}
/// Get commit metadata
pub fn get_commit_metadata(&self, commit_id: &str) -> Option<&OstreeCommitMetadata> {
self.commit_history.iter()
.find(|commit| commit.commit_id == commit_id)
}
/// Get repository path
pub fn get_repo_path(&self) -> &Path {
&self.repo_path
}
/// Get branch name
pub fn get_branch_name(&self) -> &str {
&self.branch_name
}
/// Get layer counter
pub fn get_layer_counter(&self) -> usize {
self.layer_counter
}
}