Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ name = "trustify"
path = "src/main.rs"

[dependencies]
trustify-module-fundamental = { workspace = true }

anyhow = { workspace = true }
clap = { workspace = true, features = ["derive", "env"] }
dotenvy = { workspace = true }
Expand Down
15 changes: 15 additions & 0 deletions cli/src/api/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,21 @@ impl ApiClient {
.await
}

/// Perform a DELETE request with query parameters and retry logic
pub async fn delete_with_query<T: serde::Serialize + ?Sized + Sync>(
&self,
path: &str,
query: &T,
) -> Result<String, ApiError> {
self.execute_with_retry(|| async {
let url = self.url(path);
let request = self.client.delete(&url).query(query);
let response = self.authorize(request).await.send().await?;
self.handle_response(response).await
})
.await
}

/// Execute a request with retry logic for timeouts and token refresh
async fn execute_with_retry<F, Fut>(&self, f: F) -> Result<String, ApiError>
where
Expand Down
53 changes: 20 additions & 33 deletions cli/src/api/sbom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use log;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::sync::Mutex;
use trustify_module_fundamental::sbom::model::DeleteContext;

use super::client::{ApiClient, ApiError};
use crate::common::{
Expand All @@ -20,6 +21,7 @@ use crate::common::{
};

const SBOM_PATH: &str = "/v2/sbom";
const PRUNE_PATH: &str = "/v3/prune";

/// Parameters for find duplicates
pub struct FindDuplicatesParams {
Comment on lines +24 to 27
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): CLI prune endpoints use /v3/prune while the server exposes /v3/sbom/prune.

This mismatch will cause 404s for list_prunable and prune_prunable. Please update PRUNE_PATH to "/v3/sbom/prune" so the CLI matches the server and OpenAPI spec.

Expand Down Expand Up @@ -55,6 +57,16 @@ pub async fn list(client: &ApiClient, params: &ListParams) -> Result<String, Api
client.get_with_query(SBOM_PATH, params).await
}

/// List SBOMs to be pruned - returns raw JSON
pub async fn list_prunable(client: &ApiClient, params: &ListParams) -> Result<String, ApiError> {
client.get_with_query(PRUNE_PATH, params).await
}

/// Prune SBOMs - returns raw JSON
pub async fn prune_prunable(client: &ApiClient, params: &ListParams) -> Result<String, ApiError> {
client.delete_with_query(PRUNE_PATH, params).await
}

/// Fetch a single page and extract SBOM entries
async fn fetch_page(
client: &ApiClient,
Expand Down Expand Up @@ -365,41 +377,16 @@ pub async fn prune(client: &ApiClient, params: &PruneParams) -> Result<DeleteRes
list_params.sort
);

// Get list of SBOMs matching the criteria
let response = list(client, &list_params).await?;
let parsed: Value = serde_json::from_str(&response)
.map_err(|e| ApiError::InternalError(format!("Failed to parse response: {}", e)))?;

let items = parsed
.get("items")
.and_then(|v| v.as_array())
.ok_or_else(|| ApiError::InternalError("No items in response".to_string()))?;

let total = items.len() as u32;

// Convert items to delete entries
let entries: Vec<DeleteEntry> = items
.iter()
.filter_map(|item| {
let id = item.get("id").and_then(|v| v.as_str())?;
let document_id = item
.get("document_id")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
Some(DeleteEntry {
id: id.to_string(),
identifier: document_id.to_string(),
})
})
.collect();
let response = if params.dry_run {
list_prunable(client, &list_params).await?
} else {
prune_prunable(client, &list_params).await?
};

// If dry run, just return the count without deleting
if params.dry_run {
return Ok(new_delete_result(total));
}
let parsed: DeleteContext = serde_json::from_str(&response)
.map_err(|e| ApiError::InternalError(format!("Failed to parse response: {}", e)))?;

// Perform the actual deletion
delete_list(client, entries, params.concurrency).await
Ok(parsed.into())
}

/// Read delete entries from a file
Expand Down
51 changes: 51 additions & 0 deletions cli/src/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use futures::stream::{self, StreamExt};
use indicatif::{ProgressBar, ProgressStyle};
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
use trustify_module_fundamental::sbom::model::{DeletableSbomState, DeleteContext};

use crate::api::client::{ApiClient, ApiError};

Expand Down Expand Up @@ -108,6 +109,56 @@ pub struct FailedResult {
pub error: String,
}

impl From<DeleteContext> for DeleteResult {
fn from(value: DeleteContext) -> Self {
let sboms = value.sboms.unwrap_or(vec![]);

let mut deleted = vec![];
let mut skipped = vec![];
let mut failed = vec![];

for sbom in &sboms {
let id = sbom.head.sbom_id.to_string();
let identifier = sbom
.head
.document_id
.clone()
.unwrap_or("unknown".to_string());
match &sbom.state {
DeletableSbomState::Deleted(_) => {
deleted.push(DeletedResult { id, identifier });
}
DeletableSbomState::Skipped => {
skipped.push(SkippedResult { id, identifier });
}
DeletableSbomState::Failed(error) => {
failed.push(FailedResult {
id,
identifier,
error: error.clone(),
});
}
_ => (),
}
}

let deleted_total = deleted.len() as u32;
let skipped_total = skipped.len() as u32;
let failed_total = failed.len() as u32;
let total = sboms.len() as u32;

Self {
deleted,
deleted_total,
skipped,
skipped_total,
failed,
failed_total,
total,
}
}
}

pub async fn delete_entries(
client: &ApiClient,
base_path: &str,
Expand Down
Loading
Loading