k8s-maestro 1.0.0

A Kubernetes job orchestrator tool library
Documentation
use anyhow::{Context, Result};
use sha2::{Digest, Sha256};
use std::fs;
use std::path::{Path, PathBuf};

#[derive(Debug, Clone)]
pub enum PackageSource {
    Git {
        url: String,
        branch: Option<String>,
        path: Option<String>,
    },
    RemotePath {
        url: String,
    },
    LocalPath {
        path: PathBuf,
    },
    Registry {
        registry: String,
        package_name: String,
        version: String,
    },
}

#[derive(Debug, Clone)]
pub struct PackageCache {
    cache_dir: PathBuf,
}

impl PackageCache {
    pub fn new() -> Result<Self> {
        let cache_dir = std::env::temp_dir().join("k8s-maestro").join("cache");
        fs::create_dir_all(&cache_dir).context("Failed to create cache directory")?;
        Ok(Self { cache_dir })
    }

    pub fn get_cache_path(&self, source: &PackageSource) -> PathBuf {
        let key = Self::generate_cache_key(source);
        self.cache_dir.join(key)
    }

    fn generate_cache_key(source: &PackageSource) -> String {
        let mut hasher = Sha256::new();
        match source {
            PackageSource::Git { url, branch, path } => {
                hasher.update(url.as_bytes());
                if let Some(b) = branch {
                    hasher.update(b.as_bytes());
                }
                if let Some(p) = path {
                    hasher.update(p.as_bytes());
                }
            }
            PackageSource::RemotePath { url } => {
                hasher.update(url.as_bytes());
            }
            PackageSource::LocalPath { path } => {
                if let Ok(canon) = fs::canonicalize(path) {
                    hasher.update(canon.to_string_lossy().as_bytes());
                } else {
                    hasher.update(path.to_string_lossy().as_bytes());
                }
            }
            PackageSource::Registry {
                registry,
                package_name,
                version,
            } => {
                hasher.update(registry.as_bytes());
                hasher.update(package_name.as_bytes());
                hasher.update(version.as_bytes());
            }
        }
        format!("{:x}", hasher.finalize())
    }
}

#[derive(Debug)]
pub struct PackageLoader {
    cache: PackageCache,
}

impl PackageLoader {
    pub fn new() -> Result<Self> {
        Ok(Self {
            cache: PackageCache::new()?,
        })
    }

    pub fn load(&self, source: &PackageSource) -> Result<PathBuf> {
        let cache_path = self.cache.get_cache_path(source);

        if cache_path.exists() {
            return Ok(cache_path);
        }

        match source {
            PackageSource::Git { url, branch, path } => {
                self.fetch_git(url, branch.as_deref(), path.as_deref())
            }
            PackageSource::RemotePath { url } => self.fetch_remote(url),
            PackageSource::LocalPath { path } => self.validate_local(path),
            PackageSource::Registry { .. } => {
                anyhow::bail!("Registry support not yet implemented")
            }
        }
    }

    fn fetch_git(&self, url: &str, _branch: Option<&str>, _path: Option<&str>) -> Result<PathBuf> {
        let cache_key = PackageCache::generate_cache_key(&PackageSource::Git {
            url: url.to_string(),
            branch: _branch.map(|s| s.to_string()),
            path: _path.map(|s| s.to_string()),
        });
        let clone_dir = self.cache.cache_dir.join(&cache_key);

        fs::create_dir_all(&clone_dir)?;

        let repo = git2::Repository::clone(url, &clone_dir)
            .with_context(|| format!("Failed to clone repository: {}", url))?;

        if let Some(branch) = _branch {
            let (obj, reference) = repo.revparse_ext(branch)?;
            repo.checkout_tree(&obj, None)?;
            repo.set_head(reference.unwrap().name().unwrap())?;
        }

        Ok(clone_dir)
    }

    fn fetch_remote(&self, url: &str) -> Result<PathBuf> {
        let cache_key = PackageCache::generate_cache_key(&PackageSource::RemotePath {
            url: url.to_string(),
        });
        let file_path = self.cache.cache_dir.join(&cache_key);

        if file_path.exists() {
            return Ok(file_path);
        }

        let response =
            reqwest::blocking::get(url).with_context(|| format!("Failed to fetch: {}", url))?;

        let bytes = response.bytes()?;
        fs::write(&file_path, bytes)?;

        Ok(file_path)
    }

    fn validate_local(&self, path: &Path) -> Result<PathBuf> {
        let canonical = path
            .canonicalize()
            .with_context(|| format!("Path does not exist: {:?}", path))?;

        if !canonical.exists() {
            anyhow::bail!("Path does not exist: {:?}", canonical);
        }

        Ok(canonical)
    }
}

impl Default for PackageLoader {
    fn default() -> Self {
        Self::new().unwrap()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_package_source_display() {
        let git_source = PackageSource::Git {
            url: "https://github.com/test/repo".to_string(),
            branch: Some("main".to_string()),
            path: Some("packages/core".to_string()),
        };
        assert!(matches!(git_source, PackageSource::Git { .. }));

        let remote_source = PackageSource::RemotePath {
            url: "https://example.com/package.tar.gz".to_string(),
        };
        assert!(matches!(remote_source, PackageSource::RemotePath { .. }));

        let local_source = PackageSource::LocalPath {
            path: PathBuf::from("/local/packages/core"),
        };
        assert!(matches!(local_source, PackageSource::LocalPath { .. }));

        let registry_source = PackageSource::Registry {
            registry: "https://registry.io".to_string(),
            package_name: "my-package".to_string(),
            version: "1.0.0".to_string(),
        };
        assert!(matches!(registry_source, PackageSource::Registry { .. }));
    }

    #[test]
    fn test_package_cache_key_generation() {
        let source1 = PackageSource::Git {
            url: "https://github.com/test/repo".to_string(),
            branch: Some("main".to_string()),
            path: Some("packages/core".to_string()),
        };
        let key1 = PackageCache::generate_cache_key(&source1);

        let source2 = PackageSource::Git {
            url: "https://github.com/test/repo".to_string(),
            branch: Some("main".to_string()),
            path: Some("packages/core".to_string()),
        };
        let key2 = PackageCache::generate_cache_key(&source2);

        assert_eq!(key1, key2);

        let source3 = PackageSource::Git {
            url: "https://github.com/test/repo".to_string(),
            branch: Some("develop".to_string()),
            path: None,
        };
        let key3 = PackageCache::generate_cache_key(&source3);

        assert_ne!(key1, key3);
    }

    #[test]
    fn test_package_cache_get_path() {
        let cache = PackageCache::new().unwrap();

        let source = PackageSource::RemotePath {
            url: "https://example.com/package.tar.gz".to_string(),
        };
        let path = cache.get_cache_path(&source);

        assert!(path.starts_with(cache.cache_dir));
        assert!(path.to_string_lossy().contains("k8s-maestro"));
    }
}