-
Notifications
You must be signed in to change notification settings - Fork 40
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
RFC: Bundler Version Locking #29
Conversation
6459b08
to
e8abfd7
Compare
This proposal sounds neat. FWIW, adding this behaviour to the current version switcher has already been proposed and have a couple of thumbs up: rubygems/rubygems#3318. In my opinion, it makes sense and should've been there from the beginning. Something I'd also like to see is the possibility of locking the bundler version to ensure that everybody uses the same version, even if the dependency is not explicit in the |
Honestly, I'm against it being a completely separate feature like that. The whole point of this proposal was to make it more user-friendly and build on existing functionality. By having it be a separate thing, it removes the ability for users to rely on prior knowledge, and feels as though it undermines a significant part of the proposal. If the version switcher is just removed, with no alternative, you get the Bundler 1.x behavior back. You can then just put Bundler in your gem file and run Also: wouldn't making it a config variable require committing the Bundler config file? iirc, that's the same place credentials go right now, isn't it? (I vaguely recall an issue about that.) |
Absolutely, I reread your proposal and essentially already covers this. We should probably clarify the behavior in presence of a lockfile (even if it's consistent with the current behavior, but probably doens't hurt anyways). If I understand your proposal correctly, it's this:
Would that be it? |
@deivid-rodriguez yes, exactly. |
👍 This sounds really great! 👍 The only problem I see in here is how to safely switch in between bundler versions. I'm not sure initially proposed 🤔 In theory bundler version could be duplicated in lockfile since |
@simi yeah, I'm not fully sold on the I still need to go through and revise this a bit based on this discussion; I'll try to get to that soon. |
Okay, took a lot longer than I'd have liked, but I'm finally getting back to this today. Proof of conceptAssuming this script is saved as #!/usr/bin/env ruby
require 'bundler'
bundler_dep = Bundler.load.dependencies.find { |d| d.name == 'bundler' }
if bundler_dep
result = Gem.install(bundler_dep).find { |g| g.name == 'bundler' }
Kernel.exec("bundle", "_#{result.version}_", *ARGV)
end
# If integrated into Bundler, this next line would be removed and we'd just continue as normal.
Kernel.exec("bundle", *ARGV) Then you can do this:
Safety of
|
After a quick romp through the C code, I've confirmed that as long as you provide at least one argument and avoid the first variant described in the docs, Ruby will never use the shell to run things passed to @simi given that^, what're your thoughts on |
…ndlerVersionFinder being removed.
I've cleaned everything up a bit, and removed the assumption that BundlerVersionFinder is removed — it turns out the approach I came up with can coexist with it. They may be redundant in some cases, however. I'm not sure. @deivid-rodriguez @simi could you take another look at this when you have some time? |
I made a rough implementation of it here: rubygems/rubygems#4076 Still needs tests and such, though. |
If I understand it well, this should automate the following scenario:
I think the key in here is to run both But see this case:
It is not running in the original environment (with changed env var in here). Could that be potential security problem? Also there is hardcoded Those are my initial thoughts. 🤔 Wouldn't be possible to introduce |
@simi yes, that is the workflow it automates, but there's some subtle differences — mainly that it installs bundler first and avoids installing anything besides Bundler using the wrong Bundler version). I'm unsure if the changed environment has security implications. Will need to look into that more. Good catch. 👍 The bundle command is hard-coded in what I wrote, but I suspect there's a way to reliably get the name used for the I think the binstub should be able to handle everything. If it can, that's probably the better approach, since I it would avoid both problems you mentioned. |
I made a crude proof-of-concept using a modified version of the bundler binstub I have on my system. The way it reads the Gemfile is absolutely not the correct approach, but I think it proves that it's theoretically possible to use the binstub to do what would be needed for this RFC. Here's the modified binstub: #!/usr/bin/ruby
#
# This file was generated by RubyGems.
#
# The application 'bundler' is installed as part of a gem, and
# this file is here to facilitate running it.
#
require 'rubygems'
version = ">= 0.a"
str = ARGV.first
if str
str = str.b[/\A_(.*)_\z/, 1]
end
if str and Gem::Version.correct?(str)
version = str
ARGV.shift
else
# TODO: Use RubyGems to find this information, not this trash fire.
class KludgeBucket
attr_accessor :bundler_version
def initialize
eval open('Gemfile').read
end
def gem(name, version=nil, *)
if name == 'bundler' && !version.nil?
@bundler_version = version
end
end
def method_missing(*); end
end
bundler_version = KludgeBucket.new.bundler_version
if bundler_version
# TODO: Figure out how to avoid needing to manually specify user_install here.
# It's in my ~/.gemrc, but it doesn't work here, unsurprisingly.
result = Gem.install("bundler", bundler_version, {user_install: true}).find { |g| g.name == 'bundler' }
version = "#{bundler_version}"
end
end
if Gem.respond_to?(:activate_bin_path)
load Gem.activate_bin_path('bundler', 'bundle', version)
else
gem "bundler", version
load Gem.bin_path("bundler", "bundle", version)
end And here's an example of using it (where that code is saved as
(EDIT: Realized I was using a modified version of Bundler; uninstalled that and used 2.1.4 to confirm the code from rubygems/rubygems#4076 wasn't the thing making it work.) |
…ementation suggestions since it's clear more discussion needs to happen.
Hi! I was planning to come back to this after the rubygems 3.2.0 + bundler 2.2.0 release, so here I am. First some thoughts after the release. I broke some things for people. It was unintentional, but it happened :(. It turns out bundler is very subject to breaking people's workflows when we make changes. I'm sure previous maintainers will agree with me and I can confirm it. Sometimes we simply fail to evaluate how breaking the changes we make will be, and other times we choose to call some potentially breaking things "bug fixes" in favour of distributing fixed behaviour in exchange for breaking some edge cases relying on the wrong behaviour, because we decide that it's very unlikely that those edge cases happen. So given that, I believe the best way to ensure that bundler keeps working for a given application is to make sure that all developers and environments of the application use the exact same code, bit by bit that already worked once. That means always choosing the So that would be my suggestion to modify this RFC. Regarding the implementation, the earliest we can do this, the better of course. So binstub seems like a great place. However, I'm not sure people will like that we silently install stuff under the hood when running things like |
@deivid-rodriguez my understanding is that your suggestion is basically: If Assuming my understanding of your suggestion is correct, the problem I have is this: from my perspective, putting If people want to specify it in their Gemfile and want an exact Bundler version (as opposed to a range), they can just pin it there. The way that makes the most sense to me, and takes into account the
I think this strikes a good balance of keeping backwards-compatibility while adding new functionality and opening the window to removing the reliance on What're your thoughts, @deivid-rodriguez? |
Yeah, I now strongly believe that I also felt somewhat strongly about this before, but I decided to give in since other people disagreed. However, after the last bundler release I feel even more strongly about it. The point of the lockfile is a having a way to lock your dependencies, all of them, even if not explicitly specified in the As per why locking dependencies, it's about determinism. I don't want my collegue and I banging our heads against the wall because a command is doing something for me and something different for my collegue when we are apparently using the same code. Turns out we're not because I was using bundler 2.2.0 and my collegue bundler 2.1.4. I don't want CI of people starting to fail because we released bundler 2.2 with a bug. When we released bundler 2.2, we got this issue: "Bundler is not respecting the lockfile". And they were right (it was due to using an old rubygems version, current rubygems uses the exact version in the
In my opinion, the PR you created is a very good idea (if changed to install the |
Okay, talked to @deivid-rodriguez and there's been a lot of miscommunication/confusion on the bundler version switcher in general, but I think we've sorted out what the expectations are. The key moment in that discussion for me was when it became clear that some of the behavior I was originally bothered by was a bug, not a feature. The conclusion we came to is to do something along the lines of:
I think my PR (rubygems/rubygems#4076) handles steps 2-3, but still needs some refinement and to implement step 1. I'm gonna call it a day for now, but it feels like we're making good progress on this. ^.^ |
I'm currently rewriting my RFC, and will want feedback when I'm finished with that. However, to revisit this:
I think this boils down to the concept of implicit vs explicit dependencies. The things in your Gemfile are explicit dependencies, but "the version of Bundler I used at the time I ran this command" is an implicit dependency. My general interpretation of this is that explicit dependencies should override implicit dependencies, but there's some nuance I missed initially: If they conflict, the explicit dependency should override the constraint. If they overlap, the implicit dependency should refine the constraint. Turns out that's exactly what the updated idea in my last comment does. |
ef12137
to
3b8ac25
Compare
… prior discussions.
text/0000-bundler-version-locking.md
Outdated
|
||
There are many times where locking your Bundler version is useful. | ||
The existence of `BundlerVersionFinder` shows that, but the initial attempt | ||
caused innumerable problems. This is an attempt to resolve those problems. |
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.
Let's expand on the motivation section. Some draft with ideas from my side:
Bundler is used by many people in many different scenarios. Experience has proven that releasing changes in bundler can result if breaking behaviour for people for whom the current version is working just fine. Even if no backwards incompatibiltiy was intended.
These problems could be avoided by making sure the dependency on bundler for people is locked just like all the other dependencies. After all, one of the main purposes of bundler is to lock your dependencies so that all developers and environments of an application run the exact same code, thus avoiding "works on my machine" issues, and breakage because a third party dependency was released.
This proposal expands this philosophy to the bundler dependency itself. Bundler already records the bundler version that generated a given lockfile in the lockfile itself, so by always running that version we make sure that developers always get a deterministic working version when working on a project.
Developers can still upgrade bundler, and we will of course encourage them to do so, but we don't want everyone to silently and automatically be updated just because we released a new version. This is also a quality of life improvement for the bundler maintainers team, because we won't get a lot of issues at the same time right after we release, but instead issues will be reported on a more distributed manner as people choose to upgrade. This will reduce our chances to get burnt out and overwhelmed with work.
Agreed. I made a comment with some suggestion to expand that section. |
Some test suites are using Bundler in really elaborated way, that makes it hard to remove the Bundler. Now there will be not just hard to remove Bundler dependency, but this allows to depend on quirks of some specific version of Bundler.
This is not just about lockfile, many times it is actually "easier" to recreate the Gemfile altogether just to remove all possible dependencies of some specific versions. This adds one additional worry. |
Thanks for explaining your concerns. It seems clear to me that while developing bundler we shouldn't focus on making it easier to remove bundler. I am convinced that this is a good move, and unless I get a lot of push back from other maintainers, this will happen. In principle, your concerns doesn't look too bad to me, but obviously you know best what will cause issues for you, so I apologize for creating some overhead for you. Hopefully all the other times when I tried to make your work easier can compensate for this one 😅. |
Could you please consider to provide a way to disable this feature? Of course I'd prefer something like |
@voxik I'm not opposed to adding a way to disable this. FYI, however, my RubyGems/Bundler work is pretty sporadic because I'm trying to find a house right now. I probably won't get back to this RFC until at least January 25th, if not sometime February. |
David pointed me at this thread as this proposal seems like it would solve some challenges I'm facing. Perhaps my needs/wants could help flesh out the motivation section? Something like: Motivations: SecurityScanningIt is not currently possible to determine from the source code of a project which version of Bundler it will run. This is in contrast to every other Ruby dependency – every gem a project depends on has the exact version captured in the Gemfile.lock file, and the Ruby version is recorded both in the Gemfile and often a This makes it possible to quickly and easily 'scan' a project for security issues with their dependencies. Tools like bundler-audit (and many, many others) can warn developers if any of their dependencies have known security issues, and doing so is so easy that Github can do this at scale. This makes it very easy to alert developers of issues with their dependencies so they update them faster, shrinking the window during which they are vulnerable to exploitation. All of their dependencies, that is, except Bundler. bundler-audit does not, currently, check the Bundler version – and if you read that discussion you can see the complexity of attempting to do so. Bundler has security vulnerabilities, just like any gem, and it can sometimes be extremely important to ensure that a vulnerable version is not in use. For example, multiple bugs have occurred over the years that affect dependency resolution from private vs public sources. Any organisation using private repositories would find it very useful to be able to statically check that none of their repositories are using versions of Bundler vulnerable to dependency confusion attacks – currently the only way to check what version of Bundler is being used is to run it. Guaranteeing that the version of Bundler specified in the lockfile is the version that will be run makes it possible to statically assess projects' Bundler versions, bringing it in-line with all the other gems in use. Locking Bundler versionEnsuring the specified version of Bundler is used enables the static scanning use-case detailed above, but it is also a useful feature directly, as well. If a vulnerability is found in Bundler and a patch is released, it's important to make sure everyone and everything is using that latest version. For build machines, production systems, and other systems that are controlled and defined by code this is already possible, but having this functionality built-in to Bundler would simplify the process. Currently this could be achieved by, for example, adding an explicit "gem install bundler:x.y.z" in a build script. With the Bundler version controlled by the lockfile, updating Bundler becomes the same as updating any other gem, no changes to build scripts (pipeline definitions, Dockerfile, etc) required. It would also mean that developers would immediately be using the updated version too, which is something not so easily achievable today. While production machines are of course the most important to secure, a successful dependency confusion attack resulting in remote code execution on a developer's machine is still an undesirable scenario. With the Bundler version controlled by the lockfile, developers would be moved on to the patched version of Bundler as soon as they pull down the updated lockfile. I hope this is useful. If it is, please feel free to copy/paste all or part of it as is useful, or re-jig it if you prefer. |
Okay maybe it took a bit longer. 😂 Added a lot more to the Motivation section, based on what @deivid-rodriguez and @zofrex said previously! |
I'll address this one first, since it's separate from the rest: I initially had very limited time I could set aside for this, so I focused on fleshing out the implementation details and getting to a proof-of-concept as quickly as I could. Hopefully the expanded Motivation section provides more context.
This all seems to assume that codebases relying on quirks of specific Bundler versions is a problem this will create, but I'm rather confused by that implication since part of the motivation for this RFC is that very problem already existing in the wild. |
Separate gem sources broke bundler-2.1 a little rubygems/rubygems#4381 (comment). If production runs bundler-2.1 and developer do Locking bundler version would prevent that. |
@simi it looks like |
|
To clarify, the Bundler version that might be installed on the fly will be installed like any other gem in the Gemfile.lock, right? |
No, the idea is that the version installed on the fly will be installed alongside the running bundler version. As of today, bundler is always installed globally, it never gets installed to By the way, I just noticed yesterday that the current PR might actually be doing what you suggest, but if that's the case it's unintended 😅, I'll verify that and fix it. |
Caching is based off the |
The "caching" for this feature would be: if we are running the exact bundler version if the |
The problem is then e.g. for ruby/setup-ruby (and other CIs) we'll end up downloading that specific bundler version every time, even if the cache already exists. Also there is the case where the default gem home is not writable, but yet
I'd think it's not a problem, we'll have some other version of Bundler running initially anyway and then execve() to the right one, after it is installed. So the initial bundler can and likely already does look at the bundler path, should use that to find all available bundler versions, and if it's already there should just use it. |
To make that last sentence clearer, in https://github.com/duckinator/rfcs/blob/bundler-version-locking/text/0000-bundler-version-locking.md#example-1 |
For just one single gem install? Any uncached
Sure, this will be handled smoothly.
This is a good point, actually! I think the current implementation is doing just that and although unintended in the first place, it seems indeed much better! I will verify this. |
You're correct that for non-head Rubies which ship with Bundler 2, setup-ruby currently does I think conceptually it's nice to treat bundler "like any other gem in the Gemfile/Gemfile.lock", except that of course if the initial bundler doesn't have the right version we should install only that bundler, exec to it and install the rest of the gems with that bundler. |
Oh right, I misread that code, you actually do this in more cases than I thought, which confirms it shouldn't be an issue even if we went with the initial approach. I'll still have a look at your suggestion since it seems nice anyways! |
Actually, I think you implemented at some point in |
Yes, once this land we wouldn't install bundler manually anymore in setup-ruby for Rubies which ship with a recent enough RubyGems/Bundler (which has this functionality). |
As a note: the functionality for this wound up getting merged before the RFC. (Whoops.) Not sure how to handle this RFC now, honestly. |
We should just merge it. :) |
NOTE: This RFC is proposed as an alternative to the "Bundler version switcher", and assumes that is being removed first. Whether or not to remove it should be discussed elsewhere.This RFC no longer assumes BundlerVersionFinder is removed, and there is a proof-of-concept (see #29 (comment)) implemented without removing it, but they may be redundant in some cases.
Rendered