Skip to content

Commit

Permalink
Update project management endpoints (#1442)
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 authored Aug 31, 2024
1 parent 56496cd commit 2682fa3
Show file tree
Hide file tree
Showing 11 changed files with 256 additions and 103 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Added

- Organization management under the `phylum org` subcommand
- `phylum project update --default-label` option to set a project's default label
- `phylum project list --no-group` flag to only show personal projects

## 6.6.6 - 2024-07-12

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
106 changes: 69 additions & 37 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,10 +26,11 @@ use crate::auth::{
};
use crate::config::{AuthInfo, Config};
use crate::types::{
AddOrgUserRequest, AnalysisPackageDescriptor, HistoryJob, ListUserGroupsResponse,
OrgMembersResponse, OrgsResponse, PackageSpecifier, PackageSubmitResponse, PingResponse,
PolicyEvaluationRequest, PolicyEvaluationResponse, PolicyEvaluationResponseRaw,
RevokeTokenRequest, SubmitPackageRequest, UserToken,
AddOrgUserRequest, AnalysisPackageDescriptor, CreateProjectRequest, GetProjectResponse,
HistoryJob, ListUserGroupsResponse, OrgMembersResponse, OrgsResponse, PackageSpecifier,
PackageSubmitResponse, Paginated, PingResponse, PolicyEvaluationRequest,
PolicyEvaluationResponse, PolicyEvaluationResponseRaw, ProjectListEntry, RevokeTokenRequest,
SubmitPackageRequest, UpdateProjectRequest, UserToken,
};

pub mod endpoints;
Expand Down Expand Up @@ -266,8 +264,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 @@ -279,32 +282,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 @@ -395,7 +437,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 @@ -405,19 +447,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 @@ -189,6 +189,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 @@ -203,6 +207,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::dirs;
use crate::permissions::{self, Permission};
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 2682fa3

Please sign in to comment.