ScaledObject targeting apps/v1 StatefulSet resolves to statefulsets.apps.kruise.io when OpenKruise CRDs are installed (RESTMapper group-prefix match in scale client)
Report
On a cluster where the OpenKruise CRDs are installed (no Kruise CRs needed — just the CRDs), a ScaledObject whose scaleTargetRef points at a plain apps/v1 StatefulSet never becomes Ready. KEDA looks the object up as statefulsets.apps.kruise.io instead of statefulsets.apps and reports not found.
Root cause (verified against the same library versions KEDA 2.20.2 is built with; details below): the operator calls the client-go scale client with a GroupResource (no version). The scale client resolves that via RESTMapper.ResourceFor(gr.WithVersion("")), and apimachinery's DefaultRESTMapper.ResourcesFor falls back to group-prefix matching when it does not find an exact group match. apps is a prefix of apps.kruise.io, so Kruise's statefulsets resource wins the lookup.
Deployment targets are unaffected because the apps.kruise.io group has no deployments resource. Kruise CloneSet/Advanced StatefulSet targets are also unaffected (exact group match).
Expected Behavior
A ScaledObject targeting apps/v1 StatefulSet should resolve /scale on statefulsets.apps, regardless of which other API groups (with apps as a name prefix) exist in the cluster.
Actual Behavior
ScaledObject sample-sts: Ready=False
message: statefulsets.apps.kruise.io "sample-sts" not foundSame result for a brand-new, empty apps/v1 StatefulSet with only a cron trigger, so it is not specific to the workload.
Operator log:
ERROR Reconciler error {"controller": "scaledobject", ..., "namespace": "demo", "name": "sample-sts",
"error": "statefulsets.apps.kruise.io \"sample-sts\" not found"}Steps to Reproduce the Problem
Install OpenKruise (v1.9.1 here; only the CRDs matter). No CloneSet / Advanced StatefulSet objects are required.
Install KEDA 2.20.2.
Create any
apps/v1StatefulSet, e.g.apiVersion: apps/v1 kind: StatefulSet metadata: { name: sts-probe, namespace: default } spec: serviceName: sts-probe replicas: 1 selector: { matchLabels: { app: sts-probe } } template: metadata: { labels: { app: sts-probe } } spec: containers: [{ name: pause, image: registry.k8s.io/pause:3.9 }]Create a ScaledObject for it:
apiVersion: keda.sh/v1alpha1 kind: ScaledObject metadata: { name: sts-probe, namespace: default } spec: scaleTargetRef: apiVersion: apps/v1 kind: StatefulSet name: sts-probe minReplicaCount: 0 maxReplicaCount: 1 triggers: - type: cron metadata: { timezone: Etc/UTC, start: "0 0 * * *", end: "0 1 * * *", desiredReplicas: "1" }kubectl get scaledobject sts-probe→READY=False, condition messagestatefulsets.apps.kruise.io "sts-probe" not found.
Minimal Go reproduction outside KEDA (client-go v0.35.5 + controller-runtime v0.23.3, i.e. the versions KEDA 2.20.2 depends on; controller-runtime dynamic RESTMapper + scale.NewForConfig exactly as in pkg/k8s/scaleclient.go):
RESTMapping(apps/StatefulSet, v1) -> apps/v1, Resource=statefulsets OK
mapper.ResourceFor(apps//statefulsets) -> apps.kruise.io/v1beta1, statefulsets WRONG
scaleClient.Scales(ns).Get(ctx, {apps statefulsets}, "sample-sts")
-> statefulsets.apps.kruise.io "sample-sts" not found (what KEDA reports)
client.Get(unstructured apps/v1 StatefulSet) -> OKWhere it happens (line numbers from main at the time of writing):
controllers/keda/scaledobject_controller.goL424-425 and L472-478:gr := gvkr.GroupResource()thenScaleClient.Scales(ns).Get(ctx, gr, name, ...)— the version that was already resolved intogvkris dropped here.pkg/k8s/scaleclient.goL52-54: scale client is built withmgr.GetRESTMapper()(controller-runtime dynamic mapper, backed by apimachineryDefaultRESTMapper).- client-go
scale/client.goL105 (v0.35.5):c.mapper.ResourceFor(resource.WithVersion("")). - apimachinery
pkg/api/meta/restmapper.goL233-246 (v0.35.5): "if you didn't find an exact match, match on group prefixing. This allows storageclass.storage to match storageclass.storage.k8s.io" —appsprefix-matchesapps.kruise.io.
Possible fixes on the KEDA side (either avoids the version-less lookup):
- Resolve the scale subresource with the full
GroupVersionResourcefromgvkr(e.g. useRESTMapping/ the already-knowngvkr.GVR()), instead of passing a bareGroupResourceto the scale client; or - Wrap the RESTMapper handed to
scale.NewForConfigso thatResourceForwith an empty version only accepts exact group matches.
Workaround for users: use Deployment, or switch the target to Kruise Advanced StatefulSet (apps.kruise.io/v1beta1, which KEDA resolves correctly).
Logs from KEDA operator
ERROR Reconciler error {"controller": "scaledobject", "controllerGroup": "keda.sh", "controllerKind": "ScaledObject",
"ScaledObject": {"name":"sample-sts","namespace":"demo"}, "namespace": "demo", "name": "sample-sts",
"error": "statefulsets.apps.kruise.io \"sample-sts\" not found"}KEDA Version
2.20.2
Kubernetes Version
1.36 (Alibaba Cloud ACK, v1.36.2-aliyun)
Platform
Alibaba Cloud
Scaler Details
Any — reproduced with cron; first hit with the HTTP add-on (external-push). The scaler is irrelevant, the failure is in target resolution.
Would you be open to contributing a fix?
- Yes, I am willing to contribute a fix
Anything else?
This also affects any other CRD whose group starts with apps and defines a statefulsets resource; OpenKruise is just the common case. Other API groups that are prefixes of other groups (batch vs batch.something.io) would hit the same path if resource names overlap.
Related but different: #4924 (wrong apiVersion from GVKR resolution) and #7332 (empty GVKR for CloneSet) — those are about gvkr itself; here gvkr is correct and the version is lost afterwards.
Source: kedacore/keda