From fa568d41fd12fd2e2688a23029c45025707fdc60 Mon Sep 17 00:00:00 2001 From: Manuel Fuchs Date: Tue, 18 Jun 2024 13:02:06 +0200 Subject: [PATCH] Struct Layer API (#814) --- CHANGELOG.md | 11 + examples/execd/src/main.rs | 4 + libcnb/src/build.rs | 382 +++++++++++- libcnb/src/error.rs | 6 +- libcnb/src/layer/mod.rs | 15 +- libcnb/src/layer/shared.rs | 456 ++++++++++++++ libcnb/src/layer/struct_api/handling.rs | 522 ++++++++++++++++ libcnb/src/layer/struct_api/mod.rs | 238 ++++++++ libcnb/src/layer/{ => trait_api}/handling.rs | 572 +++--------------- .../{public_interface.rs => trait_api/mod.rs} | 18 +- libcnb/src/layer/{ => trait_api}/tests.rs | 4 +- .../readonly-layer-files/src/main.rs | 4 + test-buildpacks/sbom/src/main.rs | 4 + 13 files changed, 1735 insertions(+), 501 deletions(-) create mode 100644 libcnb/src/layer/shared.rs create mode 100644 libcnb/src/layer/struct_api/handling.rs create mode 100644 libcnb/src/layer/struct_api/mod.rs rename libcnb/src/layer/{ => trait_api}/handling.rs (53%) rename libcnb/src/layer/{public_interface.rs => trait_api/mod.rs} (92%) rename libcnb/src/layer/{ => trait_api}/tests.rs (99%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e7abc18..164077e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 has been added. See the `BuildContext::cached_layer` and `BuildContext::uncached_layer` docs for examples of how to use this API. ([#814](https://github.com/heroku/libcnb.rs/pull/814)) + +### 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`: diff --git a/examples/execd/src/main.rs b/examples/execd/src/main.rs index b395f751..84c2fc42 100644 --- a/examples/execd/src/main.rs +++ b/examples/execd/src/main.rs @@ -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; diff --git a/libcnb/src/build.rs b/libcnb/src/build.rs index 01221bf6..234a3149 100644 --- a/libcnb/src/build.rs +++ b/libcnb/src/build.rs @@ -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, IntoAction, InvalidMetadataAction, LayerRef, RestoredLayerAction, + 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. @@ -24,13 +33,13 @@ pub struct BuildContext { } impl BuildContext { - /// 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: @@ -95,18 +104,373 @@ impl BuildContext { /// } /// } /// ``` - pub fn handle_layer>( + #[deprecated = "The Layer trait API was replaced by a struct based API. Use `cached_layer` and `uncached_layer`."] + #[allow(deprecated)] + pub fn handle_layer>( &self, layer_name: LayerName, layer: L, - ) -> crate::Result, B::Error> { - crate::layer::handle_layer(self, layer_name, layer).map_err(|error| match error { - HandleLayerErrorOrBuildpackError::HandleLayerError(e) => { - crate::Error::HandleLayerError(e) + ) -> crate::Result, 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 + /// allows users to be flexible in how and when the layer is modified and to abstract layer + /// creation away if necessary. + /// + /// See [`IntoAction`] for details on which values can be returned from the + /// `invalid_metadata_action` and `restored_layer_action` functions. + /// + /// # Basic Example + /// ```rust + /// # use libcnb::build::{BuildContext, BuildResult, BuildResultBuilder}; + /// # use libcnb::detect::{DetectContext, DetectResult}; + /// # use libcnb::generic::GenericPlatform; + /// # use libcnb::layer::{ + /// # CachedLayerDefinition, RestoredLayerAction, 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) -> libcnb::Result { + /// # unimplemented!() + /// # } + /// # + /// # fn build(&self, context: BuildContext) -> libcnb::Result { + /// 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_action: &|_| 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. + /// restored_layer_action: &|_: &GenericMetadata, _| RestoredLayerAction::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 `state` field of the layer reference to get detailed information about + /// // 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.write_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 for libcnb::Error { + /// # 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, RestoredLayerAction, 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) -> libcnb::Result { + /// # unimplemented!() + /// # } + /// # + /// fn build(&self, context: BuildContext) -> libcnb::Result { + /// let layer_ref = context.cached_layer( + /// layer_name!("example_layer"), + /// CachedLayerDefinition { + /// build: false, + /// launch: false, + /// invalid_metadata_action: &|_| InvalidMetadataAction::DeleteLayer, + /// restored_layer_action: &|metadata: &ExampleLayerMetadata, layer_dir| { + /// if metadata.lang_runtime_version.starts_with("0.") { + /// // The return value for restored_layer_action can be anything with an + /// // IntoAction implementation. libcnb provides built-in implementations + /// // for raw RestoredLayerAction/InvalidMetadataAction values, tuples of + /// // actions with a cause value (of any type) plus variants that are wrapped + /// // in a Result. See IntoAction for details. + /// Ok(( + /// RestoredLayerAction::DeleteLayer, + /// CustomCause::LegacyVersion, + /// )) + /// } 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(( + /// RestoredLayerAction::DeleteLayer, + /// CustomCause::HasBrokenModule, + /// )) + /// } else { + /// Ok((RestoredLayerAction::KeepLayer, CustomCause::Ok)) + /// } + /// } else { + /// Ok(( + /// RestoredLayerAction::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::RestoredLayerAction { + /// cause: CustomCause::LegacyVersion, + /// } => "Re-installing language runtime (legacy cached version)", + /// EmptyLayerCause::RestoredLayerAction { + /// 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.write_metadata(ExampleLayerMetadata { + /// lang_runtime_version: String::from("1.0.0"), + /// })?; + /// } + /// LayerState::Restored { .. } => { + /// println!("Re-using cached language runtime"); + /// } + /// } + /// + /// BuildResultBuilder::new().build() + /// } + /// # } + /// # + /// # impl From for libcnb::Error { + /// # fn from(value: ExampleBuildpackError) -> Self { + /// # Self::BuildpackError(value) + /// # } + /// # } + /// ``` + pub fn cached_layer<'a, M, MA, RA, MAC, RAC>( + &self, + layer_name: impl Borrow, + layer_definition: impl Borrow>, + ) -> crate::Result, B::Error> + where + M: 'a + Serialize + DeserializeOwned, + MA: 'a + IntoAction, MAC, B::Error>, + RA: 'a + IntoAction, + { + 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_action, + layer_definition.restored_layer_action, + 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. + /// + /// # Example + /// ```rust + /// # use libcnb::build::{BuildContext, BuildResult, BuildResultBuilder}; + /// # use libcnb::detect::{DetectContext, DetectResult}; + /// # use libcnb::generic::GenericPlatform; + /// # use libcnb::layer::{ + /// # UncachedLayerDefinition, RestoredLayerAction, 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) -> libcnb::Result { + /// # unimplemented!() + /// # } + /// # + /// # fn build(&self, context: BuildContext) -> libcnb::Result { + /// let layer_ref = context.uncached_layer( + /// layer_name!("example_layer"), + /// UncachedLayerDefinition { + /// build: false, + /// launch: false, + /// }, + /// )?; + /// + /// println!("Creating new example layer!"); + /// + /// // Modify the layer contents with regular Rust functions: + /// fs::write( + /// layer_ref.path().join("incantation.txt"), + /// "Phol ende uuodan uuorun zi holza. Du uuart demo balderes uolon sin uuoz birenkit.", + /// ) + /// .map_err(ExampleBuildpackError::WriteDataError)?; + /// + /// // Use functions on LayerRef for common CNB specific layer modifications: + /// layer_ref.write_env(LayerEnv::new().chainable_insert( + /// Scope::All, + /// ModificationBehavior::Append, + /// "PLANET", + /// "LV-246", + /// ))?; + /// # + /// # + /// # BuildResultBuilder::new().build() + /// # } + /// # } + /// # + /// # impl From for libcnb::Error { + /// # fn from(value: ExampleBuildpackError) -> Self { + /// # Self::BuildpackError(value) + /// # } + /// # } + /// ``` + pub fn uncached_layer( + &self, + layer_name: impl Borrow, + layer_definition: impl Borrow, + ) -> crate::Result, 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, _| RestoredLayerAction::DeleteLayer, + layer_name.borrow(), + &self.layers_dir, + ) + } } /// Describes the result of the build phase. diff --git a/libcnb/src/error.rs b/libcnb/src/error.rs index bc0ffd37..dfbf2889 100644 --- a/libcnb/src/error.rs +++ b/libcnb/src/error.rs @@ -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; @@ -11,8 +11,8 @@ pub type Result = std::result::Result>; /// An error that occurred during buildpack execution. #[derive(thiserror::Error, Debug)] pub enum Error { - #[error("HandleLayer error: {0}")] - HandleLayerError(#[from] HandleLayerError), + #[error("Layer error: {0}")] + LayerError(#[from] LayerError), #[error("Process type error: {0}")] ProcessTypeError(#[from] ProcessTypeError), diff --git a/libcnb/src/layer/mod.rs b/libcnb/src/layer/mod.rs index 29ab4e6b..70139dc3 100644 --- a/libcnb/src/layer/mod.rs +++ b/libcnb/src/layer/mod.rs @@ -1,10 +1,13 @@ //! Provides types and helpers to work with layers. -mod handling; -mod public_interface; +pub(crate) mod shared; +pub(crate) mod struct_api; +pub(crate) mod trait_api; -#[cfg(test)] -mod tests; +pub use shared::DeleteLayerError; +pub use shared::LayerError; +pub use shared::ReadLayerError; +pub use shared::WriteLayerError; -pub(crate) use handling::*; -pub use public_interface::*; +pub use struct_api::*; +pub use trait_api::*; diff --git a/libcnb/src/layer/shared.rs b/libcnb/src/layer/shared.rs new file mode 100644 index 00000000..cb6ea69a --- /dev/null +++ b/libcnb/src/layer/shared.rs @@ -0,0 +1,456 @@ +// This lint triggers when both layer_dir and layers_dir are present which are quite common. +#![allow(clippy::similar_names)] + +use crate::sbom::{cnb_sbom_path, Sbom}; +use crate::util::{default_on_not_found, remove_dir_recursively}; +use libcnb_common::toml_file::{read_toml_file, write_toml_file, TomlFileError}; +use libcnb_data::layer::LayerName; +use libcnb_data::layer_content_metadata::{LayerContentMetadata, LayerTypes}; +use libcnb_data::sbom::SBOM_FORMATS; +use serde::de::DeserializeOwned; +use serde::Serialize; +use std::collections::HashMap; +use std::fs; +use std::path::{Path, PathBuf}; + +pub(in crate::layer) fn read_layer>( + layers_dir: P, + layer_name: &LayerName, +) -> Result>, ReadLayerError> { + let layer_dir_path = layers_dir.as_ref().join(layer_name.as_str()); + let layer_toml_path = layers_dir.as_ref().join(format!("{layer_name}.toml")); + + if !layer_dir_path.exists() && !layer_toml_path.exists() { + return Ok(None); + } else if !layer_dir_path.exists() && layer_toml_path.exists() { + // This is a valid case according to the spec: + // https://github.com/buildpacks/spec/blob/7b20dfa070ed428c013e61a3cefea29030af1732/buildpack.md#layer-types + // + // When launch = true, build = false, cache = false, the layer metadata will be restored but + // not the layer itself. However, we choose to not support this case as of now. It would + // complicate the API we need to expose to the user of libcnb as this case is very different + // compared to all other combinations of launch, build and cache. It's the only case where + // a cache = false layer restores some of its data between builds. + // + // To normalize, we remove the layer TOML file and treat the layer as non-existent. + fs::remove_file(&layer_toml_path)?; + return Ok(None); + } + + // An empty layer content metadata file is valid and the CNB spec is not clear if the lifecycle + // has to restore them if they're empty. This is especially important since the layer types + // are removed from the file if it's restored. To normalize, we write an empty file if the layer + // directory exists without the metadata file. + if !layer_toml_path.exists() { + fs::write(&layer_toml_path, "")?; + } + + let layer_toml_contents = fs::read_to_string(&layer_toml_path)?; + + let layer_content_metadata = toml::from_str::>(&layer_toml_contents) + .map_err(ReadLayerError::LayerContentMetadataParseError)?; + + Ok(Some(ReadLayer { + name: layer_name.clone(), + path: layer_dir_path, + metadata: layer_content_metadata, + })) +} + +pub(in crate::layer) struct ReadLayer { + pub(in crate::layer) name: LayerName, + pub(in crate::layer) path: PathBuf, + pub(in crate::layer) metadata: LayerContentMetadata, +} + +#[derive(thiserror::Error, Debug)] +pub enum ReadLayerError { + #[error("Layer content metadata couldn't be parsed!")] + LayerContentMetadataParseError(toml::de::Error), + + #[error("Unexpected I/O error while reading layer: {0}")] + IoError(#[from] std::io::Error), +} + +pub(in crate::layer) fn write_layer>( + layers_dir: P, + layer_name: &LayerName, + layer_content_metadata: &LayerContentMetadata, +) -> Result<(), WriteLayerError> { + let layer_dir = layers_dir.as_ref().join(layer_name.as_str()); + fs::create_dir_all(layer_dir)?; + + let layer_content_metadata_path = layers_dir.as_ref().join(format!("{layer_name}.toml")); + + write_toml_file(&layer_content_metadata, layer_content_metadata_path) + .map_err(WriteLayerMetadataError::TomlFileError) + .map_err(WriteLayerError::WriteLayerMetadataError)?; + + Ok(()) +} + +#[derive(thiserror::Error, Debug)] +#[allow(clippy::enum_variant_names)] +pub enum WriteLayerError { + #[error("{0}")] + WriteLayerMetadataError(WriteLayerMetadataError), + + #[error("{0}")] + ReplaceLayerSbomsError(ReplaceLayerSbomsError), + + #[error("{0}")] + ReplaceLayerExecdProgramsError(ReplaceLayerExecdProgramsError), + + #[error("Unexpected I/O error while writing layer: {0}")] + IoError(#[from] std::io::Error), +} + +/// Does not error if the layer doesn't exist. +pub(in crate::layer) fn delete_layer>( + layers_dir: P, + layer_name: &LayerName, +) -> Result<(), DeleteLayerError> { + let layer_dir = layers_dir.as_ref().join(layer_name.as_str()); + let layer_toml = layers_dir.as_ref().join(format!("{layer_name}.toml")); + + default_on_not_found(remove_dir_recursively(&layer_dir))?; + default_on_not_found(fs::remove_file(layer_toml))?; + + Ok(()) +} + +#[derive(thiserror::Error, Debug)] +pub enum DeleteLayerError { + #[error("I/O error while deleting layer: {0}")] + IoError(#[from] std::io::Error), +} + +pub(in crate::layer) fn replace_layer_metadata>( + layers_dir: P, + layer_name: &LayerName, + metadata: M, +) -> Result<(), WriteLayerMetadataError> { + let layer_content_metadata_path = layers_dir.as_ref().join(format!("{layer_name}.toml")); + + let content_metadata = read_toml_file::(&layer_content_metadata_path)?; + + write_toml_file( + &LayerContentMetadata { + types: content_metadata.types, + metadata, + }, + &layer_content_metadata_path, + ) + .map_err(WriteLayerMetadataError::TomlFileError) +} + +pub(in crate::layer) fn replace_layer_types>( + layers_dir: P, + layer_name: &LayerName, + layer_types: LayerTypes, +) -> Result<(), WriteLayerMetadataError> { + let layer_content_metadata_path = layers_dir.as_ref().join(format!("{layer_name}.toml")); + + let mut content_metadata = + read_toml_file::(&layer_content_metadata_path)?; + content_metadata.types = Some(layer_types); + + write_toml_file(&content_metadata, &layer_content_metadata_path) + .map_err(WriteLayerMetadataError::TomlFileError) +} + +pub(crate) fn replace_layer_sboms>( + layers_dir: P, + layer_name: &LayerName, + sboms: &[Sbom], +) -> Result<(), ReplaceLayerSbomsError> { + let layers_dir = layers_dir.as_ref(); + + if !layers_dir.join(layer_name.as_str()).is_dir() { + return Err(ReplaceLayerSbomsError::MissingLayer(layer_name.clone())); + } + + for format in SBOM_FORMATS { + default_on_not_found(fs::remove_file(cnb_sbom_path( + format, layers_dir, layer_name, + )))?; + } + + for sbom in sboms { + fs::write( + cnb_sbom_path(&sbom.format, layers_dir, layer_name), + &sbom.data, + )?; + } + + Ok(()) +} + +#[derive(thiserror::Error, Debug)] +pub enum ReplaceLayerSbomsError { + #[error("Layer doesn't exist: {0}")] + MissingLayer(LayerName), + + #[error("Unexpected I/O error while replacing layer SBOMs: {0}")] + IoError(#[from] std::io::Error), +} + +pub(in crate::layer) fn replace_layer_exec_d_programs>( + layers_dir: P, + layer_name: &LayerName, + exec_d_programs: &HashMap, +) -> Result<(), ReplaceLayerExecdProgramsError> { + let layer_dir = layers_dir.as_ref().join(layer_name.as_str()); + + if !layer_dir.is_dir() { + return Err(ReplaceLayerExecdProgramsError::MissingLayer( + layer_name.clone(), + )); + } + + let exec_d_dir = layer_dir.join("exec.d"); + + if exec_d_dir.is_dir() { + fs::remove_dir_all(&exec_d_dir)?; + } + + if !exec_d_programs.is_empty() { + fs::create_dir_all(&exec_d_dir)?; + + for (name, path) in exec_d_programs { + // We could just try to copy the file here and let the call-site deal with the + // I/O errors when the path does not exist. We're using an explicit error variant + // for a missing exec.d binary makes it easier to debug issues with packaging + // since the usage of exec.d binaries often relies on implicit packaging the + // buildpack author might not be aware of. + Some(&path) + .filter(|path| path.exists()) + .ok_or_else(|| ReplaceLayerExecdProgramsError::MissingExecDFile(path.clone())) + .and_then(|path| { + fs::copy(path, exec_d_dir.join(name)) + .map_err(ReplaceLayerExecdProgramsError::IoError) + })?; + } + } + + Ok(()) +} + +#[derive(thiserror::Error, Debug)] +pub enum ReplaceLayerExecdProgramsError { + #[error("Unexpected I/O error while replacing layer execd programs: {0}")] + IoError(#[from] std::io::Error), + + #[error("Couldn't find exec.d file for copying: {0}")] + MissingExecDFile(PathBuf), + + #[error("Layer doesn't exist: {0}")] + MissingLayer(LayerName), +} + +#[derive(thiserror::Error, Debug)] +pub enum WriteLayerMetadataError { + #[error("Unexpected I/O error while writing layer metadata: {0}")] + IoError(#[from] std::io::Error), + + #[error("Error while writing layer content metadata TOML: {0}")] + TomlFileError(#[from] TomlFileError), +} + +#[derive(thiserror::Error, Debug)] +pub enum LayerError { + #[error("{0}")] + ReadLayerError(#[from] ReadLayerError), + #[error("{0}")] + WriteLayerError(#[from] WriteLayerError), + #[error("{0}")] + DeleteLayerError(#[from] DeleteLayerError), + #[error("Cannot read generic layer metadata: {0}")] + CouldNotReadGenericLayerMetadata(TomlFileError), + #[error("Cannot read layer {0} after creating it")] + CouldNotReadLayerAfterCreate(LayerName), + #[error("Unexpected I/O error: {0}")] + IoError(#[from] std::io::Error), + #[error("Unexpected missing layer")] + UnexpectedMissingLayer, +} + +#[cfg(test)] +mod test { + use crate::layer::ReadLayerError; + use libcnb_data::generic::GenericMetadata; + use libcnb_data::layer_content_metadata::{LayerContentMetadata, LayerTypes}; + use libcnb_data::layer_name; + use serde::Deserialize; + use std::fs; + use tempfile::tempdir; + + #[test] + fn read_layer() { + #[derive(Deserialize, Debug, Eq, PartialEq)] + struct TestLayerMetadata { + version: String, + sha: String, + } + + let layer_name = layer_name!("foo"); + let temp_dir = tempdir().unwrap(); + let layers_dir = temp_dir.path(); + let layer_dir = layers_dir.join(layer_name.as_str()); + + fs::create_dir_all(&layer_dir).unwrap(); + fs::write( + layers_dir.join(format!("{layer_name}.toml")), + r#" + [types] + launch = true + build = false + cache = true + + [metadata] + version = "1.0" + sha = "2608a36467a6fec50be1672bfbf88b04b9ec8efaafa58c71d9edf73519ed8e2c" + "#, + ) + .unwrap(); + + let layer_data = super::read_layer::(layers_dir, &layer_name) + .unwrap() + .unwrap(); + + assert_eq!(layer_data.path, layer_dir); + + assert_eq!(layer_data.name, layer_name); + + assert_eq!( + layer_data.metadata.types, + Some(LayerTypes { + launch: true, + build: false, + cache: true + }) + ); + + assert_eq!( + layer_data.metadata.metadata, + TestLayerMetadata { + version: String::from("1.0"), + sha: String::from( + "2608a36467a6fec50be1672bfbf88b04b9ec8efaafa58c71d9edf73519ed8e2c" + ) + } + ); + } + + #[test] + fn read_malformed_toml_layer() { + let layer_name = layer_name!("foo"); + let temp_dir = tempdir().unwrap(); + let layers_dir = temp_dir.path(); + let layer_dir = layers_dir.join(layer_name.as_str()); + + fs::create_dir_all(layer_dir).unwrap(); + fs::write( + layers_dir.join(format!("{layer_name}.toml")), + r" + [types + build = true + launch = true + cache = true + ", + ) + .unwrap(); + + match super::read_layer::(layers_dir, &layer_name) { + Err(ReadLayerError::LayerContentMetadataParseError(toml_error)) => { + assert_eq!(toml_error.span(), Some(19..20)); + } + _ => panic!("Expected ReadLayerError::LayerContentMetadataParseError!"), + } + } + + #[test] + fn read_incompatible_metadata_layer() { + #[derive(Deserialize, Debug, Eq, PartialEq)] + struct TestLayerMetadata { + version: String, + sha: String, + } + + let layer_name = layer_name!("foo"); + let temp_dir = tempdir().unwrap(); + let layers_dir = temp_dir.path(); + let layer_dir = layers_dir.join(layer_name.as_str()); + + fs::create_dir_all(layer_dir).unwrap(); + fs::write( + layers_dir.join(format!("{layer_name}.toml")), + r#" + [types] + build = true + launch = true + cache = true + + [metadata] + version = "1.0" + "#, + ) + .unwrap(); + + match super::read_layer::(layers_dir, &layer_name) { + Err(ReadLayerError::LayerContentMetadataParseError(toml_error)) => { + assert_eq!(toml_error.span(), Some(110..148)); + } + _ => panic!("Expected ReadLayerError::LayerContentMetadataParseError!"), + } + } + + #[test] + fn read_layer_without_layer_directory() { + let layer_name = layer_name!("foo"); + let temp_dir = tempdir().unwrap(); + let layers_dir = temp_dir.path(); + let layer_dir = layers_dir.join(layer_name.as_str()); + + fs::create_dir_all(layer_dir).unwrap(); + + match super::read_layer::(layers_dir, &layer_name) { + Ok(Some(layer_data)) => { + assert_eq!( + layer_data.metadata, + LayerContentMetadata { + types: None, + metadata: None + } + ); + } + _ => panic!("Expected Ok(Some(_)!"), + } + } + + #[test] + fn read_layer_without_layer_content_metadata() { + let layer_name = layer_name!("foo"); + let temp_dir = tempdir().unwrap(); + let layers_dir = temp_dir.path(); + + fs::write(layers_dir.join(format!("{layer_name}.toml")), "").unwrap(); + + match super::read_layer::(layers_dir, &layer_name) { + Ok(None) => {} + _ => panic!("Expected Ok(None)!"), + } + } + + #[test] + fn read_nonexistent_layer() { + let layer_name = layer_name!("foo"); + let temp_dir = tempdir().unwrap(); + let layers_dir = temp_dir.path(); + + match super::read_layer::(layers_dir, &layer_name) { + Ok(None) => {} + _ => panic!("Expected Ok(None)!"), + } + } +} diff --git a/libcnb/src/layer/struct_api/handling.rs b/libcnb/src/layer/struct_api/handling.rs new file mode 100644 index 00000000..6f02da10 --- /dev/null +++ b/libcnb/src/layer/struct_api/handling.rs @@ -0,0 +1,522 @@ +use crate::layer::shared::{ + delete_layer, read_layer, replace_layer_metadata, replace_layer_types, ReadLayerError, + WriteLayerError, +}; +use crate::layer::{ + EmptyLayerCause, IntoAction, InvalidMetadataAction, LayerError, LayerRef, LayerState, + RestoredLayerAction, +}; +use crate::Buildpack; +use libcnb_common::toml_file::read_toml_file; +use libcnb_data::generic::GenericMetadata; +use libcnb_data::layer::LayerName; +use libcnb_data::layer_content_metadata::{LayerContentMetadata, LayerTypes}; +use serde::de::DeserializeOwned; +use serde::Serialize; +use std::marker::PhantomData; +use std::path::{Path, PathBuf}; + +pub(crate) fn handle_layer( + layer_types: LayerTypes, + invalid_metadata_action_fn: &dyn Fn(&GenericMetadata) -> MA, + restored_layer_action_fn: &dyn Fn(&M, &Path) -> RA, + layer_name: &LayerName, + layers_dir: &Path, +) -> crate::Result, B::Error> +where + B: Buildpack + ?Sized, + M: Serialize + DeserializeOwned, + MA: IntoAction, MAC, B::Error>, + RA: IntoAction, +{ + match read_layer::(layers_dir, layer_name) { + Ok(None) => create_layer( + layer_types, + layer_name, + layers_dir, + EmptyLayerCause::NewlyCreated, + ), + Ok(Some(layer_data)) => { + let restored_layer_action = + restored_layer_action_fn(&layer_data.metadata.metadata, &layer_data.path) + .into_action() + .map_err(crate::Error::BuildpackError)?; + + match restored_layer_action { + (RestoredLayerAction::DeleteLayer, cause) => { + delete_layer(layers_dir, layer_name).map_err(LayerError::DeleteLayerError)?; + + create_layer( + layer_types, + layer_name, + layers_dir, + EmptyLayerCause::RestoredLayerAction { cause }, + ) + } + (RestoredLayerAction::KeepLayer, cause) => { + // Always write the layer types as: + // a) they might be different from what is currently on disk + // b) the cache field will be removed by CNB lifecycle on cache restore + replace_layer_types(layers_dir, layer_name, layer_types).map_err(|error| { + LayerError::WriteLayerError(WriteLayerError::WriteLayerMetadataError(error)) + })?; + + Ok(LayerRef { + name: layer_data.name, + layers_dir: PathBuf::from(layers_dir), + buildpack: PhantomData, + state: LayerState::Restored { cause }, + }) + } + } + } + Err(ReadLayerError::LayerContentMetadataParseError(_)) => { + let layer_content_metadata = read_toml_file::( + layers_dir.join(format!("{layer_name}.toml")), + ) + .map_err(LayerError::CouldNotReadGenericLayerMetadata)?; + + let invalid_metadata_action = + invalid_metadata_action_fn(&layer_content_metadata.metadata) + .into_action() + .map_err(crate::Error::BuildpackError)?; + + match invalid_metadata_action { + (InvalidMetadataAction::DeleteLayer, cause) => { + delete_layer(layers_dir, layer_name).map_err(LayerError::DeleteLayerError)?; + + create_layer( + layer_types, + layer_name, + layers_dir, + EmptyLayerCause::InvalidMetadataAction { cause }, + ) + } + (InvalidMetadataAction::ReplaceMetadata(metadata), _) => { + replace_layer_metadata(layers_dir, layer_name, metadata).map_err(|error| { + LayerError::WriteLayerError(WriteLayerError::WriteLayerMetadataError(error)) + })?; + + handle_layer( + layer_types, + invalid_metadata_action_fn, + restored_layer_action_fn, + layer_name, + layers_dir, + ) + } + } + } + Err(read_layer_error) => Err(LayerError::ReadLayerError(read_layer_error))?, + } +} + +fn create_layer( + layer_types: LayerTypes, + layer_name: &LayerName, + layers_dir: &Path, + empty_layer_cause: EmptyLayerCause, +) -> Result, crate::Error> +where + B: Buildpack + ?Sized, +{ + crate::layer::shared::write_layer( + layers_dir, + layer_name, + &LayerContentMetadata { + types: Some(layer_types), + metadata: GenericMetadata::default(), + }, + ) + .map_err(LayerError::WriteLayerError)?; + + let layer_data = read_layer::(layers_dir, layer_name) + .map_err(LayerError::ReadLayerError)? + .ok_or(LayerError::CouldNotReadLayerAfterCreate(layer_name.clone()))?; + + Ok(LayerRef { + name: layer_data.name, + layers_dir: PathBuf::from(layers_dir), + buildpack: PhantomData, + state: LayerState::Empty { + cause: empty_layer_cause, + }, + }) +} + +#[cfg(test)] +mod tests { + use super::handle_layer; + use crate::build::{BuildContext, BuildResult}; + use crate::detect::{DetectContext, DetectResult}; + use crate::generic::{GenericError, GenericPlatform}; + use crate::layer::{EmptyLayerCause, InvalidMetadataAction, LayerState, RestoredLayerAction}; + use crate::Buildpack; + use libcnb_common::toml_file::read_toml_file; + use libcnb_data::generic::GenericMetadata; + use libcnb_data::layer_content_metadata::{LayerContentMetadata, LayerTypes}; + use libcnb_data::layer_name; + use serde::{Deserialize, Serialize}; + use tempfile::tempdir; + use toml::toml; + + #[test] + fn create_layer() { + let temp_dir = tempdir().unwrap(); + + let cause = EmptyLayerCause::RestoredLayerAction { cause: () }; + let layer_name = layer_name!("test_layer"); + let layer_ref = super::create_layer::( + LayerTypes { + launch: true, + build: true, + cache: false, + }, + &layer_name, + temp_dir.path(), + cause, + ) + .unwrap(); + + assert_eq!(layer_ref.layers_dir, temp_dir.path()); + assert_eq!(layer_ref.state, LayerState::Empty { cause }); + assert!(temp_dir.path().join(&*layer_name).is_dir()); + assert_eq!( + read_toml_file::>( + temp_dir.path().join(format!("{layer_name}.toml")) + ) + .unwrap(), + LayerContentMetadata { + types: Some(LayerTypes { + launch: true, + build: true, + cache: false, + }), + metadata: GenericMetadata::default() + } + ); + } + + #[test] + fn handle_layer_uncached() { + let temp_dir = tempdir().unwrap(); + + let layer_name = layer_name!("test_layer"); + let layer_ref = handle_layer::< + TestBuildpack, + GenericMetadata, + InvalidMetadataAction, + RestoredLayerAction, + (), + (), + >( + LayerTypes { + build: true, + launch: true, + cache: true, + }, + &|_| panic!("invalid_metadata_action callback should not be called!"), + &|_, _| panic!("restored_layer_action callback should not be called!"), + &layer_name, + temp_dir.path(), + ) + .unwrap(); + + assert_eq!(layer_ref.path(), temp_dir.path().join(&*layer_name)); + assert!(layer_ref.path().is_dir()); + assert_eq!( + read_toml_file::>( + temp_dir.path().join(format!("{layer_name}.toml")) + ) + .unwrap(), + LayerContentMetadata { + types: Some(LayerTypes { + build: true, + launch: true, + cache: true, + }), + metadata: GenericMetadata::default() + } + ); + assert_eq!( + layer_ref.state, + LayerState::Empty { + cause: EmptyLayerCause::NewlyCreated + } + ); + } + + #[test] + fn handle_layer_cached_keep() { + const KEEP_CAUSE: &str = "cause"; + + let temp_dir = tempdir().unwrap(); + let layer_name = layer_name!("test_layer"); + + // Create a layer as if it was restored by the CNB lifecycle, most notably WITHOUT layer + // types but WITH metadata. + std::fs::create_dir_all(temp_dir.path().join(&*layer_name)).unwrap(); + std::fs::write( + temp_dir.path().join(format!("{layer_name}.toml")), + "[metadata]\nanswer=42", + ) + .unwrap(); + + let layer_ref = + handle_layer::, _, (), _>( + LayerTypes { + build: true, + launch: true, + cache: true, + }, + &|_| panic!("invalid_metadata_action callback should not be called!"), + &|metadata, path| { + assert_eq!(metadata, &Some(toml! { answer = 42 })); + assert_eq!(path, temp_dir.path().join(&*layer_name.clone())); + (RestoredLayerAction::KeepLayer, KEEP_CAUSE) + }, + &layer_name, + temp_dir.path(), + ) + .unwrap(); + + assert_eq!(layer_ref.path(), temp_dir.path().join(&*layer_name)); + assert!(layer_ref.path().is_dir()); + assert_eq!( + read_toml_file::>( + temp_dir.path().join(format!("{layer_name}.toml")) + ) + .unwrap(), + LayerContentMetadata { + types: Some(LayerTypes { + build: true, + launch: true, + cache: true, + }), + metadata: Some(toml! { answer = 42 }) + } + ); + assert_eq!(layer_ref.state, LayerState::Restored { cause: KEEP_CAUSE }); + } + + #[test] + fn handle_layer_cached_delete() { + const DELETE_CAUSE: &str = "cause"; + + let temp_dir = tempdir().unwrap(); + let layer_name = layer_name!("test_layer"); + + // Create a layer as if it was restored by the CNB lifecycle, most notably WITHOUT layer + // types but WITH metadata. + std::fs::create_dir_all(temp_dir.path().join(&*layer_name)).unwrap(); + std::fs::write( + temp_dir.path().join(format!("{layer_name}.toml")), + "[metadata]\nanswer=42", + ) + .unwrap(); + + let layer_ref = + handle_layer::, _, (), _>( + LayerTypes { + build: true, + launch: true, + cache: true, + }, + &|_| panic!("invalid_metadata_action callback should not be called!"), + &|metadata, path| { + assert_eq!(metadata, &Some(toml! { answer = 42 })); + assert_eq!(path, temp_dir.path().join(&*layer_name.clone())); + (RestoredLayerAction::DeleteLayer, DELETE_CAUSE) + }, + &layer_name, + temp_dir.path(), + ) + .unwrap(); + + assert_eq!(layer_ref.path(), temp_dir.path().join(&*layer_name)); + assert!(layer_ref.path().is_dir()); + assert_eq!( + read_toml_file::>( + temp_dir.path().join(format!("{layer_name}.toml")) + ) + .unwrap(), + LayerContentMetadata { + types: Some(LayerTypes { + build: true, + launch: true, + cache: true, + }), + metadata: GenericMetadata::default() + } + ); + assert_eq!( + layer_ref.state, + LayerState::Empty { + cause: EmptyLayerCause::RestoredLayerAction { + cause: DELETE_CAUSE + } + } + ); + } + + #[test] + fn handle_layer_cached_invalid_metadata_delete() { + const DELETE_CAUSE: &str = "cause"; + + #[derive(Serialize, Deserialize)] + struct TestLayerMetadata { + planet: String, + } + + let temp_dir = tempdir().unwrap(); + let layer_name = layer_name!("test_layer"); + + // Create a layer as if it was restored by the CNB lifecycle, most notably WITHOUT layer + // types but WITH metadata. + std::fs::create_dir_all(temp_dir.path().join(&*layer_name)).unwrap(); + std::fs::write( + temp_dir.path().join(format!("{layer_name}.toml")), + "[metadata]\nanswer=42", + ) + .unwrap(); + + let layer_ref = handle_layer::< + TestBuildpack, + TestLayerMetadata, + _, + (RestoredLayerAction, &str), + &str, + _, + >( + LayerTypes { + build: true, + launch: true, + cache: true, + }, + &|metadata| { + assert_eq!(metadata, &Some(toml! { answer = 42 })); + (InvalidMetadataAction::DeleteLayer, DELETE_CAUSE) + }, + &|_, _| panic!("restored_layer_action callback should not be called!"), + &layer_name, + temp_dir.path(), + ) + .unwrap(); + + assert_eq!(layer_ref.path(), temp_dir.path().join(&*layer_name)); + assert!(layer_ref.path().is_dir()); + assert_eq!( + read_toml_file::>( + temp_dir.path().join(format!("{layer_name}.toml")) + ) + .unwrap(), + LayerContentMetadata { + types: Some(LayerTypes { + build: true, + launch: true, + cache: true, + }), + metadata: GenericMetadata::default() + } + ); + assert_eq!( + layer_ref.state, + LayerState::Empty { + cause: EmptyLayerCause::InvalidMetadataAction { + cause: DELETE_CAUSE + } + } + ); + } + + #[test] + fn handle_layer_cached_invalid_metadata_replace() { + const KEEP_CAUSE: &str = "cause"; + + #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] + struct TestLayerMetadata { + planet: String, + } + + let temp_dir = tempdir().unwrap(); + let layer_name = layer_name!("test_layer"); + + // Create a layer as if it was restored by the CNB lifecycle, most notably WITHOUT layer + // types but WITH metadata. + std::fs::create_dir_all(temp_dir.path().join(&*layer_name)).unwrap(); + std::fs::write( + temp_dir.path().join(&*layer_name).join("data.txt"), + "some_data", + ) + .unwrap(); + std::fs::write( + temp_dir.path().join(format!("{layer_name}.toml")), + "[metadata]\nanswer=42", + ) + .unwrap(); + + let layer_ref = handle_layer::( + LayerTypes { + build: true, + launch: true, + cache: true, + }, + &|metadata| { + assert_eq!(metadata, &Some(toml! { answer = 42 })); + + InvalidMetadataAction::ReplaceMetadata(TestLayerMetadata { + planet: String::from("LV-246"), + }) + }, + &|metadata, _| { + assert_eq!( + metadata, + &TestLayerMetadata { + planet: String::from("LV-246"), + } + ); + + (RestoredLayerAction::KeepLayer, KEEP_CAUSE) + }, + &layer_name, + temp_dir.path(), + ) + .unwrap(); + + assert_eq!(layer_ref.path(), temp_dir.path().join(&*layer_name)); + assert!(layer_ref.path().is_dir()); + assert_eq!( + read_toml_file::>( + temp_dir.path().join(format!("{layer_name}.toml")) + ) + .unwrap(), + LayerContentMetadata { + types: Some(LayerTypes { + build: true, + launch: true, + cache: true, + }), + metadata: TestLayerMetadata { + planet: String::from("LV-246") + } + } + ); + + assert_eq!(layer_ref.state, LayerState::Restored { cause: KEEP_CAUSE }); + } + + struct TestBuildpack; + impl Buildpack for TestBuildpack { + type Platform = GenericPlatform; + type Metadata = GenericMetadata; + type Error = GenericError; + + fn detect(&self, _: DetectContext) -> crate::Result { + unimplemented!() + } + + fn build(&self, _: BuildContext) -> crate::Result { + unimplemented!() + } + } +} diff --git a/libcnb/src/layer/struct_api/mod.rs b/libcnb/src/layer/struct_api/mod.rs new file mode 100644 index 00000000..ecc8969c --- /dev/null +++ b/libcnb/src/layer/struct_api/mod.rs @@ -0,0 +1,238 @@ +pub(crate) mod handling; + +// BuildContext is only used in RustDoc (https://github.com/rust-lang/rust/issues/79542) +#[allow(unused)] +use crate::build::BuildContext; +use crate::layer::shared::{replace_layer_exec_d_programs, replace_layer_sboms, WriteLayerError}; +use crate::layer::{LayerError, ReadLayerError}; +use crate::layer_env::LayerEnv; +use crate::sbom::Sbom; +use crate::Buildpack; +use libcnb_data::generic::GenericMetadata; +use libcnb_data::layer::LayerName; +use serde::Serialize; +use std::borrow::Borrow; +use std::collections::HashMap; +use std::marker::PhantomData; +use std::path::{Path, PathBuf}; + +/// A definition for a cached layer. +/// +/// Refer to the docs of [`BuildContext::cached_layer`] for usage examples. +pub struct CachedLayerDefinition<'a, M, MA, RA> { + /// Whether the layer is intended for build. + pub build: bool, + /// Whether the layer is intended for launch. + pub launch: bool, + /// Callback for when the metadata of a restored layer cannot be parsed as `M`. + /// + /// Allows replacing the metadata before continuing (i.e. migration to a newer version) or + /// deleting the layer. + pub invalid_metadata_action: &'a dyn Fn(&GenericMetadata) -> MA, + /// Callback when the layer was restored from cache to validate the contents and metadata. + /// Can be used to delete existing cached layers. + pub restored_layer_action: &'a dyn Fn(&M, &Path) -> RA, +} + +/// A definition for an uncached layer. +/// +/// Refer to the docs of [`BuildContext::uncached_layer`] for usage examples. +pub struct UncachedLayerDefinition { + /// Whether the layer is intended for build. + pub build: bool, + /// Whether the layer is intended for launch. + pub launch: bool, +} + +/// The action to take when the layer metadata is invalid. +#[derive(Copy, Clone, Debug)] +pub enum InvalidMetadataAction { + /// Delete the existing layer. + DeleteLayer, + /// Keep the layer, but replace the metadata. Commonly used to migrate to a newer + /// metadata format. + ReplaceMetadata(M), +} + +/// The action to take when a previously cached layer was restored. +#[derive(Copy, Clone, Debug)] +pub enum RestoredLayerAction { + /// Delete the restored layer. + DeleteLayer, + /// Keep the restored layer. It can then be used as-is or updated if required. + KeepLayer, +} + +/// Framework metadata about the layer state. +/// +/// See: [`BuildContext::cached_layer`] and [`BuildContext::uncached_layer`] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum LayerState { + /// The layer contains validated cached contents from a previous buildpack run. + /// + /// See: `restored_layer_action` in [`CachedLayerDefinition`]. + Restored { cause: RAC }, + /// The layer is empty. Inspect the contained [`EmptyLayerCause`] for the cause. + Empty { cause: EmptyLayerCause }, +} + +/// The cause of a layer being empty. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum EmptyLayerCause { + /// The layer wasn't cached in a previous buildpack run and was newly created. + NewlyCreated, + /// The layer was cached in a previous buildpack run, but the metadata was invalid and couldn't + /// be converted into a valid form. Subsequently, the layer was deleted entirely. + /// + /// See: `invalid_metadata_action` in [`CachedLayerDefinition`]. + InvalidMetadataAction { cause: MAC }, + /// The layer was cached in a previous buildpack run, but the `restored_layer_action` function + /// rejected the contents and/or metadata. + /// + /// See: `restored_layer_action` in [`CachedLayerDefinition`]. + RestoredLayerAction { cause: RAC }, +} + +/// A value-to-value conversion for layer actions. +/// +/// Similar to [`Into`], but specialized. Allowing it to also be implemented for +/// values in the standard library such as [`Result`]. +/// +/// Implement this trait if you want to use your own types as actions. +/// +/// libcnb ships with generic implementations for the majority of the use-cases: +/// - Using [`RestoredLayerAction`] or [`InvalidMetadataAction`] directly. +/// - Using [`RestoredLayerAction`] or [`InvalidMetadataAction`] directly, wrapped in a Result. +/// - Using [`RestoredLayerAction`] or [`InvalidMetadataAction`] with a cause value in a tuple. +/// - Using [`RestoredLayerAction`] or [`InvalidMetadataAction`] with a cause value in a tuple, wrapped in a Result. +pub trait IntoAction { + fn into_action(self) -> Result<(T, C), E>; +} + +// Allows to use the layer actions directly. +impl IntoAction for T { + fn into_action(self) -> Result<(T, ()), E> { + Ok((self, ())) + } +} + +// Allows to use the layer actions directly wrapped in a Result. +impl IntoAction for Result { + fn into_action(self) -> Result<(T, ()), E> { + self.map(|value| (value, ())) + } +} + +// Allows to use the layer actions directly with a cause as a tuple. +impl IntoAction for (T, C) { + fn into_action(self) -> Result<(T, C), E> { + Ok(self) + } +} + +// Allows to use the layer actions directly with a cause as a tuple wrapped in a Result. +impl IntoAction for Result<(T, C), E> { + fn into_action(self) -> Result<(T, C), E> { + self + } +} + +/// A reference to an existing layer on disk. +/// +/// Provides functions to modify the layer such as replacing its metadata, environment, SBOMs or +/// exec.d programs. +/// +/// To obtain a such a reference, use [`BuildContext::cached_layer`] or [`BuildContext::uncached_layer`]. +pub struct LayerRef +where + B: Buildpack + ?Sized, +{ + name: LayerName, + // Technically not part of the layer itself. However, the functions that modify the layer + // will need a reference to the layers directory as they will also modify files outside the + // actual layer directory. To make LayerRef nice to use, we bite the bullet and include + // the layers_dir here. + layers_dir: PathBuf, + buildpack: PhantomData, + pub state: LayerState, +} + +impl LayerRef +where + B: Buildpack, +{ + /// Returns the path to the layer on disk. + pub fn path(&self) -> PathBuf { + self.layers_dir.join(self.name.as_str()) + } + + /// Writes the given layer metadata to disk. + /// + /// Any existing layer metadata will be overwritten. The new value does not have to be of the + /// same type as the existing metadata. + pub fn write_metadata(&self, metadata: M) -> crate::Result<(), B::Error> + where + M: Serialize, + { + crate::layer::shared::replace_layer_metadata(&self.layers_dir, &self.name, metadata) + .map_err(|error| { + crate::Error::LayerError(LayerError::WriteLayerError( + WriteLayerError::WriteLayerMetadataError(error), + )) + }) + } + + /// Writes the given layer environment to disk. + /// + /// Any existing layer environment will be overwritten. + pub fn write_env(&self, env: impl Borrow) -> crate::Result<(), B::Error> { + env.borrow() + .write_to_layer_dir(self.path()) + .map_err(|error| { + crate::Error::LayerError(LayerError::WriteLayerError(WriteLayerError::IoError( + error, + ))) + }) + } + + /// Reads the current layer environment from disk. + /// + /// Note that this includes implicit entries such as adding `bin/` to `PATH`. See [`LayerEnv`] + /// docs for details on implicit entries. + pub fn read_env(&self) -> crate::Result { + LayerEnv::read_from_layer_dir(self.path()).map_err(|error| { + crate::Error::LayerError(LayerError::ReadLayerError(ReadLayerError::IoError(error))) + }) + } + + /// Writes the given SBOMs to disk. + /// + /// Any existing SBOMs will be overwritten. + pub fn write_sboms(&self, sboms: &[Sbom]) -> crate::Result<(), B::Error> { + replace_layer_sboms(&self.layers_dir, &self.name, sboms).map_err(|error| { + crate::Error::LayerError(LayerError::WriteLayerError( + WriteLayerError::ReplaceLayerSbomsError(error), + )) + }) + } + + /// Writes the given exec.d programs to disk. + /// + /// Any existing exec.d programs will be overwritten. + pub fn write_exec_d_programs(&self, programs: P) -> crate::Result<(), B::Error> + where + S: Into, + P: IntoIterator, + { + let programs = programs + .into_iter() + .map(|(k, v)| (k.into(), v)) + .collect::>(); + + replace_layer_exec_d_programs(&self.layers_dir, &self.name, &programs).map_err(|error| { + crate::Error::LayerError(LayerError::WriteLayerError( + WriteLayerError::ReplaceLayerExecdProgramsError(error), + )) + }) + } +} diff --git a/libcnb/src/layer/handling.rs b/libcnb/src/layer/trait_api/handling.rs similarity index 53% rename from libcnb/src/layer/handling.rs rename to libcnb/src/layer/trait_api/handling.rs index 39538d43..1ad3de61 100644 --- a/libcnb/src/layer/handling.rs +++ b/libcnb/src/layer/trait_api/handling.rs @@ -1,17 +1,19 @@ // This lint triggers when both layer_dir and layers_dir are present which are quite common. #![allow(clippy::similar_names)] +use super::Layer; use crate::build::BuildContext; use crate::data::layer::LayerName; use crate::data::layer_content_metadata::LayerContentMetadata; use crate::generic::GenericMetadata; -use crate::layer::{ExistingLayerStrategy, Layer, LayerData, MetadataMigration}; +use crate::layer::shared::{ + delete_layer, replace_layer_exec_d_programs, replace_layer_sboms, ReadLayerError, + WriteLayerError, +}; +use crate::layer::{ExistingLayerStrategy, LayerData, LayerError, MetadataMigration}; use crate::layer_env::LayerEnv; -use crate::sbom::{cnb_sbom_path, Sbom}; -use crate::util::{default_on_not_found, remove_dir_recursively}; +use crate::sbom::Sbom; use crate::Buildpack; -use crate::{write_toml_file, TomlFileError}; -use libcnb_data::sbom::SBOM_FORMATS; use serde::de::DeserializeOwned; use serde::Serialize; use std::collections::HashMap; @@ -22,17 +24,20 @@ pub(crate) fn handle_layer>( context: &BuildContext, layer_name: LayerName, mut layer: L, -) -> Result, HandleLayerErrorOrBuildpackError> { +) -> Result, LayerErrorOrBuildpackError> { match read_layer(&context.layers_dir, &layer_name) { Ok(None) => handle_create_layer(context, &layer_name, &mut layer), Ok(Some(layer_data)) => { let existing_layer_strategy = layer .existing_layer_strategy(context, &layer_data) - .map_err(HandleLayerErrorOrBuildpackError::BuildpackError)?; + .map_err(LayerErrorOrBuildpackError::BuildpackError)?; match existing_layer_strategy { ExistingLayerStrategy::Recreate => { - delete_layer(&context.layers_dir, &layer_name)?; + delete_layer(&context.layers_dir, &layer_name).map_err(|error| { + LayerErrorOrBuildpackError::LayerError(LayerError::DeleteLayerError(error)) + })?; + handle_create_layer(context, &layer_name, &mut layer) } ExistingLayerStrategy::Update => { @@ -54,13 +59,21 @@ pub(crate) fn handle_layer>( }, ExecDPrograms::Keep, Sboms::Keep, - )?; + ) + .map_err(|error| { + LayerErrorOrBuildpackError::LayerError(LayerError::WriteLayerError(error)) + })?; // Reread the layer from disk to ensure the returned layer data accurately reflects // the state on disk after we messed with it. - read_layer(&context.layers_dir, &layer_name)? - .ok_or(HandleLayerError::UnexpectedMissingLayer) - .map_err(HandleLayerErrorOrBuildpackError::HandleLayerError) + read_layer(&context.layers_dir, &layer_name) + .map_err(|error| { + LayerErrorOrBuildpackError::LayerError(LayerError::ReadLayerError( + error, + )) + })? + .ok_or(LayerError::UnexpectedMissingLayer) + .map_err(LayerErrorOrBuildpackError::LayerError) } } } @@ -72,11 +85,15 @@ pub(crate) fn handle_layer>( context, &generic_layer_data.content_metadata.metadata, ) - .map_err(HandleLayerErrorOrBuildpackError::BuildpackError)?; + .map_err(LayerErrorOrBuildpackError::BuildpackError)?; match metadata_migration_strategy { MetadataMigration::RecreateLayer => { - delete_layer(&context.layers_dir, &layer_name)?; + delete_layer(&context.layers_dir, &layer_name).map_err(|error| { + LayerErrorOrBuildpackError::LayerError( + LayerError::DeleteLayerError(error), + ) + })?; } MetadataMigration::ReplaceMetadata(migrated_metadata) => { write_layer( @@ -89,19 +106,28 @@ pub(crate) fn handle_layer>( }, ExecDPrograms::Keep, Sboms::Keep, - )?; + ) + .map_err(|error| { + LayerErrorOrBuildpackError::LayerError(LayerError::WriteLayerError( + error, + )) + })?; } } handle_layer(context, layer_name, layer) } - Ok(None) => Err(HandleLayerError::UnexpectedMissingLayer.into()), - Err(read_layer_error) => { - Err(HandleLayerError::ReadLayerError(read_layer_error).into()) - } + Ok(None) => Err(LayerErrorOrBuildpackError::LayerError( + LayerError::UnexpectedMissingLayer, + )), + Err(read_layer_error) => Err(LayerErrorOrBuildpackError::LayerError( + LayerError::ReadLayerError(read_layer_error), + )), } } - Err(read_layer_error) => Err(HandleLayerError::ReadLayerError(read_layer_error).into()), + Err(read_layer_error) => Err(LayerErrorOrBuildpackError::LayerError( + LayerError::ReadLayerError(read_layer_error), + )), } } @@ -109,16 +135,16 @@ fn handle_create_layer>( context: &BuildContext, layer_name: &LayerName, layer: &mut L, -) -> Result, HandleLayerErrorOrBuildpackError> { +) -> Result, LayerErrorOrBuildpackError> { let layer_dir = context.layers_dir.join(layer_name.as_str()); fs::create_dir_all(&layer_dir) - .map_err(HandleLayerError::IoError) - .map_err(HandleLayerErrorOrBuildpackError::HandleLayerError)?; + .map_err(LayerError::IoError) + .map_err(LayerErrorOrBuildpackError::LayerError)?; let layer_result = layer .create(context, &layer_dir) - .map_err(HandleLayerErrorOrBuildpackError::BuildpackError)?; + .map_err(LayerErrorOrBuildpackError::BuildpackError)?; write_layer( &context.layers_dir, @@ -130,21 +156,23 @@ fn handle_create_layer>( }, ExecDPrograms::Replace(layer_result.exec_d_programs), Sboms::Replace(layer_result.sboms), - )?; + ) + .map_err(|error| LayerErrorOrBuildpackError::LayerError(LayerError::WriteLayerError(error)))?; - read_layer(&context.layers_dir, layer_name)? - .ok_or(HandleLayerError::UnexpectedMissingLayer) - .map_err(HandleLayerErrorOrBuildpackError::HandleLayerError) + read_layer(&context.layers_dir, layer_name) + .map_err(|error| LayerErrorOrBuildpackError::LayerError(LayerError::ReadLayerError(error)))? + .ok_or(LayerError::UnexpectedMissingLayer) + .map_err(LayerErrorOrBuildpackError::LayerError) } fn handle_update_layer>( context: &BuildContext, layer_data: &LayerData, layer: &mut L, -) -> Result, HandleLayerErrorOrBuildpackError> { +) -> Result, LayerErrorOrBuildpackError> { let layer_result = layer .update(context, layer_data) - .map_err(HandleLayerErrorOrBuildpackError::BuildpackError)?; + .map_err(LayerErrorOrBuildpackError::BuildpackError)?; write_layer( &context.layers_dir, @@ -156,238 +184,35 @@ fn handle_update_layer>( }, ExecDPrograms::Replace(layer_result.exec_d_programs), Sboms::Replace(layer_result.sboms), - )?; + ) + .map_err(|error| LayerErrorOrBuildpackError::LayerError(LayerError::WriteLayerError(error)))?; - read_layer(&context.layers_dir, &layer_data.name)? - .ok_or(HandleLayerError::UnexpectedMissingLayer) - .map_err(HandleLayerErrorOrBuildpackError::HandleLayerError) + read_layer(&context.layers_dir, &layer_data.name) + .map_err(|error| LayerErrorOrBuildpackError::LayerError(LayerError::ReadLayerError(error)))? + .ok_or(LayerError::UnexpectedMissingLayer) + .map_err(LayerErrorOrBuildpackError::LayerError) } #[derive(Debug)] -pub(crate) enum HandleLayerErrorOrBuildpackError { - HandleLayerError(HandleLayerError), +pub(crate) enum LayerErrorOrBuildpackError { + LayerError(LayerError), BuildpackError(E), } -impl From for HandleLayerErrorOrBuildpackError { - fn from(e: HandleLayerError) -> Self { - Self::HandleLayerError(e) - } -} - -impl From for HandleLayerErrorOrBuildpackError { - fn from(e: DeleteLayerError) -> Self { - Self::HandleLayerError(HandleLayerError::DeleteLayerError(e)) - } -} - -impl From for HandleLayerErrorOrBuildpackError { - fn from(e: ReadLayerError) -> Self { - Self::HandleLayerError(HandleLayerError::ReadLayerError(e)) - } -} - -impl From for HandleLayerErrorOrBuildpackError { - fn from(e: WriteLayerError) -> Self { - Self::HandleLayerError(HandleLayerError::WriteLayerError(e)) - } -} - -impl From for HandleLayerErrorOrBuildpackError { - fn from(e: std::io::Error) -> Self { - Self::HandleLayerError(HandleLayerError::IoError(e)) - } -} - -#[derive(thiserror::Error, Debug)] -pub enum HandleLayerError { - #[error("Unexpected I/O error while handling layer: {0}")] - IoError(#[from] std::io::Error), - - #[error("Unexpected DeleteLayerError while handling layer: {0}")] - DeleteLayerError(#[from] DeleteLayerError), - - #[error("Unexpected ReadLayerError while handling layer: {0}")] - ReadLayerError(#[from] ReadLayerError), - - #[error("Unexpected WriteLayerError while handling layer: {0}")] - WriteLayerError(#[from] WriteLayerError), - - #[error("Expected layer to be present, but it was missing")] - UnexpectedMissingLayer, -} - -#[derive(thiserror::Error, Debug)] -pub enum DeleteLayerError { - #[error("I/O error while deleting existing layer: {0}")] - IoError(#[from] std::io::Error), -} - -#[derive(thiserror::Error, Debug)] -pub enum ReadLayerError { - #[error("Layer content metadata couldn't be parsed!")] - LayerContentMetadataParseError(toml::de::Error), - - #[error("Unexpected I/O error while reading layer: {0}")] - IoError(#[from] std::io::Error), -} - -#[derive(thiserror::Error, Debug)] -#[allow(clippy::enum_variant_names)] -pub enum WriteLayerError { - #[error("{0}")] - WriteLayerEnvError(#[from] std::io::Error), - - #[error("{0}")] - WriteLayerMetadataError(#[from] WriteLayerMetadataError), - - #[error("{0}")] - ReplaceLayerExecdProgramsError(#[from] ReplaceLayerExecdProgramsError), - - #[error("{0}")] - ReplaceLayerSbomsError(#[from] ReplaceLayerSbomsError), -} - -#[derive(thiserror::Error, Debug)] -pub enum WriteLayerMetadataError { - #[error("Unexpected I/O error while writing layer metadata: {0}")] - IoError(#[from] std::io::Error), - - #[error("Error while writing layer content metadata TOML: {0}")] - TomlFileError(#[from] TomlFileError), -} - -#[derive(thiserror::Error, Debug)] -pub enum ReplaceLayerExecdProgramsError { - #[error("Unexpected I/O error while replacing layer execd programs: {0}")] - IoError(#[from] std::io::Error), - - #[error("Couldn't find exec.d file for copying: {0}")] - MissingExecDFile(PathBuf), - - #[error("Layer doesn't exist: {0}")] - MissingLayer(LayerName), -} - -#[derive(thiserror::Error, Debug)] -pub enum ReplaceLayerSbomsError { - #[error("Layer doesn't exist: {0}")] - MissingLayer(LayerName), - - #[error("Unexpected I/O error while replacing layer SBOMs: {0}")] - IoError(#[from] std::io::Error), -} - #[derive(Debug)] -enum ExecDPrograms { +pub(in crate::layer) enum ExecDPrograms { Keep, Replace(HashMap), } #[derive(Debug)] -enum Sboms { +pub(in crate::layer) enum Sboms { Keep, Replace(Vec), } -/// Does not error if the layer doesn't exist. -fn delete_layer>( - layers_dir: P, - layer_name: &LayerName, -) -> Result<(), DeleteLayerError> { - let layer_dir = layers_dir.as_ref().join(layer_name.as_str()); - let layer_toml = layers_dir.as_ref().join(format!("{layer_name}.toml")); - - default_on_not_found(remove_dir_recursively(&layer_dir))?; - default_on_not_found(fs::remove_file(layer_toml))?; - - Ok(()) -} - -fn replace_layer_sboms>( - layers_dir: P, - layer_name: &LayerName, - sboms: &[Sbom], -) -> Result<(), ReplaceLayerSbomsError> { - let layers_dir = layers_dir.as_ref(); - - if !layers_dir.join(layer_name.as_str()).is_dir() { - return Err(ReplaceLayerSbomsError::MissingLayer(layer_name.clone())); - } - - for format in SBOM_FORMATS { - default_on_not_found(fs::remove_file(cnb_sbom_path( - format, layers_dir, layer_name, - )))?; - } - - for sbom in sboms { - fs::write( - cnb_sbom_path(&sbom.format, layers_dir, layer_name), - &sbom.data, - )?; - } - - Ok(()) -} - -fn replace_layer_exec_d_programs>( - layers_dir: P, - layer_name: &LayerName, - exec_d_programs: &HashMap, -) -> Result<(), ReplaceLayerExecdProgramsError> { - let layer_dir = layers_dir.as_ref().join(layer_name.as_str()); - - if !layer_dir.is_dir() { - return Err(ReplaceLayerExecdProgramsError::MissingLayer( - layer_name.clone(), - )); - } - - let exec_d_dir = layer_dir.join("exec.d"); - - if exec_d_dir.is_dir() { - fs::remove_dir_all(&exec_d_dir)?; - } - - if !exec_d_programs.is_empty() { - fs::create_dir_all(&exec_d_dir)?; - - for (name, path) in exec_d_programs { - // We could just try to copy the file here and let the call-site deal with the - // I/O errors when the path does not exist. We're using an explicit error variant - // for a missing exec.d binary makes it easier to debug issues with packaging - // since the usage of exec.d binaries often relies on implicit packaging the - // buildpack author might not be aware of. - Some(&path) - .filter(|path| path.exists()) - .ok_or_else(|| ReplaceLayerExecdProgramsError::MissingExecDFile(path.clone())) - .and_then(|path| { - fs::copy(path, exec_d_dir.join(name)) - .map_err(ReplaceLayerExecdProgramsError::IoError) - })?; - } - } - - Ok(()) -} - -fn write_layer_metadata>( - layers_dir: P, - layer_name: &LayerName, - layer_content_metadata: &LayerContentMetadata, -) -> Result<(), WriteLayerMetadataError> { - let layer_dir = layers_dir.as_ref().join(layer_name.as_str()); - fs::create_dir_all(layer_dir)?; - - let layer_content_metadata_path = layers_dir.as_ref().join(format!("{layer_name}.toml")); - write_toml_file(&layer_content_metadata, layer_content_metadata_path)?; - - Ok(()) -} - /// Updates layer metadata on disk -fn write_layer>( +pub(in crate::layer) fn write_layer>( layers_dir: P, layer_name: &LayerName, layer_env: &LayerEnv, @@ -397,78 +222,54 @@ fn write_layer>( ) -> Result<(), WriteLayerError> { let layers_dir = layers_dir.as_ref(); - write_layer_metadata(layers_dir, layer_name, layer_content_metadata)?; + crate::layer::shared::write_layer(layers_dir, layer_name, layer_content_metadata)?; let layer_dir = layers_dir.join(layer_name.as_str()); layer_env.write_to_layer_dir(layer_dir)?; if let Sboms::Replace(sboms) = layer_sboms { - replace_layer_sboms(layers_dir, layer_name, &sboms)?; + replace_layer_sboms(layers_dir, layer_name, &sboms) + .map_err(WriteLayerError::ReplaceLayerSbomsError)?; } if let ExecDPrograms::Replace(exec_d_programs) = layer_exec_d_programs { - replace_layer_exec_d_programs(layers_dir, layer_name, &exec_d_programs)?; + replace_layer_exec_d_programs(layers_dir, layer_name, &exec_d_programs) + .map_err(WriteLayerError::ReplaceLayerExecdProgramsError)?; } Ok(()) } -fn read_layer>( +pub(crate) fn read_layer>( layers_dir: P, layer_name: &LayerName, ) -> Result>, ReadLayerError> { - let layer_dir_path = layers_dir.as_ref().join(layer_name.as_str()); - let layer_toml_path = layers_dir.as_ref().join(format!("{layer_name}.toml")); - - if !layer_dir_path.exists() && !layer_toml_path.exists() { - return Ok(None); - } else if !layer_dir_path.exists() && layer_toml_path.exists() { - // This is a valid case according to the spec: - // https://github.com/buildpacks/spec/blob/7b20dfa070ed428c013e61a3cefea29030af1732/buildpack.md#layer-types - // - // When launch = true, build = false, cache = false, the layer metadata will be restored but - // not the layer itself. However, we choose to not support this case as of now. It would - // complicate the API we need to expose to the user of libcnb as this case is very different - // compared to all other combinations of launch, build and cache. It's the only case where - // a cache = false layer restores some of its data between builds. - // - // To normalize, we remove the layer TOML file and treat the layer as non-existent. - fs::remove_file(&layer_toml_path)?; - return Ok(None); - } - - // An empty layer content metadata file is valid and the CNB spec is not clear if the lifecycle - // has to restore them if they're empty. This is especially important since the layer types - // are removed from the file if it's restored. To normalize, we write an empty file if the layer - // directory exists without the metadata file. - if !layer_toml_path.exists() { - fs::write(&layer_toml_path, "")?; - } - - let layer_toml_contents = fs::read_to_string(&layer_toml_path)?; - let layer_content_metadata = toml::from_str::>(&layer_toml_contents) - .map_err(ReadLayerError::LayerContentMetadataParseError)?; - - let layer_env = LayerEnv::read_from_layer_dir(&layer_dir_path)?; - - Ok(Some(LayerData { - name: layer_name.clone(), - path: layer_dir_path, - env: layer_env, - content_metadata: layer_content_metadata, - })) + crate::layer::shared::read_layer(layers_dir, layer_name).and_then(|layer| { + layer + .map(|layer| { + LayerEnv::read_from_layer_dir(&layer.path) + .map_err(ReadLayerError::IoError) + .map(|env| LayerData { + name: layer.name, + path: layer.path, + env, + content_metadata: layer.metadata, + }) + }) + .transpose() + }) } #[cfg(test)] mod tests { use super::*; - use crate::data::layer_content_metadata::LayerTypes; + use crate::data::layer_content_metadata::{LayerContentMetadata, LayerTypes}; use crate::data::layer_name; + use crate::generic::GenericMetadata; + use crate::layer::shared::ReplaceLayerExecdProgramsError; use crate::layer_env::{ModificationBehavior, Scope}; use crate::read_toml_file; - use serde::Deserialize; - use std::ffi::OsString; - + use std::fs; use tempfile::tempdir; #[test] @@ -868,191 +669,4 @@ mod tests { assert!(!layer_dir.join("exec.d").exists()); } - - #[test] - fn read_layer() { - #[derive(Deserialize, Debug, Eq, PartialEq)] - struct TestLayerMetadata { - version: String, - sha: String, - } - - let layer_name = layer_name!("foo"); - let temp_dir = tempdir().unwrap(); - let layers_dir = temp_dir.path(); - let layer_dir = layers_dir.join(layer_name.as_str()); - - fs::create_dir_all(&layer_dir).unwrap(); - fs::write( - layers_dir.join(format!("{layer_name}.toml")), - r#" - [types] - launch = true - build = false - cache = true - - [metadata] - version = "1.0" - sha = "2608a36467a6fec50be1672bfbf88b04b9ec8efaafa58c71d9edf73519ed8e2c" - "#, - ) - .unwrap(); - - // Add a bin directory to test if implicit entries are added to the LayerEnv - fs::create_dir_all(layer_dir.join("bin")).unwrap(); - - // Add a file to the env directory to test if explicit entries are added to the LayerEnv - fs::create_dir_all(layer_dir.join("env")).unwrap(); - fs::write(layer_dir.join("env/CUSTOM_ENV"), "CUSTOM_ENV_VALUE").unwrap(); - - let layer_data = super::read_layer::(layers_dir, &layer_name) - .unwrap() - .unwrap(); - - assert_eq!(layer_data.path, layer_dir); - - assert_eq!(layer_data.name, layer_name); - - assert_eq!( - layer_data.content_metadata.types, - Some(LayerTypes { - launch: true, - build: false, - cache: true - }) - ); - - assert_eq!( - layer_data.content_metadata.metadata, - TestLayerMetadata { - version: String::from("1.0"), - sha: String::from( - "2608a36467a6fec50be1672bfbf88b04b9ec8efaafa58c71d9edf73519ed8e2c" - ) - } - ); - - let applied_layer_env = layer_data.env.apply_to_empty(Scope::Build); - assert_eq!( - applied_layer_env.get("PATH").cloned(), - Some(layer_dir.join("bin").into()) - ); - - assert_eq!( - applied_layer_env.get("CUSTOM_ENV"), - Some(&OsString::from("CUSTOM_ENV_VALUE")) - ); - } - - #[test] - fn read_malformed_toml_layer() { - let layer_name = layer_name!("foo"); - let temp_dir = tempdir().unwrap(); - let layers_dir = temp_dir.path(); - let layer_dir = layers_dir.join(layer_name.as_str()); - - fs::create_dir_all(layer_dir).unwrap(); - fs::write( - layers_dir.join(format!("{layer_name}.toml")), - r" - [types - build = true - launch = true - cache = true - ", - ) - .unwrap(); - - match super::read_layer::(layers_dir, &layer_name) { - Err(ReadLayerError::LayerContentMetadataParseError(toml_error)) => { - assert_eq!(toml_error.span(), Some(19..20)); - } - _ => panic!("Expected ReadLayerError::LayerContentMetadataParseError!"), - } - } - - #[test] - fn read_incompatible_metadata_layer() { - #[derive(Deserialize, Debug, Eq, PartialEq)] - struct TestLayerMetadata { - version: String, - sha: String, - } - - let layer_name = layer_name!("foo"); - let temp_dir = tempdir().unwrap(); - let layers_dir = temp_dir.path(); - let layer_dir = layers_dir.join(layer_name.as_str()); - - fs::create_dir_all(layer_dir).unwrap(); - fs::write( - layers_dir.join(format!("{layer_name}.toml")), - r#" - [types] - build = true - launch = true - cache = true - - [metadata] - version = "1.0" - "#, - ) - .unwrap(); - - match super::read_layer::(layers_dir, &layer_name) { - Err(ReadLayerError::LayerContentMetadataParseError(toml_error)) => { - assert_eq!(toml_error.span(), Some(110..148)); - } - _ => panic!("Expected ReadLayerError::LayerContentMetadataParseError!"), - } - } - - #[test] - fn read_layer_without_layer_directory() { - let layer_name = layer_name!("foo"); - let temp_dir = tempdir().unwrap(); - let layers_dir = temp_dir.path(); - let layer_dir = layers_dir.join(layer_name.as_str()); - - fs::create_dir_all(layer_dir).unwrap(); - - match super::read_layer::(layers_dir, &layer_name) { - Ok(Some(layer_data)) => { - assert_eq!( - layer_data.content_metadata, - LayerContentMetadata { - types: None, - metadata: None - } - ); - } - _ => panic!("Expected Ok(Some(_)!"), - } - } - - #[test] - fn read_layer_without_layer_content_metadata() { - let layer_name = layer_name!("foo"); - let temp_dir = tempdir().unwrap(); - let layers_dir = temp_dir.path(); - - fs::write(layers_dir.join(format!("{layer_name}.toml")), "").unwrap(); - - match super::read_layer::(layers_dir, &layer_name) { - Ok(None) => {} - _ => panic!("Expected Ok(None)!"), - } - } - - #[test] - fn read_nonexistent_layer() { - let layer_name = layer_name!("foo"); - let temp_dir = tempdir().unwrap(); - let layers_dir = temp_dir.path(); - - match super::read_layer::(layers_dir, &layer_name) { - Ok(None) => {} - _ => panic!("Expected Ok(None)!"), - } - } } diff --git a/libcnb/src/layer/public_interface.rs b/libcnb/src/layer/trait_api/mod.rs similarity index 92% rename from libcnb/src/layer/public_interface.rs rename to libcnb/src/layer/trait_api/mod.rs index c2d7a150..e6c305c2 100644 --- a/libcnb/src/layer/public_interface.rs +++ b/libcnb/src/layer/trait_api/mod.rs @@ -1,3 +1,7 @@ +// The whole API is deprecated and relies on itself for implementation. To avoid necessary warnings, +// the use of deprecated code is allowed in this module. +#![allow(deprecated)] + use crate::build::BuildContext; use crate::data::layer::LayerName; use crate::data::layer_content_metadata::{LayerContentMetadata, LayerTypes}; @@ -10,12 +14,17 @@ use serde::Serialize; use std::collections::HashMap; use std::path::{Path, PathBuf}; +pub(crate) mod handling; +#[cfg(test)] +mod tests; + /// Represents a buildpack layer written with the libcnb framework. /// /// Buildpack authors implement this trait to define how a layer is created/updated/removed /// depending on its state. To use a `Layer` implementation during build, use /// [`BuildContext::handle_layer`](crate::build::BuildContext::handle_layer). #[allow(unused_variables)] +#[deprecated = "The Layer trait API was replaced by a struct based API. Use CachedLayerDefinition or UncachedLayerDefinition."] pub trait Layer { /// The buildpack this layer is used with. type Buildpack: Buildpack; @@ -135,6 +144,7 @@ pub trait Layer { /// The result of a [`Layer::existing_layer_strategy`] call. #[derive(Eq, PartialEq, Clone, Copy, Debug)] +#[deprecated = "Part of the Layer trait API that was replaced by a struct based API."] pub enum ExistingLayerStrategy { /// The existing layer should not be modified. Keep, @@ -145,6 +155,7 @@ pub enum ExistingLayerStrategy { } /// The result of a [`Layer::migrate_incompatible_metadata`] call. +#[deprecated = "Part of the Layer trait API that was replaced by a struct based API."] pub enum MetadataMigration { /// The layer should be recreated entirely. RecreateLayer, @@ -153,6 +164,7 @@ pub enum MetadataMigration { } /// Information about an existing CNB layer. +#[deprecated = "Part of the Layer trait API that was replaced by a struct based API."] pub struct LayerData { pub name: LayerName, /// The layer's path, should not be modified outside of a [`Layer`] implementation. @@ -165,6 +177,7 @@ pub struct LayerData { /// /// Essentially, this carries additional metadata about a layer this later persisted according /// to the CNB spec by libcnb. +#[deprecated = "Part of the Layer trait API that was replaced by a struct based API."] pub struct LayerResult { pub metadata: M, pub env: Option, @@ -173,6 +186,7 @@ pub struct LayerResult { } /// A builder that simplifies the creation of [`LayerResult`] values. +#[deprecated = "Part of the Layer trait API that was replaced by a struct based API."] pub struct LayerResultBuilder { metadata: M, env: Option, @@ -255,8 +269,8 @@ impl LayerResultBuilder { /// /// This method returns the [`LayerResult`] wrapped in a [`Result`] even though its technically /// not fallible. This is done to simplify using this method in the contexts it's most often - /// used in: a layer's [create](crate::layer::Layer::create) and/or - /// [update](crate::layer::Layer::update) methods. + /// used in: a layer's [create](Layer::create) and/or + /// [update](Layer::update) methods. /// /// See [`build_unwrapped`](Self::build_unwrapped) for an unwrapped version of this method. pub fn build(self) -> Result, E> { diff --git a/libcnb/src/layer/tests.rs b/libcnb/src/layer/trait_api/tests.rs similarity index 99% rename from libcnb/src/layer/tests.rs rename to libcnb/src/layer/trait_api/tests.rs index 9b99d39e..83b0ab52 100644 --- a/libcnb/src/layer/tests.rs +++ b/libcnb/src/layer/trait_api/tests.rs @@ -14,9 +14,9 @@ use crate::data::buildpack_id; use crate::data::layer_content_metadata::LayerTypes; use crate::detect::{DetectContext, DetectResult, DetectResultBuilder}; use crate::generic::{GenericMetadata, GenericPlatform}; +use crate::layer::trait_api::handling::handle_layer; use crate::layer::{ - handle_layer, ExistingLayerStrategy, Layer, LayerData, LayerResult, LayerResultBuilder, - MetadataMigration, + ExistingLayerStrategy, Layer, LayerData, LayerResult, LayerResultBuilder, MetadataMigration, }; use crate::layer_env::{LayerEnv, ModificationBehavior, Scope}; use crate::{read_toml_file, Buildpack, Env, Target, LIBCNB_SUPPORTED_BUILDPACK_API}; diff --git a/test-buildpacks/readonly-layer-files/src/main.rs b/test-buildpacks/readonly-layer-files/src/main.rs index 3d7340f8..96b264fa 100644 --- a/test-buildpacks/readonly-layer-files/src/main.rs +++ b/test-buildpacks/readonly-layer-files/src/main.rs @@ -1,3 +1,7 @@ +// This test buildpack uses the older trait Layer API. It will be updated to the newer API +// before the next libcnb.rs release. +#![allow(deprecated)] + mod layer; use crate::layer::TestLayer; diff --git a/test-buildpacks/sbom/src/main.rs b/test-buildpacks/sbom/src/main.rs index ce021365..f8543cf7 100644 --- a/test-buildpacks/sbom/src/main.rs +++ b/test-buildpacks/sbom/src/main.rs @@ -1,3 +1,7 @@ +// This test buildpack uses the older trait Layer API. It will be updated to the newer API +// before the next libcnb.rs release. +#![allow(deprecated)] + mod test_layer; mod test_layer_2;