Skip to content

Commit

Permalink
Migrate go_deps to struct layer API (#339)
Browse files Browse the repository at this point in the history
* Migrate go_deps layer to struct layer API

* Drop go_clean, which is no longer used

* Drop infallible result and logging side effects
  • Loading branch information
joshwlewis authored Feb 19, 2025
1 parent 747a19e commit 65eb616
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 102 deletions.
15 changes: 0 additions & 15 deletions buildpacks/go/src/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,6 @@ pub(crate) enum Error {
Exit(ExitStatus),
}

/// Run `go clean -tags heroku`. Useful for clearing the modcache or buildcache.
///
/// # Errors
///
/// Returns an error of the command exit code is not 0 or if there is an IO
/// issue with the command.
pub(crate) fn go_clean<S: AsRef<str>>(flag: S, go_env: &Env) -> Result<(), Error> {
let status = Command::new("go")
.args(["clean", "-tags", "heroku", flag.as_ref()])
.envs(go_env)
.status()?;

status.success().then_some(()).ok_or(Error::Exit(status))
}

/// Run `go install -tags heroku pkg [..pkgn]`. Useful for compiling a list
/// of packages and installing each of them in `GOBIN`. This command is module
/// aware, and will download required modules as a side-effect.
Expand Down
135 changes: 58 additions & 77 deletions buildpacks/go/src/layers/deps.rs
Original file line number Diff line number Diff line change
@@ -1,108 +1,89 @@
use crate::{cmd, GoBuildpack, GoBuildpackError};
use crate::{GoBuildpack, GoBuildpackError};
use libcnb::build::BuildContext;
use libcnb::data::layer_content_metadata::LayerTypes;
use libcnb::layer::{ExistingLayerStrategy, Layer, LayerData, LayerResult, LayerResultBuilder};
use libcnb::data::layer_name;
use libcnb::layer::{
CachedLayerDefinition, EmptyLayerCause, InvalidMetadataAction, LayerState, RestoredLayerAction,
};
use libcnb::layer_env::{LayerEnv, Scope};
use libcnb::{Buildpack, Env};
use libherokubuildpack::log::log_info;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;

const LAYER_VERSION: &str = "1";
const MAX_CACHE_USAGE_COUNT: f32 = 100.0;
const CACHE_ENV: &str = "GOMODCACHE";
const CACHE_DIR: &str = "cache";

/// A layer that caches the go modules cache
pub(crate) struct DepsLayer {
pub(crate) go_env: Env,
}

#[derive(Deserialize, Serialize, Clone, PartialEq)]
pub(crate) struct DepsLayerMetadata {
// Using float here due to [an issue with lifecycle's handling of integers](https://github.com/buildpacks/lifecycle/issues/884)
cache_usage_count: f32,
layer_version: String,
}

#[derive(thiserror::Error, Debug)]
pub(crate) enum DepsLayerError {
#[error("Couldn't create Go modules cache layer: {0}")]
Create(std::io::Error),
#[error("Couldn't clean Go modules cache: {0}")]
Clean(#[from] cmd::Error),
}

impl Layer for DepsLayer {
type Buildpack = GoBuildpack;
type Metadata = DepsLayerMetadata;

fn types(&self) -> LayerTypes {
LayerTypes {
/// Create or restore the layer for the go modules cache (non-vendored dependencies)
pub(crate) fn handle_deps_layer(
context: &BuildContext<GoBuildpack>,
) -> libcnb::Result<LayerEnv, GoBuildpackError> {
let layer_ref = context.cached_layer(
layer_name!("go_deps"),
CachedLayerDefinition {
build: true,
launch: false,
cache: true,
}
}
invalid_metadata_action: &|_| InvalidMetadataAction::DeleteLayer,
restored_layer_action: &|restored_metadata: &DepsLayerMetadata, _| {
if restored_metadata.cache_usage_count >= MAX_CACHE_USAGE_COUNT {
return (
RestoredLayerAction::DeleteLayer,
restored_metadata.cache_usage_count,
);
}
(
RestoredLayerAction::KeepLayer,
restored_metadata.cache_usage_count,
)
},
},
)?;

fn create(
&mut self,
_ctx: &BuildContext<Self::Buildpack>,
layer_path: &Path,
) -> Result<LayerResult<Self::Metadata>, GoBuildpackError> {
log_info("Creating new Go modules cache");
let cache_dir = layer_path.join(CACHE_DIR);
fs::create_dir(&cache_dir).map_err(DepsLayerError::Create)?;
LayerResultBuilder::new(DepsLayerMetadata {
cache_usage_count: 1.0,
layer_version: LAYER_VERSION.to_string(),
})
.env(LayerEnv::new().chainable_insert(
Scope::Build,
libcnb::layer_env::ModificationBehavior::Override,
CACHE_ENV,
cache_dir,
))
.build()
match layer_ref.state {
LayerState::Empty {
cause: EmptyLayerCause::NewlyCreated,
} => (),
LayerState::Empty {
cause: EmptyLayerCause::RestoredLayerAction { .. },
} => log_info("Discarding expired Go modules cache"),
LayerState::Empty { .. } => log_info("Discarding invalid Go modules cache"),
LayerState::Restored { .. } => log_info("Reusing Go modules cache"),
}

fn update(
&mut self,
_ctx: &BuildContext<Self::Buildpack>,
layer: &LayerData<Self::Metadata>,
) -> Result<LayerResult<Self::Metadata>, GoBuildpackError> {
LayerResultBuilder::new(DepsLayerMetadata {
cache_usage_count: layer.content_metadata.metadata.cache_usage_count + 1.0,
layer_version: LAYER_VERSION.to_string(),
})
.env(LayerEnv::new().chainable_insert(
Scope::Build,
libcnb::layer_env::ModificationBehavior::Override,
CACHE_ENV,
layer.path.join(CACHE_DIR),
))
.build()
let mut cache_usage_count = 1.0;
match layer_ref.state {
LayerState::Restored {
cause: previous_cache_usage_count,
} => cache_usage_count += previous_cache_usage_count,
LayerState::Empty { .. } => {
log_info("Creating new Go modules cache");
let cache_dir = layer_ref.path().join(CACHE_DIR);
fs::create_dir(&cache_dir).map_err(DepsLayerError::Create)?;
layer_ref.write_env(LayerEnv::new().chainable_insert(
Scope::Build,
libcnb::layer_env::ModificationBehavior::Override,
CACHE_ENV,
cache_dir,
))?;
}
}
layer_ref.write_metadata(DepsLayerMetadata { cache_usage_count })?;
layer_ref.read_env()
}

fn existing_layer_strategy(
&mut self,
_ctx: &BuildContext<Self::Buildpack>,
layer: &LayerData<Self::Metadata>,
) -> Result<ExistingLayerStrategy, <Self::Buildpack as Buildpack>::Error> {
if layer.content_metadata.metadata.cache_usage_count >= MAX_CACHE_USAGE_COUNT
|| layer.content_metadata.metadata.layer_version != LAYER_VERSION
{
log_info("Expired Go modules cache");
// Go restricts write permissions in cache folders, which blocks libcnb
// from deleting the layer in a `Recreate` scenario. Go clean will
// delete the entire cache, including the restricted access files.
let mut go_env = self.go_env.clone();
go_env.insert(CACHE_ENV, layer.path.join(CACHE_DIR));
cmd::go_clean("-modcache", &go_env).map_err(DepsLayerError::Clean)?;
return Ok(ExistingLayerStrategy::Recreate);
}
log_info("Reusing Go modules cache");
Ok(ExistingLayerStrategy::Update)
impl From<DepsLayerError> for libcnb::Error<GoBuildpackError> {
fn from(value: DepsLayerError) -> Self {
libcnb::Error::BuildpackError(GoBuildpackError::DepsLayer(value))
}
}
12 changes: 2 additions & 10 deletions buildpacks/go/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ mod tgz;

use heroku_go_utils::vrs::GoVersion;
use layers::build::{BuildLayer, BuildLayerError};
use layers::deps::{DepsLayer, DepsLayerError};
use layers::deps::{handle_deps_layer, DepsLayerError};
use layers::dist::{handle_dist_layer, DistLayerError};
use layers::target::{TargetLayer, TargetLayerError};
use libcnb::build::{BuildContext, BuildResult, BuildResultBuilder};
Expand Down Expand Up @@ -93,15 +93,7 @@ impl Buildpack for GoBuildpack {
if Path::exists(&context.app_dir.join("vendor").join("modules.txt")) {
log_info("Using vendored Go modules");
} else {
go_env = context
.handle_layer(
layer_name!("go_deps"),
DepsLayer {
go_env: go_env.clone(),
},
)?
.env
.apply(Scope::Build, &go_env);
go_env = handle_deps_layer(&context)?.apply(Scope::Build, &go_env);
}

go_env = context
Expand Down

0 comments on commit 65eb616

Please sign in to comment.