Skip to content

Commit

Permalink
Merge pull request #259 from helsing-ai/gbouv/add-latest-by-default
Browse files Browse the repository at this point in the history
buffrs add now accept dependency with no version and defaults to `latest`
  • Loading branch information
gbouv authored Jul 5, 2024
2 parents badf7e5 + 0729eb4 commit accac5e
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 15 deletions.
13 changes: 8 additions & 5 deletions docs/src/commands/buffrs-add.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,17 @@ The dependency should be specified with the repository, name and version
according to the following format:

```
<repository>/<package>@<version>
<repository>/<package>[@<version>]
```

Note: the version can be omitted (or set to `@latest`), in which case
it will default to the latest version of this artifact in the registry.

The repository name should adhere to lower-kebab case (e.g. `my-buffrs-repo`).
The package name has its own set of constraints as detailed in [Package Name
Specification](../reference/pkgid-spec.md). The version must adhere to the
[Semantic Version convention](https://semver.org/) (e.g. `1.2.3`) -- see [SemVer
compatibility](../reference/semver.md) for more information.
Specification](../reference/pkgid-spec.md). When specified, the version must
adhere to the [Semantic Version convention](https://semver.org/) (e.g. `1.2.3`)
-- see [SemVer compatibility](../reference/semver.md) for more information.

Currently there is no support for resolving version operators but the specific
version has to be provided. This means `^1.0.0`, `<2.3.0`, `~2.0.0`, etc. can't
Expand All @@ -34,6 +37,6 @@ be installed, but `=1.2.3` has to be provided.
#### Lockfile interaction

Currently adding a new dependency won't automatically update the lockfile
(`Proto.lock`). This is planned to change, but for now follow up
(`Proto.lock`). This is planned to change, but for now follow up
with [`buffrs install`](buffrs-install.md) after adding a new dependency to make
sure your lockfile is kept in sync.
50 changes: 41 additions & 9 deletions src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,12 @@ pub async fn new(kind: Option<PackageType>, name: PackageName) -> miette::Result
struct DependencyLocator {
repository: String,
package: PackageName,
version: VersionReq,
version: DependencyLocatorVersion,
}

enum DependencyLocatorVersion {
Version(VersionReq),
Latest,
}

impl FromStr for DependencyLocator {
Expand All @@ -140,18 +145,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 @@ -171,6 +182,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 @@ -548,14 +576,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,
}

0 comments on commit accac5e

Please sign in to comment.