Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

buffrs add now accept dependency with no version and defaults to latest #259

Merged
merged 3 commits into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
50 changes: 41 additions & 9 deletions src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,12 @@ pub async fn init(kind: Option<PackageType>, name: Option<PackageName>) -> miett
struct DependencyLocator {
repository: String,
package: PackageName,
version: VersionReq,
version: DependencyLocatorVersion,
}

enum DependencyLocatorVersion {
Version(VersionReq),
Latest,
}

impl FromStr for DependencyLocator {
Expand All @@ -103,18 +108,24 @@ impl FromStr for DependencyLocator {

let repository = repository.into();

let (package, version) = dependency.split_once('@').ok_or_else(|| {
miette!("dependency specification is missing version part: {dependency}")
})?;
let (package, version) = dependency
.split_once('@')
.map(|(package, version)| (package, Some(version)))
.unwrap_or_else(|| (dependency, None));

let package = package
.parse::<PackageName>()
.wrap_err(miette!("invalid package name: {package}"))?;

let version = version
.parse::<VersionReq>()
.into_diagnostic()
.wrap_err(miette!("not a valid version requirement: {version}"))?;
let version = match version {
Some("latest") | None => DependencyLocatorVersion::Latest,
Some(version_str) => {
let parsed_version = VersionReq::parse(version_str)
.into_diagnostic()
.wrap_err(miette!("not a valid version requirement: {version_str}"))?;
DependencyLocatorVersion::Version(parsed_version)
}
};

Ok(Self {
repository,
Expand All @@ -134,6 +145,23 @@ pub async fn add(registry: RegistryUri, dependency: &str) -> miette::Result<()>
version,
} = dependency.parse()?;

let version = match version {
DependencyLocatorVersion::Version(version_req) => version_req,
DependencyLocatorVersion::Latest => {
// query artifactory to retrieve the actual latest version
let credentials = Credentials::load().await?;
let artifactory = Artifactory::new(registry.clone(), &credentials)?;

let latest_version = artifactory
.get_latest_version(repository.clone(), package.clone())
.await?;
// Convert semver::Version to semver::VersionReq. It will default to operator `>`, which is what we want for Proto.toml
VersionReq::parse(&latest_version.to_string())
.into_diagnostic()
.map_err(miette::Report::from)?
}
};

manifest
.dependencies
.push(Dependency::new(registry, repository, package, version));
Expand Down Expand Up @@ -511,14 +539,18 @@ mod tests {
assert!("repo/pkg@=1.0.0-with-prerelease"
.parse::<DependencyLocator>()
.is_ok());
assert!("repo/pkg@latest".parse::<DependencyLocator>().is_ok());
assert!("repo/pkg".parse::<DependencyLocator>().is_ok());
}

#[test]
fn invalid_dependency_locators() {
assert!("/[email protected]".parse::<DependencyLocator>().is_err());
assert!("repo/@1.0.0".parse::<DependencyLocator>().is_err());
assert!("[email protected]".parse::<DependencyLocator>().is_err());
assert!("repo/pkg".parse::<DependencyLocator>().is_err());
assert!("repo/pkg@latestwithtypo"
.parse::<DependencyLocator>()
.is_err());
assert!("repo/pkg@=1#meta".parse::<DependencyLocator>().is_err());
assert!("repo/PKG@=1.0".parse::<DependencyLocator>().is_err());
}
Expand Down
98 changes: 97 additions & 1 deletion src/registry/artifactory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,15 @@
// limitations under the License.

use super::RegistryUri;
use crate::{credentials::Credentials, manifest::Dependency, package::Package};
use crate::{
credentials::Credentials,
manifest::Dependency,
package::{Package, PackageName},
};
use miette::{ensure, miette, Context, IntoDiagnostic};
use reqwest::{Body, Method, Response};
use semver::Version;
use serde::Deserialize;
use url::Url;

/// The registry implementation for artifactory
Expand Down Expand Up @@ -65,6 +71,86 @@ impl Artifactory {
.map_err(miette::Report::from)
}

/// Retrieves the latest version of a package by querying artifactory. Returns an error if no artifact could be found
pub async fn get_latest_version(
&self,
repository: String,
name: PackageName,
) -> miette::Result<Version> {
// First retrieve all packages matching the given name
let search_query_url: Url = {
let mut url = self.registry.clone();
url.set_path("artifactory/api/search/artifact");
url.set_query(Some(&format!("name={}&repos={}", name, repository)));
url.into()
};

let response = self
.new_request(Method::GET, search_query_url)
.send()
.await?;
let response: reqwest::Response = response.into();

let headers = response.headers();
let content_type = headers
.get(&reqwest::header::CONTENT_TYPE)
.ok_or_else(|| miette!("missing content-type header"))?;
ensure!(
content_type
== reqwest::header::HeaderValue::from_static(
"application/vnd.org.jfrog.artifactory.search.ArtifactSearchResult+json"
),
"server response has incorrect mime type: {content_type:?}"
);

let response_str = response.text().await.into_diagnostic().wrap_err(miette!(
"unexpected error: unable to retrieve response payload"
))?;
let parsed_response = serde_json::from_str::<ArtifactSearchResponse>(&response_str)
.into_diagnostic()
.wrap_err(miette!(
"unexpected error: response could not be deserialized to ArtifactSearchResponse"
))?;

tracing::debug!(
"List of artifacts found matching the name: {:?}",
parsed_response
);

// Then from all package names retrieved from artifactory, extract the highest version number
let highest_version = parsed_response
.results
.iter()
.filter_map(|artifact_search_result| {
let uri = artifact_search_result.to_owned().uri;
let full_artifact_name = uri
.split('/')
.last()
.map(|name_tgz| name_tgz.trim_end_matches(".tgz"));
let artifact_version = full_artifact_name
.and_then(|name| name.split('-').last())
.and_then(|version_str| Version::parse(version_str).ok());

// we double check that the artifact name matches exactly
let expected_artifact_name = artifact_version
.clone()
.map(|av| format!("{}-{}", name, av));
if full_artifact_name.is_some_and(|actual| {
expected_artifact_name.is_some_and(|expected| expected == actual)
}) {
artifact_version
} else {
None
}
})
.max();

tracing::debug!("Highest version for artifact: {:?}", highest_version);
highest_version.ok_or_else(|| {
miette!("no version could be found on artifactory for this artifact name. Does it exist in this registry and repository?")
})
}

/// Downloads a package from artifactory
pub async fn download(&self, dependency: Dependency) -> miette::Result<Package> {
let artifact_url = {
Expand Down Expand Up @@ -189,3 +275,13 @@ impl TryFrom<Response> for ValidatedResponse {
value.error_for_status().into_diagnostic().map(Self)
}
}

#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
struct ArtifactSearchResponse {
results: Vec<ArtifactSearchResult>,
}

#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
struct ArtifactSearchResult {
uri: String,
}
Loading