-
Notifications
You must be signed in to change notification settings - Fork 18.4k
Description
go modules resolve the minimum required version of dependencies based on the go.mod
of modules used by a project. This works well for small modules where the list of dependencies in go.mod
is representative for the code, but is problematic for larger modules that provide many packages.
Let's illustrate with an example.
Example: project "foobar"
This is our "foobar" project. It uses logrus to print "Hello foobar":
mkdir foobar && cd foobar
cat > main.go <<EOF
package main
import (
"github.com/sirupsen/logrus"
)
func main() {
logrus.Info("Hello foobar")
}
EOF
go mod init foobar
Project foobar requires logrus 1.7.0 - it can't currently use a newer version of this dependency, because it has change in behavior that causes foobar to break (of course, SemVer should guard us against breaking changes, but the world isn't perfect, so we specify we want v1.7.0):
go mod edit -require github.com/sirupsen/logrus@v1.7.0
go mod tidy
cat go.mod
module foobar
go 1.18
require github.com/sirupsen/logrus v1.7.0
require golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect
No project would be complete without an AppArmor check, and containerd provides an implementation for this. It's a small package, and pkg/apparmor
has no dependencies, other than Go stdlib (apparmor.go, apparmor_linux.go, apparmor_unsupported.go).
cat > main.go <<EOF
package main
import (
"github.com/containerd/containerd/pkg/apparmor"
"github.com/sirupsen/logrus"
)
func main() {
if apparmor.HostSupports() {
logrus.Infof("Running Foobar Deluxe, with AppArmor")
} else {
logrus.Infof("Running Foobar Basic")
}
}
EOF
So we add the containerd v1.6.2 dependency:
go mod edit -require github.com/containerd/containerd@v1.6.2
go mod tidy
However, checking our go.mod
;
Adding containerd as a dependency forced us to also updates the logrus
dependency to a newer (for us "incompatible") version (as well as updates the golang.org/x/sys
dependency), even though none of the files in containerd's pkg/apparmor
package use this dependency
$ cat go.mod
module foobar
go 1.18
require github.com/sirupsen/logrus v1.8.1
require (
github.com/containerd/containerd v1.6.2
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
)
While the project "foobar" example is of course just to illustrate the problem, this issue is problematic for many real-life situations (some more details below).
Current "solutions"
There are various "solutions" for this problem, but they're not for the faint of heart.
A. Use replace
rules
Projects can add a replace
rule to force go modules to use a fixed version. While this helps "us" (the "foobar project" maintainers) build and ship our project, it's a different story for consumers of the "foobar" module; replace
rules are not transitional, and because of this, all projects depending on our module will (out of the box) be "forced" to use the newer version, unless they copy the replace rules.
Various (sometimes "high profile") projects currently use replace
rules (read them, and weep! 😭😭😭), e.g.: containerd and kubernetes. Worst of all, using replace rules (especially when use to the extend as the kubernetes example) throws out one of the biggest advantages of go modules; version resolution / management.
B. Separate modules (multi-module repository)
We can ask the containerd
maintainers to provide pkg/apparmor
as a separate module. While this may be an option in some cases, maintaining a multi-module repository gets complicated fast;
- modules become separate entities (need to be tested separately)
- if there's dependencies between packages (now separate modules),
replace
rules may be needed to make sure code it tested against the version in the repository (not the latest released version of the module) - if these modules are expected to be used externally, each of them has to be tagged/released separately
- which, in combination with "inter-module dependencies" also means tagging and releasing MUST be performed in the correct order (to make sure all modules use the latest release)
In short; unless "you're Google", or have a dedicated team of engineers to set up automation to perform these actions (e.g., the complicated release procedures for the kubernetes project), maintaining a multi-module repository is complicated, and in many a heavy burden for project maintainers.
C. Separate modules (multiple repositories)
We can ask the containerd
maintainers to provide pkg/apparmor
as a separate module in a separate repository.
While this gives a clearer separation between the modules, it shares the same (if not more) problems as the previous solution. Maintaining a separate repository can add significant overhead for project maintainers (and in some cases may be restricted due to (company) policies). In addition, not all packages may be suitable to become a module / project of their own (let's not encourage creating another "leftpad").
D. Just copy the code! (It's open source, y'all!)
Unfortunately, this solution has been chosen on many occasions. I don't think this needs explaining why this should not be a preferred solution.
What did you expect to see? (proposed solution)
I'd like to see go modules to only consider version resolution based on the packages that are actually consumed from a module. Go modules conflates all packages in a repository, resulting in the (main) go module / go.mod
to become a collection of all possible dependencies that may be needed (depending on which packages are consumed from the module).
While go modules won't use dependencies if they're not used by any code, version resolution is still be influenced by them (see the example above). I'm not very familiar with the internals of go module's tooling, but I think go has all the "building blocks" available to make this possible;
It's able to provide which imports a package needs:
go list -json ./pkg/apparmor/ | jq .Imports
[
"os",
"sync"
]
With that information, it could;
- take all dependencies listed in the module's
go.mod
- remove all direct dependencies that are not used by the packages that are consumed
- use the remaining dependencies to perform version resolution
And, if in future containerd's pkg/apparmor
would introduce a new dependency, that's the moment it gets its "right to vote" in the version-resolution for that dependency.