initial-commit

This commit is contained in:
Guillaume GRABÉ
2026-05-12 17:06:43 +02:00
commit 051a080dfa
110 changed files with 26377 additions and 0 deletions
@@ -0,0 +1,3 @@
mod project;
pub use project::ProjectHandler;
@@ -0,0 +1,172 @@
use anyhow::Result;
use async_trait::async_trait;
use charybdis::charybdis::entities::{entity, Entity};
use charybdis::database::EntityRepository;
use charybdis::plugins::ResourceHandler;
use std::collections::HashMap;
use std::sync::Arc;
use tracing::{info, warn};
use crate::{DependencyTrackClient, DependencyTrackConfig};
/// Project handler — maps Component entities to Dependency-Track Projects
pub struct ProjectHandler {
client: DependencyTrackClient,
repository: Arc<EntityRepository>,
config: DependencyTrackConfig,
trigger_kinds: Vec<String>,
}
impl ProjectHandler {
pub fn new(
client: DependencyTrackClient,
repository: Arc<EntityRepository>,
config: DependencyTrackConfig,
) -> Self {
Self {
client,
repository,
config,
trigger_kinds: vec!["Component".to_string()],
}
}
/// Get project UUID from entity annotations
fn get_project_uuid(&self, entity: &Entity) -> Option<String> {
entity
.annotations
.get("dependencytrack.com/project-uuid")
.cloned()
}
/// Extract metadata fields from a Component entity
fn extract_component_fields(&self, entity: &Entity) -> (String, String, Vec<String>) {
let mut name = entity.id.clone();
let mut description = String::new();
let mut tags = Vec::new();
if let Some(entity::Metadata::ComponentMetadata(m)) = &entity.metadata {
name = m.name.clone();
description = m.description.clone();
tags.extend(m.tags.clone());
}
if let Some(entity::Spec::ComponentSpec(s)) = &entity.spec {
if !s.lifecycle.is_empty() {
tags.push(format!("lifecycle:{}", s.lifecycle));
}
if !s.r#type.is_empty() {
tags.push(format!("type:{}", s.r#type));
}
if !s.owner.is_empty() {
tags.push(format!("owner:{}", s.owner));
}
}
(name, description, tags)
}
/// Extract version from entity (defaults to "latest")
fn extract_version(&self, entity: &Entity) -> String {
// Check annotations first
if let Some(version) = entity.annotations.get("app.kubernetes.io/version") {
return version.clone();
}
if let Some(version) = entity.annotations.get("version") {
return version.clone();
}
"latest".to_string()
}
}
#[async_trait]
impl ResourceHandler for ProjectHandler {
fn resource_type(&self) -> &str {
"dependencytrack_project"
}
fn trigger_kinds(&self) -> &[String] {
&self.trigger_kinds
}
fn creates_entity_kind(&self) -> &str {
""
}
async fn handle_create(&self, entity: &Entity) -> Result<Option<Entity>> {
let (name, description, tags) = self.extract_component_fields(entity);
let version = self.extract_version(entity);
let project_uuid = self
.client
.create_project(&name, &version, &description, &tags)
.await?;
// Store project UUID in annotations (annotation-only update, no double-write)
let mut annotations = HashMap::new();
annotations.insert(
"dependencytrack.com/project-uuid".to_string(),
project_uuid.clone(),
);
self.repository
.update_annotations(&entity.id, annotations)
.await?;
info!(
"Linked entity {} to Dependency-Track project {}",
entity.id, project_uuid
);
Ok(None)
}
async fn handle_update(&self, entity: &Entity) -> Result<()> {
if let Some(project_uuid) = self.get_project_uuid(entity) {
let (name, description, tags) = self.extract_component_fields(entity);
let version = self.extract_version(entity);
self.client
.update_project(&project_uuid, &name, &version, &description, &tags)
.await?;
} else {
warn!(
"Entity {} has no Dependency-Track project UUID — creating one",
entity.id
);
let (name, description, tags) = self.extract_component_fields(entity);
let version = self.extract_version(entity);
let project_uuid = self
.client
.create_project(&name, &version, &description, &tags)
.await?;
let mut annotations = HashMap::new();
annotations.insert(
"dependencytrack.com/project-uuid".to_string(),
project_uuid,
);
self.repository
.update_annotations(&entity.id, annotations)
.await?;
}
Ok(())
}
async fn handle_delete(&self, entity: &Entity) -> Result<()> {
if let Some(project_uuid) = self.get_project_uuid(entity) {
self.client.delete_project(&project_uuid).await?;
} else {
warn!(
"Entity {} has no Dependency-Track project UUID — skipping deletion",
entity.id
);
}
Ok(())
}
}
+162
View File
@@ -0,0 +1,162 @@
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use charybdis::database::EntityRepository;
use charybdis::plugins::http_client::{AuthConfig, PluginHttpClient};
use charybdis::plugins::{EventDrivenPlugin, Plugin, PluginConfig, PluginType, ResourceHandler};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tracing::info;
/// Dependency-Track plugin configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DependencyTrackConfig {
/// Dependency-Track API base URL
pub base_url: String,
/// API key for authentication
pub api_key: String,
/// Request timeout in seconds
#[serde(default = "default_timeout")]
pub timeout_secs: u64,
/// Auto-create projects for new Component entities
#[serde(default = "default_auto_create_project")]
pub auto_create_project: bool,
/// Default team UUID to assign to new projects (optional)
#[serde(default)]
pub default_team_uuid: Option<String>,
/// Field mappings for each resource type
#[serde(default)]
pub field_mappings: HashMap<String, HashMap<String, serde_json::Value>>,
}
fn default_timeout() -> u64 {
30
}
fn default_auto_create_project() -> bool {
true
}
impl DependencyTrackConfig {
/// Load Dependency-Track configuration from TOML config map
pub fn from_toml(config: &HashMap<String, toml::Value>) -> Result<Self> {
// Convert HashMap to TOML Value
let value = toml::Value::Table(config.clone().into_iter().collect());
// Deserialize to DependencyTrackConfig
let config: DependencyTrackConfig = value
.try_into()
.map_err(|e| anyhow!("Failed to parse Dependency-Track configuration: {}", e))?;
Ok(config)
}
}
/// Dependency-Track API client (wrapper around PluginHttpClient)
#[derive(Clone)]
pub struct DependencyTrackClient {
client: PluginHttpClient,
}
impl DependencyTrackClient {
pub fn new(base_url: String, api_key: String) -> Result<Self> {
let client = PluginHttpClient::new(base_url, AuthConfig::api_key("X-Api-Key", api_key))?;
Ok(Self { client })
}
/// Make a GET request to Dependency-Track API
pub async fn get(&self, path: &str) -> Result<serde_json::Value> {
self.client.get(path).await
}
/// Make a POST request to Dependency-Track API
pub async fn post(&self, path: &str, body: &serde_json::Value) -> Result<serde_json::Value> {
self.client.post(path, body).await
}
/// Make a PUT request to Dependency-Track API
pub async fn put(&self, path: &str, body: &serde_json::Value) -> Result<serde_json::Value> {
self.client.put(path, body).await
}
/// Make a DELETE request to Dependency-Track API
pub async fn delete(&self, path: &str) -> Result<()> {
self.client.delete(path).await
}
}
/// Dependency-Track event-driven plugin
#[allow(dead_code)]
pub struct DependencyTrackPlugin {
config: DependencyTrackConfig,
client: DependencyTrackClient,
repository: Arc<EntityRepository>,
handlers: Vec<Arc<dyn ResourceHandler>>,
}
impl DependencyTrackPlugin {
pub fn new(config: DependencyTrackConfig, repository: Arc<EntityRepository>) -> Result<Self> {
// Validate configuration
if config.base_url.is_empty() {
return Err(anyhow!("Dependency-Track base_url cannot be empty"));
}
if config.api_key.is_empty() {
return Err(anyhow!("Dependency-Track api_key cannot be empty"));
}
let client = DependencyTrackClient::new(config.base_url.clone(), config.api_key.clone())?;
// Resource handlers will be registered here as they are implemented
let handlers: Vec<Arc<dyn ResourceHandler>> = vec![];
info!(
"Dependency-Track plugin initialized (base_url={})",
config.base_url
);
Ok(Self {
config,
client,
repository,
handlers,
})
}
}
#[async_trait]
impl Plugin for DependencyTrackPlugin {
fn name(&self) -> &str {
"dependencytrack"
}
fn plugin_type(&self) -> PluginType {
PluginType::EventDriven
}
fn load_config(&mut self, _config: PluginConfig) -> Result<()> {
// Configuration is loaded during construction
Ok(())
}
fn validate_config(&self) -> Result<()> {
if self.config.base_url.is_empty() {
return Err(anyhow!("Dependency-Track base_url cannot be empty"));
}
if self.config.api_key.is_empty() {
return Err(anyhow!("Dependency-Track api_key cannot be empty"));
}
Ok(())
}
}
#[async_trait]
impl EventDrivenPlugin for DependencyTrackPlugin {
fn resource_handlers(&self) -> Vec<Arc<dyn ResourceHandler>> {
self.handlers.clone()
}
}