Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement coffee nurse Handler for Locally Missing Repository #197

Merged
merged 5 commits into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions coffee_cmd/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,16 @@ async fn main() -> Result<(), CoffeeError> {
}
Err(err) => Err(err),
},
CoffeeCommand::Nurse {} => {
term::info!("Nurse command is not implemented");
Ok(())
}
CoffeeCommand::Nurse {} => match coffee.nurse().await {
Ok(val) => {
// For every status in CoffeeNurse, print it
for status in val.status {
term::success!("{}", status.to_string());
}
Ok(())
}
Err(err) => Err(err),
tareknaser marked this conversation as resolved.
Show resolved Hide resolved
},
};

if let Err(err) = result {
Expand Down
74 changes: 71 additions & 3 deletions coffee_core/src/coffee.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//! Coffee mod implementation
use coffee_storage::nosql_db::NoSQlStorage;
tareknaser marked this conversation as resolved.
Show resolved Hide resolved

use std::collections::HashMap;
use std::fmt::Debug;
use std::vec::Vec;
Expand All @@ -23,6 +23,7 @@ use coffee_lib::types::response::*;
use coffee_lib::url::URL;
use coffee_lib::{commit_id, error, get_repo_info, sh};
use coffee_storage::model::repository::{Kind, Repository as RepositoryInfo};
use coffee_storage::nosql_db::NoSQlStorage;
use coffee_storage::storage::StorageManager;

use super::config;
Expand Down Expand Up @@ -78,6 +79,11 @@ pub struct CoffeeManager {
}

impl CoffeeManager {
/// return the repos of the plugin manager
pub fn repos(&self) -> &HashMap<String, Box<dyn Repository + Send + Sync>> {
&self.repos
}
tareknaser marked this conversation as resolved.
Show resolved Hide resolved

pub async fn new(conf: &dyn CoffeeArgs) -> Result<Self, CoffeeError> {
let conf = CoffeeConf::new(conf).await?;
let mut coffee = CoffeeManager {
Expand Down Expand Up @@ -442,8 +448,70 @@ impl PluginManager for CoffeeManager {
Err(err)
}

async fn nurse(&mut self) -> Result<(), CoffeeError> {
self.recovery_strategies.scan().await
async fn nurse(&mut self) -> Result<CoffeeNurse, CoffeeError> {
let status = self.recovery_strategies.scan(self).await?;
let mut nurse_actions: Vec<NurseStatus> = vec![];
for defect in status.defects.iter() {
log::debug!("defect: {:?}", defect);
match defect {
Defect::RepositoryLocallyAbsent(repos) => {
tareknaser marked this conversation as resolved.
Show resolved Hide resolved
let mut actions = self.patch_repository_locally_absent(repos.to_vec()).await?;
nurse_actions.append(&mut actions);
}
}
}

// If there was no actions taken by nurse, we return a sane status.
if nurse_actions.is_empty() {
nurse_actions.push(NurseStatus::Sane);
}
tareknaser marked this conversation as resolved.
Show resolved Hide resolved
Ok(CoffeeNurse {
status: nurse_actions,
})
}

async fn patch_repository_locally_absent(
&mut self,
repos: Vec<String>,
) -> Result<Vec<NurseStatus>, CoffeeError> {
// initialize the nurse actions
let mut nurse_actions: Vec<NurseStatus> = vec![];
// for every repository that is absent locally
// we try to recover it.
// There are 2 cases:
// 1. the repository can be recovered from the remote
// 2. the repository can't be recovered from the remote. In this case
// we remove the repository from the coffee configuration.
for repo_name in repos.iter() {
// Get the repository from the name
let repo = self
.repos
.get_mut(repo_name)
.ok_or_else(|| error!("repository with name: {repo_name} not found"))?;
tareknaser marked this conversation as resolved.
Show resolved Hide resolved

match repo.recover().await {
Ok(_) => {
log::info!("repository {} recovered", repo_name.clone());
nurse_actions.push(NurseStatus::RepositoryLocallyRestored(vec![
repo_name.clone()
]));
}
Err(err) => {
log::debug!(
"error while recovering repository {}: {err}",
tareknaser marked this conversation as resolved.
Show resolved Hide resolved
repo_name.clone()
tareknaser marked this conversation as resolved.
Show resolved Hide resolved
);
log::info!("removing repository {}", repo_name.clone());
self.repos.remove(repo_name);
log::debug!("remote removed: {}", repo_name);
self.flush().await?;
nurse_actions.push(NurseStatus::RepositoryLocallyRemoved(vec![
repo_name.clone()
]));
}
}
}
Ok(nurse_actions)
}
}

Expand Down
31 changes: 21 additions & 10 deletions coffee_core/src/nurse/chain.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Nurse Chain of Responsibility rust implementation
//!
//! If you do not know what Chain Of Responsibility patter
//! If you do not know what Chain Of Responsibility pattern
//! is, here is a small description:
//!
//! > Chain of Responsibility is behavioral design pattern
Expand All @@ -14,7 +14,7 @@
//! > a standard handler interface.
//!
//! In our case we do not need to handle a request, but we should
//! handler through the various recovery strategy to see what can
//! handler through the various recovery strategies to see what can
//! be applied.
//!
//! So in our case the handler is a specific recovery strategy
Expand All @@ -31,33 +31,44 @@ use std::sync::Arc;
use async_trait::async_trait;

use coffee_lib::errors::CoffeeError;
use coffee_lib::types::response::{ChainOfResponsibilityStatus, Defect};

use super::strategy::RecoveryStrategy;
use super::strategy::GitRepositoryLocallyAbsentStrategy;
use crate::coffee::CoffeeManager;

#[async_trait]
pub trait Handler: Send + Sync {
async fn can_be_apply(
async fn can_be_applied(
self: Arc<Self>,
) -> Result<Option<Arc<dyn RecoveryStrategy>>, CoffeeError>;
coffee: &CoffeeManager,
) -> Result<Option<Defect>, CoffeeError>;
}

pub struct RecoveryChainOfResponsibility {
pub handlers: Vec<Arc<dyn Handler>>,
}

impl RecoveryChainOfResponsibility {
/// Create a new instance of the chain of responsibility
pub async fn new() -> Result<Self, CoffeeError> {
Ok(Self {
handlers: Vec::new(),
handlers: vec![Arc::new(GitRepositoryLocallyAbsentStrategy)],
})
}

pub async fn scan(&self) -> Result<(), CoffeeError> {
/// Scan the chain of responsibility to see what can be applied
/// and return the status of the chain of responsibility
/// with the list of defects
pub async fn scan(
&self,
coffee: &CoffeeManager,
) -> Result<ChainOfResponsibilityStatus, CoffeeError> {
let mut defects: Vec<Defect> = vec![];
for handler in self.handlers.iter() {
if let Some(strategy) = handler.clone().can_be_apply().await? {
strategy.patch().await?;
if let Some(defect) = handler.clone().can_be_applied(coffee).await? {
defects.push(defect);
}
}
Ok(())
Ok(ChainOfResponsibilityStatus { defects })
}
}
tareknaser marked this conversation as resolved.
Show resolved Hide resolved
57 changes: 40 additions & 17 deletions coffee_core/src/nurse/strategy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,33 +21,56 @@
//! be able to choose the algorithm at runtime.
//!
//! Author: Vincenzo Palazzo <[email protected]>
use std::path::Path;
use std::sync::Arc;

use async_trait::async_trait;

use coffee_lib::errors::CoffeeError;
use coffee_lib::types::response::Defect;

use crate::coffee::CoffeeManager;
use crate::nurse::chain::Handler;

#[async_trait]
pub trait RecoveryStrategy: Send + Sync {
async fn patch(&self) -> Result<(), CoffeeError>;
}

pub struct GitRepositoryMissedStrategy;

#[async_trait]
impl RecoveryStrategy for GitRepositoryMissedStrategy {
async fn patch(&self) -> Result<(), CoffeeError> {
unimplemented!()
}
}
/// Strategy for handling the situation when a Git repository exists in coffee configuration
/// but is absent from the local storage.
///
/// This strategy is invoked when a Git repository is documented in the coffee configuration but
/// is not found in the local storage directory (./coffee/repositories).
/// The absence of the repository locally may be due to reasons such as accidental deletion or
/// a change in the storage location.
pub struct GitRepositoryLocallyAbsentStrategy;

#[async_trait]
impl Handler for GitRepositoryMissedStrategy {
async fn can_be_apply(
impl Handler for GitRepositoryLocallyAbsentStrategy {
/// Determines if a repository is missing from local storage.
///
/// This function iterates over the Git repositories listed in the coffee configuration and
/// checks if each one exists in the `.coffee/repositories` folder. If any repository is found
/// to be missing from local storage, it indicates that the strategy to handle
/// this situation should be applied.
async fn can_be_applied(
self: Arc<Self>,
) -> Result<Option<std::sync::Arc<dyn RecoveryStrategy>>, CoffeeError> {
Ok(Some(self))
coffee: &CoffeeManager,
) -> Result<Option<Defect>, CoffeeError> {
let mut repos: Vec<String> = Vec::new();
let coffee_repos = coffee.repos();
for repo in coffee_repos.values() {
log::debug!("Checking if repository {} exists locally", repo.name());
let repo_path = repo.url().path_string;
let repo_path = Path::new(&repo_path);
if !repo_path.exists() {
log::debug!("Repository {} is missing locally", repo.name());
repos.push(repo.name().to_string());
}
}

if repos.is_empty() {
log::debug!("No repositories missing locally");
return Ok(None);
} else {
log::debug!("Found {} repositories missing locally", repos.len());
return Ok(Some(Defect::RepositoryLocallyAbsent(repos)));
}
tareknaser marked this conversation as resolved.
Show resolved Hide resolved
}
}
40 changes: 40 additions & 0 deletions coffee_github/src/repository.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,46 @@ impl Repository for Github {
})
}

async fn recover(&mut self) -> Result<(), CoffeeError> {
let commit = self.git_head.clone();

debug!(
"recovering repository: {} {} > {}",
self.name, &self.url.url_string, &self.url.path_string,
);
tareknaser marked this conversation as resolved.
Show resolved Hide resolved
// recursively clone the repository
let res = git2::Repository::clone(&self.url.url_string, &self.url.path_string);
match res {
Ok(repo) => {
// get the commit id
let oid = git2::Oid::from_str(&commit.unwrap())
.map_err(|err| error!("{}", err.message()))?;
// Retrieve the commit associated with the OID
let target_commit = match repo.find_commit(oid) {
Ok(commit) => commit,
Err(err) => return Err(error!("{}", err.message())),
};

// Update HEAD to point to the target commit
repo.set_head_detached(target_commit.id())
.map_err(|err| error!("{}", err.message()))?;

// retrieve the submodules
let submodules = repo.submodules().unwrap_or_default();
for (_, sub) in submodules.iter().enumerate() {
let path =
format!("{}/{}", &self.url.path_string, sub.path().to_str().unwrap());
if let Err(err) = git2::Repository::clone(sub.url().unwrap(), &path) {
return Err(error!("{}", err.message()));
}
}

tareknaser marked this conversation as resolved.
Show resolved Hide resolved
Ok(())
}
Err(err) => Err(error!("{}", err.message())),
}
}

/// list of the plugin installed inside the repository.
async fn list(&self) -> Result<Vec<Plugin>, CoffeeError> {
Ok(self.plugins.clone())
Expand Down
9 changes: 8 additions & 1 deletion coffee_lib/src/plugin_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,12 @@ pub trait PluginManager {
async fn search(&mut self, plugin: &str) -> Result<CoffeeSearch, CoffeeError>;

/// clean up storage information about the remote repositories of the plugin manager.
async fn nurse(&mut self) -> Result<(), CoffeeError>;
async fn nurse(&mut self) -> Result<CoffeeNurse, CoffeeError>;

/// patch coffee configuration in the case that a repository is present in the coffee
/// configuration but is absent from the local storage.
async fn patch_repository_locally_absent(
&mut self,
repos: Vec<String>,
) -> Result<Vec<NurseStatus>, CoffeeError>;
}
3 changes: 3 additions & 0 deletions coffee_lib/src/repository.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ pub trait Repository: Any {
/// upgrade the repository
async fn upgrade(&mut self, plugins: &Vec<Plugin>) -> Result<CoffeeUpgrade, CoffeeError>;

/// recover the repository from the commit id.
async fn recover(&mut self) -> Result<(), CoffeeError>;

/// return the name of the repository.
fn name(&self) -> String;

Expand Down
48 changes: 48 additions & 0 deletions coffee_lib/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ pub mod request {
// Definition of the response types.
pub mod response {
use serde::{Deserialize, Serialize};
use std::fmt;
tareknaser marked this conversation as resolved.
Show resolved Hide resolved

use crate::plugin::Plugin;

Expand Down Expand Up @@ -139,4 +140,51 @@ pub mod response {
pub repository_url: String,
pub plugin: Plugin,
}

/// This struct is used to represent a defect
/// that can be patched by the nurse.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Defect {
// A patch operation when a git repository is present in the coffee configuration
// but is absent from the local storage.
RepositoryLocallyAbsent(Vec<String>),
// TODO: Add more patch operations
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ChainOfResponsibilityStatus {
pub defects: Vec<Defect>,
}

/// This struct is used to represent the status of nurse,
/// either sane or not.
/// If not sane, return the action that nurse has taken.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum NurseStatus {
Sane,
RepositoryLocallyRestored(Vec<String>),
RepositoryLocallyRemoved(Vec<String>),
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CoffeeNurse {
pub status: Vec<NurseStatus>,
}

impl fmt::Display for NurseStatus {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
NurseStatus::Sane => write!(
f,
"coffee configuration is not corrupt! No need to run coffee nurse"
),
NurseStatus::RepositoryLocallyRestored(val) => {
write!(f, "Repositories restored locally: {}", val.join(" "))
}
NurseStatus::RepositoryLocallyRemoved(val) => {
write!(f, "Repositories removed locally: {}", val.join(" "))
}
}
}
}
}
Loading
Loading