-
Couldn't load subscription status.
- Fork 9
Migrate inventory code #861
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
Merged
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
0abce6a
Add src files from inventory repo
runesoerensen b6aa3e8
Merge lib.rs and inventory.rs
runesoerensen e345f99
Declare inventory module and feature
runesoerensen 988bbe6
Prefer PhantomData
runesoerensen 27dc5a2
Add must_use attribute
runesoerensen eb8fa87
Remove unused import
runesoerensen a2c28f9
Allow unwrap for now
runesoerensen 7cb5e0f
Add sha2 feature
runesoerensen c590c0a
Add semver feature
runesoerensen 45d6cb4
Include patch version in dependency requirements
runesoerensen 77806dc
Add inventory toml dependency
runesoerensen 50ace6f
Add inventory thiserror dependency
runesoerensen 9deec38
Add changelog entry
runesoerensen 99d7d3a
Allow unreachable pub for re-exports
runesoerensen 369a70f
Remove semver and sha2 re-exports
runesoerensen 2906e0b
Rename `semver` feature to `inventory-semver`
runesoerensen 453861c
Rename `sha2` feature to `inventory-sha2`
runesoerensen 1ac0955
[stacked] Add docs to inventory code (#864)
schneems 8f66c88
Group inventory features alphabetically
runesoerensen File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,284 @@ | ||
| //! # Inventory | ||
| //! | ||
| //! Many buildpacks need to provide artifacts from different URLs. A helpful pattern | ||
| //! is to provide a list of artifacts in a TOML file, which can be parsed and used by | ||
| //! the buildpack to download the correct artifact. For example, a Ruby buildpack | ||
| //! might need to download pre-compiled Ruby binaries hosted on S3. | ||
| //! | ||
| //! This module can be used to produce and consume such an inventory file. | ||
| //! | ||
| //! ## Features | ||
| //! | ||
| //! - Version lookup and comparison: To implement the inventory, you'll need to define how | ||
| //! versions are compared. This allows the inventory code to find an appropriate artifact | ||
| //! based on whatever custom version logic you need. If you don't need custom logic, you can | ||
| //! use the included `inventory-semver` feature. | ||
| //! - Architecture aware: Beyond version specifiers, buildpack authors may need to provide different | ||
| //! artifacts for different computer architectures such as ARM64 or AMD64. The inventory encodes | ||
| //! this information which is used to select the correct artifact. | ||
| //! - Checksum validation: In addition to knowing the URL of an artifact, buildp authors | ||
| //! want to be confident that the artifact they download is the correct one. To accomplish this | ||
| //! the inventory contains a checksum of the download and can be used to validate the download | ||
| //! has not been modified or tampered with. To use sha256 or sha512 checksums out of the box, | ||
| //! enable the `inventory-sha2` feature | ||
| //! - Extensible with metadata: The default inventory format covers a lot of common use cases, | ||
| //! but if you need more, you can extend it by adding custom metadata to each artifact. | ||
| //! | ||
| //! ## Example usage | ||
| //! | ||
| //! This example demonstrates: | ||
| //! * Creating an artifact using the `inventory-sha2` and `inventory-semver` features. | ||
| //! * Adding the artifact to an inventory. | ||
| //! * Serializing and deserializing the inventory [to](Inventory#method.fmt) and [from](Inventory::from_str) TOML. | ||
| //! * [Resolving an inventory artifact](Inventory::resolve) specifying relevant OS, architecture, and version requirements. | ||
| //! * Using the resolved artifact's checksum value to verify "downloaded" data. | ||
| //! | ||
| //! ```rust | ||
| //! use libherokubuildpack::inventory::{artifact::{Arch, Artifact, Os}, Inventory, checksum::Checksum}; | ||
| //! use semver::{Version, VersionReq}; | ||
| //! use sha2::{Sha256, Digest}; | ||
| //! | ||
| //! // Create an artifact with a SHA256 checksum and `semver::Version` | ||
| //! let new_artifact = Artifact { | ||
| //! version: Version::new(1, 0, 0), | ||
| //! os: Os::Linux, | ||
| //! arch: Arch::Arm64, | ||
| //! url: "https://example.com/foo.txt".to_string(), | ||
| //! checksum: "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae" | ||
| //! .parse::<Checksum<Sha256>>() | ||
| //! .unwrap(), | ||
| //! metadata: None, | ||
| //! }; | ||
| //! | ||
| //! // Create an inventory and add the artifact | ||
| //! let mut inventory = Inventory::<Version, Sha256, Option<()>>::new(); | ||
| //! inventory.push(new_artifact.clone()); | ||
| //! | ||
| //! // Serialize the inventory to TOML | ||
| //! let inventory_toml = inventory.to_string(); | ||
| //! assert_eq!( | ||
| //! r#"[[artifacts]] | ||
| //! version = "1.0.0" | ||
| //! os = "linux" | ||
| //! arch = "arm64" | ||
| //! url = "https://example.com/foo.txt" | ||
| //! checksum = "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae" | ||
| //! "#, | ||
| //! inventory_toml | ||
| //! ); | ||
| //! | ||
| //! // Deserialize the inventory from TOML | ||
| //! let parsed_inventory = inventory_toml | ||
| //! .parse::<Inventory<Version, Sha256, Option<()>>>() | ||
| //! .unwrap(); | ||
| //! | ||
| //! // Resolve the artifact by OS, architecture, and version requirement | ||
| //! let version_req = VersionReq::parse("=1.0.0").unwrap(); | ||
| //! let resolved_artifact = parsed_inventory.resolve(Os::Linux, Arch::Arm64, &version_req).unwrap(); | ||
| //! | ||
| //! assert_eq!(&new_artifact, resolved_artifact); | ||
| //! | ||
| //! // Verify checksum of the resolved artifact | ||
| //! let downloaded_data = "foo"; // Example downloaded file content | ||
| //! let downloaded_checksum = Sha256::digest(downloaded_data).to_vec(); | ||
| //! | ||
| //! assert_eq!(downloaded_checksum, resolved_artifact.checksum.value); | ||
| //! println!( | ||
| //! "Successfully downloaded {} with checksum {}", | ||
| //! resolved_artifact.url, | ||
| //! hex::encode(&resolved_artifact.checksum.value) | ||
| //! ); | ||
| //! ``` | ||
| pub mod artifact; | ||
| pub mod checksum; | ||
| pub mod version; | ||
|
|
||
| #[cfg(feature = "inventory-semver")] | ||
| mod semver; | ||
| #[cfg(feature = "inventory-sha2")] | ||
| mod sha2; | ||
| mod unit; | ||
|
|
||
| use crate::inventory::artifact::{Arch, Artifact, Os}; | ||
| use crate::inventory::checksum::Digest; | ||
| use crate::inventory::version::ArtifactRequirement; | ||
| use serde::de::DeserializeOwned; | ||
| use serde::{Deserialize, Serialize}; | ||
| use std::cmp::Ordering; | ||
| use std::fmt::Formatter; | ||
| use std::str::FromStr; | ||
|
|
||
| /// Represents an inventory of artifacts. | ||
| /// | ||
| /// An inventory can be read directly from a TOML file on disk and used by a buildpack to resolve | ||
| /// requirements for a specific artifact to download. | ||
| /// | ||
| /// The inventory can be manipulated in-memory and then re-serialized to disk to facilitate both | ||
| /// reading and writing inventory files. | ||
| #[derive(Debug, Serialize, Deserialize)] | ||
| pub struct Inventory<V, D, M> { | ||
| #[serde(bound = "V: Serialize + DeserializeOwned, D: Digest, M: Serialize + DeserializeOwned")] | ||
| pub artifacts: Vec<Artifact<V, D, M>>, | ||
| } | ||
|
|
||
| impl<V, D, M> Default for Inventory<V, D, M> { | ||
| fn default() -> Self { | ||
| Self { artifacts: vec![] } | ||
| } | ||
| } | ||
|
|
||
| impl<V, D, M> Inventory<V, D, M> { | ||
| /// Creates a new empty inventory | ||
| #[must_use] | ||
| pub fn new() -> Self { | ||
| Self::default() | ||
| } | ||
|
|
||
| /// Add a new artifact to the in-memory inventory | ||
| pub fn push(&mut self, artifact: Artifact<V, D, M>) { | ||
| self.artifacts.push(artifact); | ||
| } | ||
|
|
||
| /// Return a single artifact as the best match given the input constraints | ||
| /// | ||
| /// If multiple artifacts match the constraints, the one with the highest version is returned. | ||
| pub fn resolve<R>(&self, os: Os, arch: Arch, requirement: &R) -> Option<&Artifact<V, D, M>> | ||
| where | ||
| V: Ord, | ||
| R: ArtifactRequirement<V, M>, | ||
| { | ||
| self.artifacts | ||
| .iter() | ||
| .filter(|artifact| { | ||
| artifact.os == os | ||
| && artifact.arch == arch | ||
| && requirement.satisfies_version(&artifact.version) | ||
| && requirement.satisfies_metadata(&artifact.metadata) | ||
| }) | ||
| .max_by_key(|artifact| &artifact.version) | ||
| } | ||
|
|
||
| /// Resolve logic for Artifacts that implement `PartialOrd` rather than `Ord` | ||
| /// | ||
| /// Some version implementations are only partially ordered. One example could be f32 which is not totally ordered | ||
| /// because NaN is not comparable to any other number. | ||
| pub fn partial_resolve<R>( | ||
joshwlewis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| &self, | ||
| os: Os, | ||
| arch: Arch, | ||
| requirement: &R, | ||
| ) -> Option<&Artifact<V, D, M>> | ||
| where | ||
| V: PartialOrd, | ||
| R: ArtifactRequirement<V, M>, | ||
| { | ||
| #[inline] | ||
| fn partial_max_by_key<I, F, A>(iterator: I, f: F) -> Option<I::Item> | ||
| where | ||
| I: Iterator, | ||
| F: Fn(&I::Item) -> A, | ||
| A: PartialOrd, | ||
| { | ||
| iterator.fold(None, |acc, item| match acc { | ||
| None => Some(item), | ||
| Some(acc) => match f(&item).partial_cmp(&f(&acc)) { | ||
| Some(Ordering::Greater | Ordering::Equal) => Some(item), | ||
| None | Some(Ordering::Less) => Some(acc), | ||
| }, | ||
| }) | ||
| } | ||
|
|
||
| partial_max_by_key( | ||
| self.artifacts.iter().filter(|artifact| { | ||
| artifact.os == os | ||
| && artifact.arch == arch | ||
| && requirement.satisfies_version(&artifact.version) | ||
| && requirement.satisfies_metadata(&artifact.metadata) | ||
| }), | ||
| |artifact| &artifact.version, | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| #[derive(thiserror::Error, Debug)] | ||
| pub enum ParseInventoryError { | ||
| #[error("TOML parsing error: {0}")] | ||
| TomlError(toml::de::Error), | ||
| } | ||
|
|
||
| impl<V, D, M> FromStr for Inventory<V, D, M> | ||
| where | ||
| V: Serialize + DeserializeOwned, | ||
| D: Digest, | ||
| M: Serialize + DeserializeOwned, | ||
| { | ||
| type Err = ParseInventoryError; | ||
|
|
||
| fn from_str(s: &str) -> Result<Self, Self::Err> { | ||
| toml::from_str(s).map_err(ParseInventoryError::TomlError) | ||
| } | ||
| } | ||
|
|
||
| impl<V, D, M> std::fmt::Display for Inventory<V, D, M> | ||
| where | ||
| V: Serialize + DeserializeOwned, | ||
| D: Digest, | ||
| M: Serialize + DeserializeOwned, | ||
| { | ||
| #![allow(clippy::unwrap_used)] | ||
| fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { | ||
| f.write_str(&toml::to_string(self).unwrap()) | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod test { | ||
| use crate::inventory::artifact::{Arch, Artifact, Os}; | ||
| use crate::inventory::checksum::tests::BogusDigest; | ||
| use crate::inventory::Inventory; | ||
|
|
||
| #[test] | ||
| fn test_matching_artifact_resolution() { | ||
| let mut inventory = Inventory::new(); | ||
| inventory.push(create_artifact("foo", Os::Linux, Arch::Arm64)); | ||
|
|
||
| assert_eq!( | ||
| "foo", | ||
| &inventory | ||
| .resolve(Os::Linux, Arch::Arm64, &String::from("foo")) | ||
| .expect("should resolve matching artifact") | ||
| .version, | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_dont_resolve_artifact_with_wrong_arch() { | ||
| let mut inventory = Inventory::new(); | ||
| inventory.push(create_artifact("foo", Os::Linux, Arch::Arm64)); | ||
|
|
||
| assert!(inventory | ||
| .resolve(Os::Linux, Arch::Amd64, &String::from("foo")) | ||
| .is_none()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_dont_resolve_artifact_with_wrong_version() { | ||
| let mut inventory = Inventory::new(); | ||
| inventory.push(create_artifact("foo", Os::Linux, Arch::Arm64)); | ||
|
|
||
| assert!(inventory | ||
| .resolve(Os::Linux, Arch::Arm64, &String::from("bar")) | ||
| .is_none()); | ||
| } | ||
|
|
||
| fn create_artifact(version: &str, os: Os, arch: Arch) -> Artifact<String, BogusDigest, ()> { | ||
| Artifact { | ||
| version: String::from(version), | ||
| os, | ||
| arch, | ||
| url: "https://example.com".to_string(), | ||
| checksum: BogusDigest::checksum("cafebabe"), | ||
| metadata: (), | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.