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

Struct Layer API #814

Merged
merged 29 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
44a01a9
Add struct layer API
Malax Mar 12, 2024
0d93533
Add deprecation annotations
Malax Mar 12, 2024
ec9067d
Move existing layer API to separate module, move shared code
Malax Mar 13, 2024
18bf79e
Make compilation succeed again
Malax Apr 8, 2024
10c2caf
Fix RustDoc
Malax Apr 8, 2024
7bc68d0
Add `#![allow(deprecated)]` to example and test buildpacks
Malax Apr 9, 2024
194035d
Remove no longer necessary clippy allow
Malax Apr 9, 2024
3fad3c4
Fix replace_layer_types visibility
Malax Apr 9, 2024
239c01c
Properly expose layer errors
Malax Apr 9, 2024
96d010d
Move shared tests for read_layer to shared module
Malax Apr 9, 2024
9d2ee42
Move write_layer to shared code
Malax Apr 9, 2024
9984be3
Fix WriteLayerError Display impl
Malax Apr 10, 2024
408000d
Rename execute to handle
Malax Apr 10, 2024
b0d5aed
Apply miscellaneous polish
Malax Jun 13, 2024
8171c64
Add struct_api handling tests
Malax Jun 14, 2024
fc81889
Rename LayerContents to LayerState, InspectExisting to InspectRestored
Malax Jun 14, 2024
21ad86b
Update CHANGELOG
Malax Jun 14, 2024
c900b88
Fix typos
Malax Jun 17, 2024
de6351b
Fix CHANGELOG entry
Malax Jun 17, 2024
06d4668
Rename LayerRef methods
Malax Jun 17, 2024
bfcb0ce
Rename CachedLayerDefinition callbacks
Malax Jun 17, 2024
e317deb
RustDoc update
Malax Jun 18, 2024
603f0c5
Renamed EmptyLayerCause variants
Malax Jun 18, 2024
3f2f832
Explain IntoAction in cached_layer example
Malax Jun 18, 2024
400b1b3
Explain IntoAction cached_layer docs
Malax Jun 18, 2024
a4877e4
Add uncached_layer example
Malax Jun 18, 2024
2625478
Rename remaining inspect_action related names
Malax Jun 18, 2024
11b6ce1
Add LayerRef::read_env
Malax Jun 18, 2024
57683ad
RustDoc improvements
Malax Jun 18, 2024
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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- `libcnb`:
- A new API for working with layers as been added. Check `BuildContext::cached_layer` for a starting point how to work with this API. ([#814](https://github.com/heroku/libcnb.rs/pull/814))
Malax marked this conversation as resolved.
Show resolved Hide resolved
Malax marked this conversation as resolved.
Show resolved Hide resolved

### Changed

- `libcnb`:
- The `Layer` trait and related types and functions have been deprecated. Please migrate to the new API. ([#814](https://github.com/heroku/libcnb.rs/pull/814))
- Errors related to layers have been restructured. While this is technically a breaking change, buildpacks usually don't have to be modified in practice. ([#814](https://github.com/heroku/libcnb.rs/pull/814))

### Fixed

- `libcnb-data`:
Expand Down
4 changes: 4 additions & 0 deletions examples/execd/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
// This example uses the older trait Layer API. The example will be updated to the newer API
// before the next libcnb.rs release.
#![allow(deprecated)]

mod layer;

use crate::layer::ExecDLayer;
Expand Down
306 changes: 297 additions & 9 deletions libcnb/src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,18 @@ use crate::data::store::Store;
use crate::data::{
buildpack::ComponentBuildpackDescriptor, buildpack_plan::BuildpackPlan, launch::Launch,
};
use crate::layer::{HandleLayerErrorOrBuildpackError, Layer, LayerData};
use crate::layer::trait_api::handling::LayerErrorOrBuildpackError;
use crate::layer::{
CachedLayerDefinition, InspectRestoredAction, IntoAction, InvalidMetadataAction, LayerRef,
UncachedLayerDefinition,
};
use crate::sbom::Sbom;
use crate::Target;
use libcnb_data::generic::GenericMetadata;
use libcnb_data::layer_content_metadata::LayerTypes;
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::borrow::Borrow;
use std::path::PathBuf;

/// Context for the build phase execution.
Expand All @@ -24,13 +33,13 @@ pub struct BuildContext<B: Buildpack + ?Sized> {
}

impl<B: Buildpack + ?Sized> BuildContext<B> {
/// Handles the given [`Layer`] implementation in this context.
/// Handles the given [`crate::layer::Layer`] implementation in this context.
///
/// It will ensure that the layer with the given name is created and/or updated accordingly and
/// handles all errors that can occur during the process. After this method has executed, the
/// layer will exist on disk or an error has been returned by this method.
///
/// Use the returned [`LayerData`] to access the layers metadata and environment variables for
/// Use the returned [`crate::layer::LayerData`] to access the layers metadata and environment variables for
/// subsequent logic or layers.
///
/// # Example:
Expand Down Expand Up @@ -95,18 +104,297 @@ impl<B: Buildpack + ?Sized> BuildContext<B> {
/// }
/// }
/// ```
pub fn handle_layer<L: Layer<Buildpack = B>>(
#[deprecated = "The Layer trait API was replaced by a struct based API. Use `cached_layer` and `uncached_layer`."]
#[allow(deprecated)]
pub fn handle_layer<L: crate::layer::Layer<Buildpack = B>>(
&self,
layer_name: LayerName,
layer: L,
) -> crate::Result<LayerData<L::Metadata>, B::Error> {
crate::layer::handle_layer(self, layer_name, layer).map_err(|error| match error {
HandleLayerErrorOrBuildpackError::HandleLayerError(e) => {
crate::Error::HandleLayerError(e)
) -> crate::Result<crate::layer::LayerData<L::Metadata>, B::Error> {
crate::layer::trait_api::handling::handle_layer(self, layer_name, layer).map_err(|error| {
match error {
LayerErrorOrBuildpackError::LayerError(e) => crate::Error::LayerError(e),
LayerErrorOrBuildpackError::BuildpackError(e) => crate::Error::BuildpackError(e),
}
HandleLayerErrorOrBuildpackError::BuildpackError(e) => crate::Error::BuildpackError(e),
})
}

/// Creates a cached layer, potentially re-using a previously cached version.
///
/// Buildpack code uses this function to create a cached layer and will get back a reference to
/// the layer directory on disk. Intricacies of the CNB spec are automatically handled such as
/// the maintenance of TOML files. Buildpack code can also specify a callback for cached layer
/// invalidation.
///
/// Users of this function pass in a [`CachedLayerDefinition`] that describes the desired layer
/// and the returned `LayerRef` can then be used to modify the layer like any other path. This
Malax marked this conversation as resolved.
Show resolved Hide resolved
/// allows users to be flexible when and how the layer is modified and to abstract layer
Malax marked this conversation as resolved.
Show resolved Hide resolved
/// creation away if necessary.
///
/// # Basic Example
/// ```rust
/// # use libcnb::build::{BuildContext, BuildResult, BuildResultBuilder};
/// # use libcnb::detect::{DetectContext, DetectResult};
/// # use libcnb::generic::GenericPlatform;
/// # use libcnb::layer::{
/// # CachedLayerDefinition, InspectRestoredAction, InvalidMetadataAction, LayerState,
/// # };
/// # use libcnb::layer_env::{LayerEnv, ModificationBehavior, Scope};
/// # use libcnb::Buildpack;
/// # use libcnb_data::generic::GenericMetadata;
/// # use libcnb_data::layer_name;
/// # use std::fs;
/// #
/// # struct ExampleBuildpack;
/// #
/// # #[derive(Debug)]
/// # enum ExampleBuildpackError {
/// # WriteDataError(std::io::Error),
/// # }
/// #
/// # impl Buildpack for ExampleBuildpack {
/// # type Platform = GenericPlatform;
/// # type Metadata = GenericMetadata;
/// # type Error = ExampleBuildpackError;
/// #
/// # fn detect(&self, context: DetectContext<Self>) -> libcnb::Result<DetectResult, Self::Error> {
/// # unimplemented!()
/// # }
/// #
/// # fn build(&self, context: BuildContext<Self>) -> libcnb::Result<BuildResult, Self::Error> {
/// let layer_ref = context.cached_layer(
/// layer_name!("example_layer"),
/// CachedLayerDefinition {
/// build: false,
/// launch: false,
/// // Will be called if a cached version of the layer was found, but the metadata
/// // could not be parsed. In this example, we instruct libcnb to always delete the
/// // existing layer in such a case. But we can implement any logic here if we want.
/// invalid_metadata: &|_| InvalidMetadataAction::DeleteLayer,
/// // Will be called if a cached version of the layer was found. This allows us to
/// // inspect the contents and metadata to decide if we want to keep the existing
/// // layer or let libcnb delete the existing layer and create a new one for us.
/// // This is libcnb's method to implement cache invalidations for layers.
/// inspect_restored: &|_: &GenericMetadata, _| InspectRestoredAction::KeepLayer,
/// },
/// )?;
///
/// // At this point, a layer exists on disk. It might contain cached data or might be empty.
/// // Since we need to conditionally work with the layer contents based on its state, we can
/// // inspect the `contents` field of the layer reference to get detailed information about
Malax marked this conversation as resolved.
Show resolved Hide resolved
/// // the current layer contents and the cause(s) for the state.
/// //
/// // In the majority of cases, we don't need more details beyond if it's empty or not and can
/// // ignore the details. This is what we do in this example. See the later example for a more
/// // complex situation.
/// match layer_ref.state {
/// LayerState::Empty { .. } => {
/// println!("Creating new example layer!");
///
/// // Modify the layer contents with regular Rust functions:
/// fs::write(
/// layer_ref.path().join("data.txt"),
/// "Here is some example data",
/// )
/// .map_err(ExampleBuildpackError::WriteDataError)?;
///
/// // Use functions on LayerRef for common CNB specific layer modifications:
/// layer_ref.replace_env(LayerEnv::new().chainable_insert(
/// Scope::All,
/// ModificationBehavior::Append,
/// "PLANET",
/// "LV-246",
/// ))?;
/// }
/// LayerState::Restored { .. } => {
/// println!("Reusing example layer from previous run!");
/// }
/// }
/// #
/// # BuildResultBuilder::new().build()
/// # }
/// # }
/// #
/// # impl From<ExampleBuildpackError> for libcnb::Error<ExampleBuildpackError> {
/// # fn from(value: ExampleBuildpackError) -> Self {
/// # Self::BuildpackError(value)
/// # }
/// # }
/// ```
///
/// # More complex example
/// ```rust
/// # use libcnb::build::{BuildContext, BuildResult, BuildResultBuilder};
/// # use libcnb::detect::{DetectContext, DetectResult};
/// # use libcnb::generic::GenericPlatform;
/// # use libcnb::layer::{
/// # CachedLayerDefinition, EmptyLayerCause, InspectRestoredAction, InvalidMetadataAction,
/// # LayerState,
/// # };
/// # use libcnb::Buildpack;
/// # use libcnb_data::generic::GenericMetadata;
/// # use libcnb_data::layer_name;
/// # use serde::{Deserialize, Serialize};
/// # use std::fs;
/// #
/// # struct ExampleBuildpack;
/// #
/// # #[derive(Debug)]
/// # enum ExampleBuildpackError {
/// # UnexpectedIoError(std::io::Error),
/// # }
/// #
/// #[derive(Deserialize, Serialize)]
/// struct ExampleLayerMetadata {
/// lang_runtime_version: String,
/// }
///
/// enum CustomCause {
/// Ok,
/// LegacyVersion,
/// HasBrokenModule,
/// MissingModulesFile,
/// }
///
/// # impl Buildpack for ExampleBuildpack {
/// # type Platform = GenericPlatform;
/// # type Metadata = GenericMetadata;
/// # type Error = ExampleBuildpackError;
/// #
/// # fn detect(&self, _: DetectContext<Self>) -> libcnb::Result<DetectResult, Self::Error> {
/// # unimplemented!()
/// # }
/// #
/// fn build(&self, context: BuildContext<Self>) -> libcnb::Result<BuildResult, Self::Error> {
/// let layer_ref = context.cached_layer(
/// layer_name!("example_layer"),
/// CachedLayerDefinition {
/// build: false,
/// launch: false,
/// invalid_metadata: &|_| InvalidMetadataAction::DeleteLayer,
/// inspect_restored: &|metadata: &ExampleLayerMetadata, layer_dir| {
/// if metadata.lang_runtime_version.starts_with("0.") {
/// Ok((
/// InspectRestoredAction::DeleteLayer,
/// CustomCause::LegacyVersion,
/// ))
Malax marked this conversation as resolved.
Show resolved Hide resolved
/// } else {
/// let file_path = layer_dir.join("modules.txt");
///
/// if file_path.is_file() {
/// // This is a fallible operation where an unexpected IO error occurs
/// // during operation. In this example, we chose not to map it to
/// // a layer action but let it automatically "bubble up". This error will
/// // end up in the regular libcnb buildpack on_error.
/// let file_contents = fs::read_to_string(&file_path)
/// .map_err(ExampleBuildpackError::UnexpectedIoError)?;
///
/// if file_contents == "known-broken-0.1c" {
/// Ok((
/// InspectRestoredAction::DeleteLayer,
/// CustomCause::HasBrokenModule,
/// ))
/// } else {
/// Ok((InspectRestoredAction::KeepLayer, CustomCause::Ok))
/// }
/// } else {
/// Ok((
/// InspectRestoredAction::DeleteLayer,
/// CustomCause::MissingModulesFile,
/// ))
/// }
/// }
/// },
/// },
/// )?;
///
/// match layer_ref.state {
/// LayerState::Empty { ref cause } => {
/// // Since the cause is just a regular Rust value, we can match it with regular
/// // Rust syntax and be as complex or simple as we need.
/// let message = match cause {
/// EmptyLayerCause::Inspect {
/// cause: CustomCause::LegacyVersion,
/// } => "Re-installing language runtime (legacy cached version)",
/// EmptyLayerCause::Inspect {
/// cause: CustomCause::HasBrokenModule | CustomCause::MissingModulesFile,
/// } => "Re-installing language runtime (broken modules detected)",
/// _ => "Installing language runtime",
/// };
///
/// println!("{message}");
///
/// // Code to install the language runtime would go here
///
/// layer_ref.replace_metadata(ExampleLayerMetadata {
/// lang_runtime_version: String::from("1.0.0"),
/// })?;
/// }
/// LayerState::Restored { .. } => {
/// println!("Re-using cached language runtime");
/// }
/// }
///
/// BuildResultBuilder::new().build()
/// }
/// # }
/// #
/// # impl From<ExampleBuildpackError> for libcnb::Error<ExampleBuildpackError> {
/// # fn from(value: ExampleBuildpackError) -> Self {
/// # Self::BuildpackError(value)
/// # }
/// # }
/// ```
pub fn cached_layer<'a, M, MA, IA, MAC, IAC>(
&self,
layer_name: impl Borrow<LayerName>,
layer_definition: impl Borrow<CachedLayerDefinition<'a, M, MA, IA>>,
) -> crate::Result<LayerRef<B, MAC, IAC>, B::Error>
where
M: 'a + Serialize + DeserializeOwned,
MA: 'a + IntoAction<InvalidMetadataAction<M>, MAC, B::Error>,
IA: 'a + IntoAction<InspectRestoredAction, IAC, B::Error>,
{
let layer_definition = layer_definition.borrow();

crate::layer::struct_api::handling::handle_layer(
LayerTypes {
launch: layer_definition.launch,
build: layer_definition.build,
cache: true,
},
layer_definition.invalid_metadata,
layer_definition.inspect_restored,
layer_name.borrow(),
&self.layers_dir,
)
}

/// Creates an uncached layer.
///
/// If the layer already exists because it was cached in a previous buildpack run, the existing
/// data will be deleted.
///
/// This function is essentially the same as [`BuildContext::uncached_layer`] but simpler.
Malax marked this conversation as resolved.
Show resolved Hide resolved
pub fn uncached_layer(
&self,
layer_name: impl Borrow<LayerName>,
layer_definition: impl Borrow<UncachedLayerDefinition>,
) -> crate::Result<LayerRef<B, (), ()>, B::Error> {
let layer_definition = layer_definition.borrow();

crate::layer::struct_api::handling::handle_layer(
LayerTypes {
launch: layer_definition.launch,
build: layer_definition.build,
cache: false,
},
&|_| InvalidMetadataAction::DeleteLayer,
&|_: &GenericMetadata, _| InspectRestoredAction::DeleteLayer,
layer_name.borrow(),
&self.layers_dir,
)
}
}

/// Describes the result of the build phase.
Expand Down
6 changes: 3 additions & 3 deletions libcnb/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::data::launch::ProcessTypeError;
use crate::layer::HandleLayerError;
use crate::layer::LayerError;
use libcnb_common::toml_file::TomlFileError;
use std::fmt::Debug;

Expand All @@ -11,8 +11,8 @@ pub type Result<T, E> = std::result::Result<T, Error<E>>;
/// An error that occurred during buildpack execution.
#[derive(thiserror::Error, Debug)]
pub enum Error<E> {
#[error("HandleLayer error: {0}")]
HandleLayerError(#[from] HandleLayerError),
#[error("Layer error: {0}")]
LayerError(#[from] LayerError),

#[error("Process type error: {0}")]
ProcessTypeError(#[from] ProcessTypeError),
Expand Down
Loading