-
Notifications
You must be signed in to change notification settings - Fork 8
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
Add higher level environment handling #30
Conversation
71d4b76
to
57dce3f
Compare
LayerEnv becomes LayerEnvDelta as it can only handle one set of env entries. For layer paths, we have a separate, implicit, list of deltas that must not be overwritten directly by user provided entries. LayerEnvDelta is also required to support env/ env.build/ env.launch/ as well as process specific layer env entries.
Code in the same module requires the entries to have a consistent order for both iterating over and comparing it. To ensure that consistency, we now use the appropiate collection type: BTreeSet.
To make it more consistent with the ecosystem. Also adds Default implementions for both types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for working on this!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was a beast to review. I'm betting much more so to actually write. Great job!
It was easier to review than I was originally anticipating. Credit to you and Rust.
I left some comments as I went. Nothing blocking except maybe for the case of mis-spelling a process.
For style, I generally like a space after markdown headers here are some docs for diesel.rs that have an extra space after headers https://docs.diesel.rs/src/diesel/query_builder/functions.rs.html#80-82. If you want I could send you suggestions for the rest of them.
/// logic that uses the build tool to download dependencies. The download process does not need to | ||
/// know the layer name or any logic how to construct `PATH`. | ||
/// | ||
/// # Applying the delta |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extremely pedantic, but can we get whitespace after headers. It helps me pick them out in the source.
/// # Applying the delta | |
/// # Applying the delta | |
/// |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not against adding newlines after markdown headings, but I'm hesitant to do the changes to this PR as we have other headings outside of this PR as well. If you feel strongly about this, I suggest we make DR about this, figure out how to enforce this automatically via a lint and ship a dedicated PR with the lint and the fixes. Is that okay for you?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was curious to see what rustdocs/markdown related lints Clippy had already - there are a few listed at https://rust-lang.github.io/rust-clippy/stable/index.html but none that cover this case.
I did find rust-lang/rust-clippy#1007 which covers adding more docs related lints.
/// | ||
/// let mut layer_env = LayerEnv::new(); | ||
/// layer_env.insert(TargetLifecycle::All, ModificationBehavior::Append, "VAR", "bar"); | ||
/// layer_env.insert(TargetLifecycle::All, ModificationBehavior::Default, "VAR2", "default"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I'm looking at this code, I'm curious if there's a more concise way to represent this logic:
layer_env.insert(TargetLifecycle::All, ModificationBehavior::Append, "VAR", "bar");
For example (without implementation constraints) could be:
layer_env.all.append("VAR", "bar");
layer_env.build.append("VAR", "bar");
layer_env.launch.append("VAR", "bar");
Or something like:
layer_env.append_all("VAR", "bar");
layer_env.append_build("VAR", "bar");
layer_env.append_launch("VAR", "bar");
What you've got now is great to move forward. As I'm trying to figure out what makes a "good" interface in Rust, I would be interested to hear your thoughts (Again, non-blocking...we can take it offline to chat).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Filed as #73
/// env.insert("VAR", "foo"); | ||
/// env.insert("VAR2", "previous-value"); | ||
/// | ||
/// let modified_env = layer_env.apply(TargetLifecycle::Build, &env); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was confused by this TargetLifecycle::Build
here. I couldn't figure out why TargetLifecycle::All
was being specified above, but just build here. If I'm understanding correctly it's because we're currently in a build environment so we only want to apply modifications to build to the current env.
That makes sense as I type it out, but it was unclear on first pass. Right now I'm trying to think of cases where we would want to assign to the current env but have it not be build. I'm not sure I can come up with any.
I'm also left somewhat wanting to compress/combine LayerEnv and Env updates. It's a two step process but seems like I always want to do them in lockstep. I would need to see it utilized in a buildpack, but I bet that I'll insert the env var and then forget to update the current env.
Maybe some interface like this would help prevent that situation:
layer_env.insert_and_apply(TargetLifecycle::All, ModificationBehavior::Default, "VAR2", "default", &env);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added this comment to #73
let mut process_deltas = vec![&self.all]; | ||
if let Some(process_specific_delta) = self.process.get(&process) { | ||
process_deltas.push(process_specific_delta); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can fail silently if someone passes in a process that they've typo-d TargetLifecycle::Process("w3b")
instead of "web"
. It looks like if that happens then the process would get ALL of the env vars which is likely not want they want (but maybe better than nothing). We should warn or error (panic).
We've got 2 cases, either someone typo-d when creating the delta or when applying the delta. To be on the safe side we shouldn't allow applying to a process we've not inserted into (I think). Alternatively, maybe issue a warning. I guess I'm suggesting we panic in that situation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like if that happens then the process would get ALL of the env vars
I can see why you think that, but that's not the case. self.all
contains all env var deltas that apply to all target lifecycles, not all deltas across all lifecycles.
With that out of the way, I don't think panicking is a good idea here. There is nothing wrong to apply the deltas for a process that doesn't have specific environment variables. If the user of this library passes in the wrong process type string, this is nothing the library should attempt to fix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess I'm struggling to see the use-case where someone would be using process-specific .apply()
during the build, to understand what the user expectations might be? (I can see why we need to write out process-specific env files, but in my mind they mostly would be useful for runtime.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there is a use-case during build it's most likely an edge-case. This PR includes support for applying them because it is mostly free in terms of required code. If we'd remove the ability to apply process specific environment deltas, we'd need more code as TargetLifecycle
would be split up into two: one for specifying, one for applying. I can imagine the code being harder to follow too.
Co-authored-by: Ed Morley <501702+edmorley@users.noreply.github.com>
Thank you @edmorley for the spelling and sentence structure corrections! |
|
||
/// Returns a cloned value corresponding to the given key. | ||
pub fn get(&self, key: impl AsRef<OsStr>) -> Option<OsString> { | ||
self.inner.get(key.as_ref()).cloned() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Malax Isn't it usually common here to return a borrowed and not cloned from an object?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Filed #74
} | ||
} | ||
|
||
mod test { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't this be in #[cfg(test)]
so it's not built for non test envs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is where rust-lang/rust-clippy#1719 would be useful :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Filed #75
I've created GUS-W-9959585 to track adding the next part of this. |
Adds higher level environment handling, decoupling layer environment variables from the filesystem. This is the first step towards a declarative API for layer environment variables where buildpack authors don't have to write environment variables to disk, greatly improving testability due to no side-effects.
In a nutshell, this implements the layer environment variable handling that is done in the CNB lifecycle at runtime. I ported over the relevant tests from the CNB lifecycle source to ensure testing parity.
This will be integrated into layer lifecycles in a dedicated PR (let's face it, this PR is already too large).
Please refer to the included RustDoc for an overview how this works and is used. I copied the RustDoc for
LayerEnv
below:LayerEnv RustDoc
Represents environment variable modifications of a Cloud Native Buildpack layer.
Cloud Native Buildpacks can add a special directory to their layer directories to modify the
environment of subsequent buildpacks, the running container or specific processes at launch.
The rules for these modifications are described in the relevant section of the specification.
This type decouples this information from the file system, providing a type-safe in-memory
representation of the environment delta that is specified in the
env/*
directories of a layer.Using this type, libcnb can provide declarative APIs that enable buildpack authors to easily
test their layer environment variable logic since they no longer write them to disk manually.
One use-case are environment variables that are modified by a layer that are required by the
same buildpack in later stages of the build process. For example, a buildpack might install a
build tool (i.e. Apache Maven) in one layer and adding the main binary to
PATH
via theenv
directory of that layer. The same buildpack then wants to execute Maven to download dependencies
to a different layer. By using
LayerEnv
, the buildpack can encode these changes in atype and, in addition to passing it to libcnb which will persist it to disk, pass it to the
logic that uses the build tool to download dependencies. The download process does not need to
know the layer name or any logic how to construct
PATH
.Applying the delta
LayerEnv
is not a static set of environment variables, but a delta. Layers can modify existingvariables by appending or prepending or setting new ones only conditionally. If you only need a
static set of environment variables, see [
Env
].To apply a
LayerEnv
delta to a givenEnv
, use [LayerEnv::apply
] like so:Implicit Entries
Some directories in a layer directory are be implicitly added to the layer environment if they
exist. The prime example for this behaviour is the
bin
directory. If it exists, its path willbe automatically appended to
PATH
using the operating systems path delimiter as the delimiter.A full list of these special directories can be found in the
Cloud Native Buildpack specification.
libcnb supports these, including all precedence and lifecycle rules, when a
LayerEnv
is readfrom disk: