Skip to content

Commit

Permalink
Update project management endpoints
Browse files Browse the repository at this point in the history
This patch updates the endpoints used for the project management
subcommands to avoid the soon-to-be deprecated overview endpoint.

The `phylum project update` subcommand has been updated to make use of
the new `default_label` field, which can now be updated.

The `phylum project list` subcommand now lists all projects by default,
including group projects. To still allow listing only non-group projects
the new `--no-group` flag has been added to this subcommand
specifically. The `repository_url` has been removed from the command
output, since it is not included in the API response.

The group name has been removed from `phylum project status`, since the
new endpoint used by the subcommand does not return this information.
However the `default_label` field was added instead.

Closes #1439.
  • Loading branch information
cd-work committed Aug 29, 2024
1 parent 1229114 commit c1ab104
Show file tree
Hide file tree
Showing 10 changed files with 249 additions and 99 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Added

- PNPM v5 lockfile support
- `phylum project update --default-label`

### Fixed

Expand Down
26 changes: 6 additions & 20 deletions cli/src/api/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,25 +97,18 @@ pub fn get_group_project_history(
Ok(url)
}

/// GET /data/projects/overview
pub fn get_project_summary(api_uri: &str) -> Result<Url, BaseUriError> {
Ok(get_api_path(api_uri)?.join("data/projects/overview")?)
/// GET /projects
pub fn projects(api_uri: &str) -> Result<Url, BaseUriError> {
Ok(get_api_path(api_uri)?.join("projects")?)
}

/// POST /data/projects
pub fn post_create_project(api_uri: &str) -> Result<Url, BaseUriError> {
pub fn create_project(api_uri: &str) -> Result<Url, BaseUriError> {
Ok(get_api_path(api_uri)?.join("data/projects")?)
}

/// PUT /data/projects/<project_id>
pub fn update_project(api_uri: &str, project_id: &str) -> Result<Url, BaseUriError> {
let mut url = get_api_path(api_uri)?;
url.path_segments_mut().unwrap().pop_if_empty().extend(["data", "projects", project_id]);
Ok(url)
}

/// DELETE /data/projects/<project_id>
pub fn delete_project(api_uri: &str, project_id: &str) -> Result<Url, BaseUriError> {
/// GET/PUT/DELETE /data/projects/<project_id>
pub fn project(api_uri: &str, project_id: &str) -> Result<Url, BaseUriError> {
let mut url = get_api_path(api_uri)?;
url.path_segments_mut().unwrap().pop_if_empty().extend(["data", "projects", project_id]);
Ok(url)
Expand All @@ -138,13 +131,6 @@ pub(crate) fn group_delete(api_uri: &str, group: &str) -> Result<Url, BaseUriErr
Ok(url)
}

/// GET /groups/<groupName>/projects
pub fn group_project_summary(api_uri: &str, group: &str) -> Result<Url, BaseUriError> {
let mut url = get_api_path(api_uri)?;
url.path_segments_mut().unwrap().pop_if_empty().extend(["groups", group, "projects"]);
Ok(url)
}

/// POST/DELETE /groups/<groupName>/members/<userEmail>
pub fn group_usermod(api_uri: &str, group: &str, user: &str) -> Result<Url, BaseUriError> {
let mut url = get_api_path(api_uri)?;
Expand Down
104 changes: 68 additions & 36 deletions cli/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,12 @@ use phylum_types::types::group::{
};
use phylum_types::types::job::{AllJobsStatusResponse, SubmitPackageResponse};
use phylum_types::types::package::PackageDescriptor;
use phylum_types::types::project::{
CreateProjectRequest, CreateProjectResponse, ProjectSummaryResponse, UpdateProjectRequest,
};
use phylum_types::types::project::CreateProjectResponse;
use reqwest::header::{HeaderMap, HeaderValue};
use reqwest::{Client, IntoUrl, Method, StatusCode};
use serde::de::{DeserializeOwned, IgnoredAny};
use serde::{Deserialize, Serialize};
use thiserror::Error as ThisError;
use uuid::Uuid;
#[cfg(feature = "vulnreach")]
use vulnreach_types::{Job, Vulnerability};

Expand All @@ -29,9 +26,10 @@ use crate::auth::{
};
use crate::config::{AuthInfo, Config};
use crate::types::{
AnalysisPackageDescriptor, HistoryJob, ListUserGroupsResponse, PackageSpecifier,
PackageSubmitResponse, PingResponse, PolicyEvaluationRequest, PolicyEvaluationResponse,
PolicyEvaluationResponseRaw, RevokeTokenRequest, SubmitPackageRequest, UserToken,
AnalysisPackageDescriptor, CreateProjectRequest, GetProjectResponse, HistoryJob,
ListUserGroupsResponse, PackageSpecifier, PackageSubmitResponse, Paginated, PingResponse,
PolicyEvaluationRequest, PolicyEvaluationResponse, PolicyEvaluationResponseRaw,
ProjectListEntry, RevokeTokenRequest, SubmitPackageRequest, UpdateProjectRequest, UserToken,
};

pub mod endpoints;
Expand Down Expand Up @@ -265,8 +263,13 @@ impl PhylumApi {
group: Option<String>,
repository_url: Option<String>,
) -> Result<ProjectId> {
let url = endpoints::post_create_project(&self.config.connection.uri)?;
let body = CreateProjectRequest { repository_url, name: name.into(), group_name: group };
let url = endpoints::create_project(&self.config.connection.uri)?;
let body = CreateProjectRequest {
repository_url,
default_label: None,
group_name: group,
name: name.into(),
};
let response: CreateProjectResponse = self.post(url, body).await?;
Ok(response.id)
}
Expand All @@ -278,32 +281,71 @@ impl PhylumApi {
group: Option<String>,
name: impl Into<String>,
repository_url: Option<String>,
default_label: Option<String>,
) -> Result<ProjectId> {
let url = endpoints::update_project(&self.config.connection.uri, project_id)?;
let body = UpdateProjectRequest { repository_url, name: name.into(), group_name: group };
let url = endpoints::project(&self.config.connection.uri, project_id)?;
let body = UpdateProjectRequest {
repository_url,
default_label,
name: name.into(),
group_name: group,
};
let response: CreateProjectResponse = self.put(url, body).await?;
Ok(response.id)
}

/// Delete a project
pub async fn delete_project(&self, project_id: ProjectId) -> Result<()> {
let _: IgnoredAny = self
.delete(endpoints::delete_project(
&self.config.connection.uri,
&format!("{project_id}"),
)?)
.delete(endpoints::project(&self.config.connection.uri, &project_id.to_string())?)
.await?;
Ok(())
}

/// Get a list of projects
pub async fn get_projects(&self, group: Option<&str>) -> Result<Vec<ProjectSummaryResponse>> {
let uri = match group {
Some(group) => endpoints::group_project_summary(&self.config.connection.uri, group)?,
None => endpoints::get_project_summary(&self.config.connection.uri)?,
};
/// Get all projects.
///
/// If a group is passed, only projects of that group will be returned.
/// Otherwise all projects, including group projects, will be returned.
///
/// The project name filter does not require an exact match, it is
/// equivalent to filtering with [`str::contains`].
pub async fn get_projects(
&self,
group: Option<&str>,
name_filter: Option<&str>,
) -> Result<Vec<ProjectListEntry>> {
let mut uri = endpoints::projects(&self.config.connection.uri)?;

// Add filter query parameters.
if let Some(group) = group {
uri.query_pairs_mut().append_pair("filter.group", group);
}
if let Some(name_filter) = name_filter {
uri.query_pairs_mut().append_pair("filter.name", name_filter);
}

self.get(uri).await
// Set maximum pagination size, since we want everything anyway.
uri.query_pairs_mut().append_pair("paginate.limit", "100");

let mut projects: Vec<ProjectListEntry> = Vec::new();
loop {
// Update the pagination cursor point.
let mut uri = uri.clone();
if let Some(project) = projects.last() {
uri.query_pairs_mut().append_pair("paginate.cursor", &project.id.to_string());
}

// Get next page of projects.
let mut page: Paginated<ProjectListEntry> = self.get(uri).await?;
projects.append(&mut page.values);

// Keep paginating until there's nothing left.
if !page.has_more {
break;
}
}

Ok(projects)
}

/// Submit a new request to the system
Expand Down Expand Up @@ -394,7 +436,7 @@ impl PhylumApi {
project_name: &str,
group_name: Option<&str>,
) -> Result<ProjectId> {
let projects = self.get_projects(group_name).await?;
let projects = self.get_projects(group_name, Some(project_name)).await?;

projects
.iter()
Expand All @@ -404,19 +446,9 @@ impl PhylumApi {
}

/// Get a project using its ID and group name.
pub async fn get_project(
&self,
project_id: &str,
group_name: Option<&str>,
) -> Result<ProjectSummaryResponse> {
let project_id = Uuid::parse_str(project_id).map_err(|err| anyhow!(err))?;

let projects = self.get_projects(group_name).await?;

projects
.into_iter()
.find(|project| project.id == project_id)
.ok_or_else(|| anyhow!("No project found with ID {:?}", project_id).into())
pub async fn get_project(&self, project_id: &str) -> Result<GetProjectResponse> {
let url = endpoints::project(&self.config.connection.uri, project_id)?;
self.get(url).await
}

/// Submit a single package
Expand Down
9 changes: 9 additions & 0 deletions cli/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ pub fn add_subcommands(command: Command) -> Command {
.long("repository-url")
.value_name("REPOSITORY_URL")
.help("New repository URL"),
Arg::new("default-label")
.short('l')
.long("default-label")
.help("Default project label"),
]),
)
.subcommand(
Expand All @@ -191,6 +195,11 @@ pub fn add_subcommands(command: Command) -> Command {
.long("group")
.value_name("GROUP_NAME")
.help("Group to list projects for"),
Arg::new("no-group")
.action(ArgAction::SetTrue)
.long("no-group")
.help("Exclude all group projects from the output")
.conflicts_with("group"),
]),
)
.subcommand(
Expand Down
8 changes: 4 additions & 4 deletions cli/src/commands/extensions/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ use phylum_project::ProjectConfig;
use phylum_types::types::auth::{AccessToken, RefreshToken};
use phylum_types::types::common::{JobId, ProjectId};
use phylum_types::types::package::{PackageDescriptor, PackageDescriptorAndLockfile};
use phylum_types::types::project::ProjectSummaryResponse;
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};

Expand All @@ -37,7 +36,8 @@ use crate::config::Config;
use crate::dirs;
use crate::types::{
AnalysisPackageDescriptor, ListUserGroupsResponse, Package, PackageSpecifier,
PackageSubmitResponse, PolicyEvaluationResponse, PolicyEvaluationResponseRaw, PurlWithOrigin,
PackageSubmitResponse, PolicyEvaluationResponse, PolicyEvaluationResponseRaw, ProjectListEntry,
PurlWithOrigin,
};

/// Package format accepted by extension API.
Expand Down Expand Up @@ -329,11 +329,11 @@ async fn get_groups(op_state: Rc<RefCell<OpState>>) -> Result<ListUserGroupsResp
async fn get_projects(
op_state: Rc<RefCell<OpState>>,
#[string] group: Option<String>,
) -> Result<Vec<ProjectSummaryResponse>> {
) -> Result<Vec<ProjectListEntry>> {
let state = ExtensionState::from(op_state);
let api = state.api().await?;

api.get_projects(group.as_deref()).await.map_err(Error::from)
api.get_projects(group.as_deref(), None).await.map_err(Error::from)
}

#[derive(Serialize)]
Expand Down
Loading

0 comments on commit c1ab104

Please sign in to comment.