Description
What steps did you take and what happened?
Upgrade capi components with clusterctl
from v1.5.2
to v1.6.0
clusterctl upgrade apply
completes successfully. However, on the next change I make to the cluster, reconciliation for Machines gets stuck due to this error when reading the provider machine object:
"Reconciler error" err="failed to retrieve DockerMachine external object "my-ns"/"m-docker-etcd-1705429991949-cpfqz": failed to get restmapping: failed to get API group resources: unable to retrieve the complete list of server APIs: infrastructure.cluster.x-k8s.io/v1alpha4: the server could not find the requested resource" controller="machine" controllerGroup="cluster.x-k8s.io" controllerKind="Machine" Machine="my-ns/m-docker-etcd-fr7bh" namespace="my-ns" name="m-docker-etcd-fr7bh"
This error continues on loop and is not resolved until the container is restarted. Once the container is restarted, the reconciliation continues without any other issue.
This error also appears (sometimes) in the MachineSet and MachineDeployment controllers.
Note: I haven't been able to replicate this using the capi official artifacts and only using EKS-A's fork. I believe this is just because of the nature of the race condition and not because of the difference in code (I haven't been able to find even one patch in the codepath of this issue).
What did you expect to happen?
All controllers should be able to continue reconciliation after upgrading with clusterctl
.
Cluster API version
v1.5.2
to v1.6.0
Kubernetes version
1.27
Anything else you would like to add?
TLDR: capi v1.6.0
marks v1alpha4
apis as not served. The problem comes from controller-runtime
caching the results of the call to list APIGroup
s. If this call returns v1alpha4
as one of the available versions for infrastructure.cluster.x-k8s.io
, the client will try to get the APIResource
definition for v1alpha4
. If the call to get this APIResource is made after the api is marked as not served, the client will receive a not found error. Since v1alpha4
has been cached already as available, the client will try to keep making this call and receive the same error forever.
This is the stack for the previously mentioned error:
sigs.k8s.io/controller-runtime/pkg/client/apiutil.(*mapper).fetchGroupVersionResources
sigs.k8s.io/[email protected]/pkg/client/apiutil/restmapper.go:294
sigs.k8s.io/controller-runtime/pkg/client/apiutil.(*mapper).addKnownGroupAndReload
sigs.k8s.io/[email protected]/pkg/client/apiutil/restmapper.go:191
sigs.k8s.io/controller-runtime/pkg/client/apiutil.(*mapper).RESTMapping
sigs.k8s.io/[email protected]/pkg/client/apiutil/restmapper.go:122
sigs.k8s.io/controller-runtime/pkg/client/apiutil.IsGVKNamespaced
sigs.k8s.io/[email protected]/pkg/client/apiutil/apimachinery.go:96
sigs.k8s.io/controller-runtime/pkg/client/apiutil.IsObjectNamespaced
sigs.k8s.io/[email protected]/pkg/client/apiutil/apimachinery.go:90
sigs.k8s.io/controller-runtime/pkg/cache.(*multiNamespaceCache).Get
sigs.k8s.io/[email protected]/pkg/cache/multi_namespace_cache.go:202
sigs.k8s.io/controller-runtime/pkg/cache.(*delegatingByGVKCache).Get
sigs.k8s.io/[email protected]/pkg/cache/delegating_by_gvk_cache.go:44
sigs.k8s.io/controller-runtime/pkg/client.(*client).Get
sigs.k8s.io/[email protected]/pkg/client/client.go:348
sigs.k8s.io/cluster-api/controllers/external.Get
sigs.k8s.io/cluster-api/controllers/external/util.go:43
sigs.k8s.io/cluster-api/internal/controllers/machine.(*Reconciler).reconcileExternal
sigs.k8s.io/cluster-api/internal/controllers/machine/machine_controller_phases.go:106
sigs.k8s.io/cluster-api/internal/controllers/machine.(*Reconciler).reconcileInfrastructure
sigs.k8s.io/cluster-api/internal/controllers/machine/machine_controller_phases.go:256
sigs.k8s.io/cluster-api/internal/controllers/machine.(*Reconciler).reconcile
sigs.k8s.io/cluster-api/internal/controllers/machine/machine_controller.go:297
sigs.k8s.io/cluster-api/internal/controllers/machine.(*Reconciler).Reconcile
sigs.k8s.io/cluster-api/internal/controllers/machine/machine_controller.go:222
sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller).Reconcile
sigs.k8s.io/[email protected]/pkg/internal/controller/controller.go:119
sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller).reconcileHandler
sigs.k8s.io/[email protected]/pkg/internal/controller/controller.go:316
sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller).processNextWorkItem
sigs.k8s.io/[email protected]/pkg/internal/controller/controller.go:266
sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller).Start.func2.2
sigs.k8s.io/[email protected]/pkg/internal/controller/controller.go:227
runtime.goexit
runtime/asm_amd64.s:1598
The first time a resource from a particular group is requested, the restmapper for the client detects that group hasn't been seen before and it gets the definition for all the available versions of that group. The rest mapper caches both the group and the versions for that group.
If there is an error trying to get one of the available versions, the rest mapper aborts and returns an error. The restmapper doesn't distinguish between different errors, so a 404 will result in this call failing until either the group is re-created or the cached is invalidated (the only way to do that is to restart the program).
The issue here is that if the first call to get the available versions returns a version that has been (or will be soon) deleted, the client becomes incapable of making requests for any resource of that group, independently of the version.
My hypothesis here is that the first call to get the APIGroup is made either just before the CRDs are updated or immediately after but before the kube api server cache is refreshed. This call then returns v1alpha4
. And immediately after, the restmapper's call to get the APIResource for infrastructure.cluster.x-k8s.io/v1alpha4
returns a not found error since this version has already been marked as not served in the CRD.
IMO the long term solution is to not fail if a cached group version is not found. Actually, this has already been implemented in controller-runtime. It has been released as part of v0.17.0
but since it's marked as a breaking change, it doesn't seem like it will be backported to v0.16
. We already bumped controller-runtime to v0.17.0
but it won't be backported to our v1.6
branch, so we can't leverage this fix.
I propose to update external.Get
to be able to identify this issue and restart the controller when it happens. This restart should happen at most once (immediately after a CRD upgrade) and it suspect it won't be frequent, since it seems that until now I'm the only one who faced this race condition.
In addition, we could change the upgrade logic in clusterctl
to:
- Update all the CRDs (for core and all providers)
- Wait for the api server to return only the served versions for all groups and only after
- Scale back up the controller deployments.
This wouldn't be enough to guarantee the issue doesn't happen, so I don't think this can be an alternative to restarting the controller when this issue happens. Given this change is more involved and this is only required as a short term solution for the v1.6 releases, I would vote to only implement the first change for now.
Label(s) to be applied
/kind bug
/area clusterctl