profile picture

The Kubernetes and go modules versioning system

May 21, 2021 - kubernetes versions

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:

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:

Just let me quote https://semver.org/ here:

Given a version number MAJOR.MINOR.PATCH, increment the:

  1. MAJOR version when you make incompatible API changes,
  2. MINOR version when you add functionality in a backward compatible manner, and
  3. 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 kubernetes v1.x.y)
  • Semver tags of v0.x.y (corresponding to kubernetes v1.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?