Skip to content

fix(transformer): recover the GroupVersionKind of an owner whose TypeMeta is empty - #6139

Merged
RongGu merged 1 commit into
fluid-cloudnative:masterfrom
cheyang:chore/harden-owner-reference-gvk
Jul 29, 2026
Merged

fix(transformer): recover the GroupVersionKind of an owner whose TypeMeta is empty#6139
RongGu merged 1 commit into
fluid-cloudnative:masterfrom
cheyang:chore/harden-owner-reference-gvk

Conversation

@cheyang

@cheyang cheyang commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to the review observation on #6138 (thanks @cheyang).

Problem

transformer.GenerateOwnerReferenceFromObject read the kind and the apiVersion straight from the object's TypeMeta:

APIVersion: obj.GetObjectKind().GroupVersionKind().GroupKind().Group + "/" + obj.GetObjectKind().GroupVersionKind().Version,
Kind:       obj.GetObjectKind().GroupVersionKind().Kind,

A typed client is allowed to hand back objects with an empty TypeMeta. In that case the produced reference has an empty Kind and "/" as its APIVersion — a reference the API server rejects, and which an owner-based watch cannot resolve back to its owner (Owns() / EnqueueRequestForOwner match on group+kind).

Two of the chains fed by this helper do end up under an owner-based watch: DataLoad and DataMigrate render a batchv1.Job whose ownerReference comes from here, and dataload_controller.go / datamigrate_controller.go both register Owns(&batchv1.Job{}).

The unit tests of the DataLoad value transformers encoded that broken output as their expectation, in 5 packages:

