Public Access
initial-commit
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "charybdis-dependencytrack-plugin"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "charybdis_dependencytrack"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
charybdis = { path = "../.." }
|
||||
anyhow = "1.0"
|
||||
async-trait = "0.1"
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
toml = "0.8"
|
||||
tokio = { version = "1.48", features = ["full"] }
|
||||
tracing = "0.1"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.4"
|
||||
@@ -0,0 +1,23 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package charybdis.plugins.dependencytrack;
|
||||
|
||||
// Dependency-Track plugin metadata
|
||||
message DependencytrackMetadata {
|
||||
string name = 1;
|
||||
string description = 2;
|
||||
|
||||
// Resource type (project)
|
||||
string resource_type = 3;
|
||||
}
|
||||
|
||||
// Dependency-Track plugin spec
|
||||
message DependencytrackSpec {
|
||||
// Sync status information
|
||||
string sync_status = 1;
|
||||
string last_sync = 2;
|
||||
string error_message = 3;
|
||||
|
||||
// Configuration stored as JSON
|
||||
string config_json = 4;
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user