The Kubernetes and go modules versioning system
May 21, 2021 -While go modules have been with us for a while already and in general seems to be a stable solution they still give some issues occasionally. Recently I stuck trying to figure out how to map Kubernetes version (format 1.XX.YY) to the Kubernetes libraries versions (format 0.XX.YY).
In the end, this post was written as a historical overview and, mostly, as a personal reminder.
TL;DR:
- To find a compatible version for your target cluster and any library from
k8s.io/{libName}
just use the same version as your cluster has, but replace major v1 with v0 (for example, for cluster "1.20.1" usek8s.io/client-go@v0.20.1
) - do not use
k8s.io/{libName}@kubernetes-VERSION
or be prepared for problems aftergo get -u
. - check the skew to find out the skew policy for the different versions.
What is it all about?
In the project I work on we have a bunch of dependencies from k8s.io staging libraries we use to communicate with kube-api and cluster. You also probably going to use some of those libraries in case if you are writing some CDR controller or just a small piece of glue code to automate anything in your cluster or maybe you are working on kubectl plugin.
Let's say we have running target cluster version v1.21.0, how to figure out, which version of the k8s.io/client-go
we should use?
My first naive idea was to directly map them 1 to 1, let's try:
go get k8s.io/client-go@v1.21.0
go: finding k8s.io/client-go v1.21.0
go: finding k8s.io v1.21.0
go: finding k8s.io/client-go v1.21.0
go get k8s.io/client-go@v1.21.0: k8s.io/client-go@v1.21.0: invalid version: unknown revision v1.21.0
That doesn't work. Let's check which versions do we even have in the git repo:
# git clone git@github.com:kubernetes/client-go.git
# cd client-go
# git tag
The output (depending on how much time in the future you are doing this) will have something like:
kubernetes-1.10.0
kubernetes-1.10.0-alpha.0
kubernetes-1.10.0-alpha.1
<a lot of lines are skipped>
kubernetes-1.21.0
...
kubernetes-1.21.1
...
v0.17.0
v0.17.1
...
v0.21.0-alpha.1
v0.21.0-alpha.2
v0.21.0-alpha.3
v0.21.0-beta.0
v0.21.0-beta.1
v0.21.0-rc.0
v0.21.1
...
The natural choice for me would be to use the tag kubernetes-VERSION
.
Will it work? Yes, it will. Is it the best choice? In fact, no, thanks to pseudo-versions and semantic versioning.
To understand why I need first to put more light on the whole semver system and how it is adapted in the go modules.
What is semver
Semver stands for "semantic version". It's a versioning scheme, which forces any artifact to have three components in the version:
- Major
- Minor
- Patch
Just let me quote https://semver.org/ here:
Given a version number MAJOR.MINOR.PATCH, increment the:
- MAJOR version when you make incompatible API changes,
- MINOR version when you add functionality in a backward compatible manner, and
- PATCH version when you make backward compatible bug fixes. Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.
Whenever we change MAJOR we automatically claim, that there are no guarantees that your code would work. In fact, in AOT compilation languages, there is no guaranty that the code would even compile. The opposite with minor and patch changes - we automatically claim, that there should be no breaking changes in those releases.
But that's not all.
According to the specification there is a special case that claim there are no guarantees at all:
Major version zero (0.y.z) is for initial development. Anything MAY change at any time. The public API SHOULD NOT be considered stable.
That's all great stuff, but how it exactly makes choosedk8s.io/client-go@kubernetes-1.20.1
not the optimal one?
How semver is used by go modules
Long story short, whenever go get
command gets so-called revision identifier. That identifier is used to select proper code revision in the underlying repository.
If that revision happened to be also tagged with proper semver tagging, then go modules going to use semver. Otherwise, the pseudo version is going to be used.
The pseudo-versions is a mechanism to resolve some non-semver identifiers by creating on the fly some non-existence semver tag (see the linked documentation section above). It also respects the specification in terms of version zero:
In semantic versioning, major version v0 is for initial development, indicating no expectations of stability or backward compatibility. Major version v0 does not appear in the module path, because those versions are preparation for v1.0.0, and v1 does not appear in the module path either.
So how to map versions?
What's in all of it to us? If we use an old-style tag like kubernetes-VERSION
it will add the proper version but only until next time you try to update deps with go get -u
. After running the go get -u
, the created pseudo version would be considered as something that need to be updated (because it starts with v.0.0.0) and the version that would be chosen as a current latest
would be something from the old times, when kubernetes components didn't even use proper semver scheme.
To deal with proper updates and semver, according to the KEP the modern versioning scheme suggest following:
- Non-semver tags of
kubernetes-1.x.y
(corresponding to kubernetesv1.x.y
)- Semver tags of
v0.x.y
(corresponding to kubernetesv1.x.y
)
That means, basically all we need (as stated in the TL;DR) is just to use the same MINOR.PATCH versions but with MAJOR version equals to 0.
so?
- Historically, the k8s repo was there way before
go modules
landed and adopted by the community, so some of the old tags could mess version auto-selection. - The go.sum now looks eaiser to understand (because it uses proper semver tags and not pseudo-versions).
- It relaxes the possible guaranties for backward compatibility by not having the major version be equal to 1.
- According to the k8s version policy there is no criteria for shipping 2.0 (or any other MAJOR level version changes).
- Technically, according to semver specification, there are no guaranties at all since components live in v0 major version.