Unnecessary transitive dependencies have crept in and are now limiting our ability to move independently of unrelated projects.
HyperShift depends on several upstream libraries using Go modules, including:
- k8s.io/client-go
- sigs.k8s.io/controller-runtime
- k8s.io/api
HyperShift also depends on the following Go modules for the purpose of having Go types representing the third-party CRD APIs we leverage:
- sigs.k8s.io/cluster-api
- sigs.k8s.io/cluster-api-provider-aws
It's important to note the only reason we declare these API dependencies is to gain use of the Go types in our codebase. This is convenient but is in no way a technical constraint. For example, if we really wanted to, we could use unstructured types to achieve the same effect.
The CRD API modules we depend on are not structured as Go submodules; therefore, when HyperShift declares a Go dependency on these API modules, HyperShift inherits those projects' own internal dependencies transitively. As a result, we currently have irreconcilable conflicting versions of the machinery libraries listed above.
The net effect is our application has imposed upon it serious limitations by unrelated third-party applications. For example, the version of the Kubernetes API client or controller-runtime we use in HyperShift should not be in any way dictated by the fact that an unrelated third-party application (e.g. cluster-api-provider-aws) also uses a conflicting version controller-runtime.
This coupling of application dependencies is unacceptable and must be eliminated. The upstream projects which seek to provide publicly consumable API types as Go modules need to make changes to separate these very different concerns:
- Providing public API types
- Providing an application that is a consumer of the API types
Options
We have options for regaining control of our dependency graph. Here are some I thought of:
- Use unstructured types instead of the upstream Go types.
- Convince the upstream projects to extract their APIs into new Go modules which themselves declare no unreasonable external dependencies.
- Example: Separate API repositories
- Example: Nested Go modules in the same repository
- Copy and keep in sync the Go API types we want from their upstream sources.
- Some of the existing upstream API types are also doing anti-consumer things like declaring dependencies on k8s machinery and controller-runtime interfaces, so a degree of post-processing is inevitable right now with this approach.
- Generate our own internal versions of the Go API types from the OpenAPI schemas provided by the upstream projects.
Beyond
After the immediate problems are resolved, we must remain dilligent with our dependencies going forward, taking care not to stumble back into a similar situation. Unfortunately, Go modules and the communities that use them make it very easy to inadvertantly end up in a web of unintended transitive dependencies. One mitigation is to simply agree on a principle that all changes that affect the dependency graph should be carefully scrutinized and viewed through a skeptical lens (i.e. "do we really understand the implications of accepting this dependency?")
Postscript: sigs.k8s.io/cluster-api/util/patch
The use of the sigs.k8s.io/cluster-api/util/patch
package has found its way into the codebase, and for understandable reasons (it seems to be useful in some contexts, DRY, etc.) However, regardless of how useful it is, the fact remains it is published as part of the sigs.k8s.io/cluster-api
module and so brings along all the problems described above with the API types. If this utility is truly desirable for controller authors generally, the code should be packaged and published with public library consumers in mind: in a separate Go submodule with a minimal/narrow set of dependencies, in a separate repo, or incorporated into an existing upstream project focused on enabling controller authors (e.g. controller-runtime).
Until then, the sigs.k8s.io/cluster-api/util/patch
dependency must be eliminated one way or another, e.g. copying the code, replacing it with controller-runtime equivalents, persuading upstream to change how it's published, etc.