497 lines
No EOL
17 KiB
Rust
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
|
|
}
|
|
}
|