Owner: &common.OwnerReference{
    APIVersion:         "/",     // <- and no Kind at all
    ...

Change

Resolve the GVK from a scheme that knows the fluid API types when the object's TypeMeta is empty:

gvk := obj.GetObjectKind().GroupVersionKind()
if gvk.Empty() {
    if resolved, err := apiutil.GVKForObject(obj, fluidScheme); err == nil {
        gvk = resolved
    }
}

Objects that do carry a TypeMeta are unaffected (existing expectations with data.fluid.io/v1alpha1 and even data.fluid.io/v1beta1 still hold), so the only references this changes are ones the API server would have rejected anyway.

The 20 stale expectations across pkg/ddc/{alluxio,jindo,jindofsx,jindocache,juicefs} now expect Kind: DataLoad / APIVersion: data.fluid.io/v1alpha1.

Tests

  • pkg/utils/transformer/owner_reference_test.go: new table covering an empty TypeMeta for Dataset, AlluxioRuntime and DataLoad, asserting the kind is recovered and the apiVersion is the fluid group/version. Verified these fail without the source change (14 failed assertions) and pass with it.
$ go test -gcflags="all=-N -l" -count=1 ./pkg/utils/transformer/...
ok      github.com/fluid-cloudnative/fluid/pkg/utils/transformer   1.694s   (25/25 specs)

$ go test -gcflags="all=-N -l" -count=1 -run 'DataLoadValue|genDataLoadValue|GenerateDataLoadValueFile' ./pkg/ddc/{alluxio,jindo,juicefs,jindofsx,jindocache}/
ok  ... alluxio 1.921s | jindo 1.379s | juicefs 1.400s | jindofsx 1.387s | jindocache 1.414s

Note for reviewers running the full ./pkg/ddc/... suite on darwin/arm64: the AlluxioFileUtils_* / operations gomonkey tests are flaky there independently of this change (three baseline runs of the same commit without this change produced 20, 24 and 21 failures, with a different set each time). Test_genDataLoadValue was the only deterministic difference and it is fixed by the updated expectations.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 65.10%. Comparing base (44cfae9) to head (e1b5efd).
⚠️ Report is 2 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #6139      +/-   ##
==========================================
+ Coverage   65.08%   65.10%   +0.02%     
==========================================
  Files         485      485              
  Lines       33989    34015      +26     
==========================================
+ Hits        22122    22147      +25     
- Misses      10126    10127       +1     
  Partials     1741     1741              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@cheyang
cheyang requested a review from Copilot July 28, 2026 06:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

This PR fixes GenerateOwnerReferenceFromObject so it can recover an object's GroupVersionKind (GVK) from a scheme when TypeMeta is empty, preventing invalid ownerReferences that the API server rejects and that owner-based watches can’t resolve.

Changes:

  • Add GVK recovery via apiutil.GVKForObject when TypeMeta is empty.
  • Add new unit tests covering empty-TypeMeta Fluid API objects.
  • Update multiple DDC DataLoad transformer test expectations to include valid Kind and APIVersion.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
pkg/utils/transformer/owner_reference.go Adds scheme-backed GVK resolution and uses it to build valid ownerReferences.
pkg/utils/transformer/owner_reference_test.go Adds table-driven tests to ensure kind/apiVersion are recovered when TypeMeta is missing.
pkg/ddc/juicefs/data_load_test.go Updates expected ownerReference output to include recovered GVK fields.
pkg/ddc/jindofsx/load_data_test.go Updates expected ownerReference output to include recovered GVK fields.
pkg/ddc/jindocache/load_data_test.go Updates expected ownerReference output to include recovered GVK fields.
pkg/ddc/jindo/load_data_test.go Updates expected ownerReference output to include recovered GVK fields.
pkg/ddc/alluxio/load_data_test.go Updates expected ownerReference output to include recovered GVK fields.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +32 to +44
var fluidScheme = runtime.NewScheme()

func init() {
utilruntime.Must(datav1alpha1.AddToScheme(fluidScheme))
}

func GenerateOwnerReferenceFromObject(obj client.Object) *common.OwnerReference {
gvk := obj.GetObjectKind().GroupVersionKind()
if gvk.Empty() {
if resolved, err := apiutil.GVKForObject(obj, fluidScheme); err == nil {
gvk = resolved
}
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The premise here does not hold, so no change on this point — but let me show the checks rather than just assert it:

  1. Other fluid API versions: there is only one. ls api/v1alpha1, and grep -rn v1beta1 api/ returns nothing. The data.fluid.io/v1beta1 in the PR description comes from an existing test fixture that sets that string in TypeMeta by hand; that path never reaches the scheme lookup, because a populated field is used as-is. So datav1alpha1.AddToScheme covers every fluid version that exists.
  2. Non-fluid owners: all 16 call sites pass fluid custom resources (dataload, dataMigrate, dataProcess, and the various runtime types) — no built-in type is ever passed. Adding the client-go scheme would register types that no caller can reach.

Rather than widen the scheme speculatively, the failure is now observable: an unregistered type is logged (see the other thread), and a spec pins that behaviour. If a non-fluid owner ever shows up, the log will say so instead of silently producing an empty kind.

Comment on lines +29 to +31
// fluidScheme knows the fluid API types. It recovers the GroupVersionKind of an object whose TypeMeta is
// empty, which a typed client is allowed to hand back: an ownerReference without kind/apiVersion is rejected
// by the API server and can not be resolved back to its owner by an owner based watch.
Comment on lines +194 to +196
// A typed client is allowed to hand back objects without TypeMeta, so the kind and the apiVersion have
// to be recovered from the scheme. Otherwise the ownerReference is rejected by the API server and can
// not be resolved back to its owner by an owner based watch.
@cheyang
cheyang requested a review from Copilot July 28, 2026 07:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

pkg/utils/transformer/owner_reference.go:44

  • The fallback scheme only registers datav1alpha1 types. If GenerateOwnerReferenceFromObject is ever called with an empty-TypeMeta object from another Fluid API version (e.g., v1beta1) or any other type not added to fluidScheme, GVKForObject will fail and the function will still emit an invalid ownerReference (empty kind / \"/\" apiVersion). Consider registering all supported Fluid API versions into fluidScheme, or (more robustly) refactoring the helper to accept a *runtime.Scheme so callers can pass the same scheme used by the manager/client. Also, swallowing the error (err == nil only) makes this failure mode hard to diagnose—please surface it (e.g., via utilruntime.HandleError/logging) when resolution fails.
// fluidScheme knows the fluid API types. It recovers the GroupVersionKind of an object whose TypeMeta is
// empty, which a typed client is allowed to hand back: an ownerReference without kind/apiVersion is rejected
// by the API server and can not be resolved back to its owner by an owner based watch.
var fluidScheme = runtime.NewScheme()

func init() {
	utilruntime.Must(datav1alpha1.AddToScheme(fluidScheme))
}

func GenerateOwnerReferenceFromObject(obj client.Object) *common.OwnerReference {
	gvk := obj.GetObjectKind().GroupVersionKind()
	if gvk.Empty() {
		if resolved, err := apiutil.GVKForObject(obj, fluidScheme); err == nil {
			gvk = resolved
		}
	}

pkg/utils/transformer/owner_reference.go:31

  • Corrected spelling/grammar of 'can not' to 'cannot'.
// by the API server and can not be resolved back to its owner by an owner based watch.

@RongGu RongGu left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/lgtm
/approve


func GenerateOwnerReferenceFromObject(obj client.Object) *common.OwnerReference {
gvk := obj.GetObjectKind().GroupVersionKind()
if gvk.Empty() {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gvk.Empty() is true only when Group, Version and Kind are all empty, so an object whose TypeMeta is partially populated never reaches the scheme lookup and still ends up with a malformed reference. I checked this with a unit test against this branch, and the failure is asymmetric:

  • TypeMeta{Kind: "DataLoad"} -> {Kind: "DataLoad", APIVersion: ""}
  • TypeMeta{APIVersion: "data.fluid.io/v1alpha1"} -> {Kind: "", APIVersion: "data.fluid.io/v1alpha1"}

Both shapes are rejected by the API server for the same reason the fully-empty case was, and partially populated TypeMeta is reachable for objects decoded from user YAML or round-tripped through a typed client.

Worth noting that #6138 solves the same problem for the Dataset owner with a per-field fallback that does handle this case:

kind := dataset.GetObjectKind().GroupVersionKind().Kind
if len(kind) == 0 {
	kind = datav1alpha1.Datasetkind
}
apiVersion := dataset.APIVersion
if len(apiVersion) == 0 {
	apiVersion = datav1alpha1.GroupVersion.String()
}

Filling Kind and APIVersion independently here, instead of gating the whole recovery on gvk.Empty(), would close the gap and make the two code paths agree.

For what it is worth, none of the 16 current call sites hit this — they all pass concrete registered v1alpha1 structs — so this is a latent robustness gap rather than a live bug. It only matters because the helper now advertises itself as recovering the GVK.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e1b5efd — you are right, and the asymmetry you measured is exactly what the gvk.Empty() gate caused. Both fields now fall back on their own, matching the per-field shape used for the Dataset owner in #6138:

gvk := obj.GetObjectKind().GroupVersionKind()
if len(gvk.Kind) == 0 || len(gvk.Version) == 0 {
	resolved, err := apiutil.GVKForObject(obj, fluidScheme)
	if err != nil {
		log.Error(err, "failed to recover the GroupVersionKind of the owner from the scheme, the generated ownerReference stays incomplete", ...)
	} else {
		if len(gvk.Kind) == 0 {
			gvk.Kind = resolved.Kind
		}
		if len(gvk.Version) == 0 {
			gvk.Group, gvk.Version = resolved.Group, resolved.Version
		}
	}
}

Two entries added for the partial shapes (TypeMeta{Kind: "DataLoad"} and TypeMeta{APIVersion: "data.fluid.io/v1alpha1"}), both asserting kind and apiVersion. Confirmed they fail against the previous revision of this branch — 4 failed assertions — and pass now (28/28 specs).

func GenerateOwnerReferenceFromObject(obj client.Object) *common.OwnerReference {
gvk := obj.GetObjectKind().GroupVersionKind()
if gvk.Empty() {
if resolved, err := apiutil.GVKForObject(obj, fluidScheme); err == nil {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The err == nil guard means a type that is not registered in fluidScheme falls through silently and yields {Kind: "", APIVersion: ""}. I confirmed zero log records are emitted on that path.

Since the function has no error return, a single log.Error here would be the only way a caller could ever notice a misconfigured scheme. Low priority while every caller passes a registered type, but today the failure is completely invisible.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in e1b5efd: the scheme lookup failure is now logged through ctrl.Log.WithName("utils.transformer"), with the namespace, the name and the incoming GVK, and the message states that the reference stays incomplete. Matches the package-level logger pattern used elsewhere under pkg/utils (utils, utils.time, utils.security, kubeclient).

There is also a spec asserting the fall-through behaviour for a type the fluid scheme does not know (corev1.ConfigMap), so the shape of that path is pinned even though the log itself is not asserted.

…Meta is incomplete

GenerateOwnerReferenceFromObject read the kind and the apiVersion straight from the
object's TypeMeta, which a typed client is allowed to hand back empty. The resulting
reference carries an empty kind and "/" as its apiVersion, which the API server
rejects, and which an owner based watch cannot resolve back to its owner. The unit
tests of the DataLoad value transformers encoded exactly that broken output as their
expectation.

Recover the missing fields from the GroupVersionKind known by the fluid scheme. The
kind and the apiVersion fall back on their own rather than only when the whole
TypeMeta is empty, because a partially populated TypeMeta (only the kind, or only the
apiVersion, as objects decoded from user YAML can carry) produces a reference which is
just as malformed, and because that matches the per-field fallback used for the Dataset
owner in fluid-cloudnative#6138. A type which is missing from the scheme is logged, since the function
has no error return and the incomplete reference would be invisible otherwise.

Objects which do carry a complete TypeMeta are unaffected, so this only changes
references which the API server would have rejected anyway. The expectations of the
affected tests are fixed accordingly.

Two of the chains fed by this helper end up under an owner based watch: DataLoad and
DataMigrate render a batchv1.Job whose ownerReference comes from here, and both
controllers register Owns(&batchv1.Job{}).

Reported by @cheyang while reviewing fluid-cloudnative#6138.

Signed-off-by: cheyang <cheyang@163.com>
@cheyang
cheyang force-pushed the chore/harden-owner-reference-gvk branch from b24718f to e1b5efd Compare July 29, 2026 02:40
@sonarqubecloud

Copy link
Copy Markdown

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

@RongGu RongGu left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/lgtm
/approve

@RongGu RongGu left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/lgtm
/approve

@RongGu
RongGu merged commit 95138fc into fluid-cloudnative:master Jul 29, 2026
23 of 24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants