Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

apiserver: clarify watcher fanout deepcopy and self-link code #105700

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 6 additions & 4 deletions staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,11 +228,13 @@ func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interfac
}
trace.Step("Object stored in database")

if err := setObjectSelfLink(ctx, result, req, scope.Namer); err != nil {
scope.err(err, w, req)
return
if !utilfeature.DefaultFeatureGate.Enabled(features.RemoveSelfLink) {
Copy link
Member

Choose a reason for hiding this comment

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

Actually - I'm fairly sure you can just remove "setObjectSelfLink" from here completely.

before (currently in line 241) you are calling transformResponseObject, which underneaths is calling transformObject which is setting selflink.

So I think it's not needed at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

only exception is metav1.Status which skips transformObject for whatever reason. Do we care?

Copy link
Member

Choose a reason for hiding this comment

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

Hmm - I can't see how this can matter if we call it only in patch and not in created/update/get/...
If it matters - I think we should set it in other operations too, right?

So I think that we should set it elsewhere too or get rid from here for consistency...

Copy link
Member

Choose a reason for hiding this comment

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

I'd rather not spend a lot of time trying to fix existing inconsistencies in self-link-adding, since that is disabled by default and is planned to be removed. Just honoring the feature gate here is easier to reason about.

Copy link
Member

Choose a reason for hiding this comment

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

OK - I would like to propose it for 1.24 (we moved to Beta in 1.16, so I think we're either ready to do that now or will never be). I'm going to start a conversation for it on slack.

But with that - I agree this is probably a moot discussion.

if err := setObjectSelfLink(ctx, result, req, scope.Namer); err != nil {
scope.err(err, w, req)
return
}
trace.Step("Self-link added")
}
trace.Step("Self-link added")

status := http.StatusOK
if wasCreated {
Expand Down
16 changes: 14 additions & 2 deletions staging/src/k8s.io/apiserver/pkg/endpoints/handlers/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
"k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
utiltrace "k8s.io/utils/trace"
)

Expand Down Expand Up @@ -59,8 +61,18 @@ func doTransformObject(ctx context.Context, obj runtime.Object, opts interface{}
if _, ok := obj.(*metav1.Status); ok {
return obj, nil
}
if err := setObjectSelfLink(ctx, obj, req, scope.Namer); err != nil {
return nil, err

if !utilfeature.DefaultFeatureGate.Enabled(features.RemoveSelfLink) {
if err := setObjectSelfLink(ctx, obj, req, scope.Namer); err != nil {
return nil, err
}
}

// Ensure that for empty lists we don't return <nil> items.
if meta.IsListType(obj) && meta.LenList(obj) == 0 {
if err := meta.SetList(obj, []runtime.Object{}); err != nil {
return nil, err
}
}
Comment on lines +71 to 76
Copy link
Member

Choose a reason for hiding this comment

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

off the top of my head, I can't think of a reason it would matter, but since SetList was conditionally done at the top of setObjectSelfLink, should we do that before calling setObjectSelfLink to be perfectly equivalent?


switch target := mediaType.Convert; {
Expand Down
20 changes: 0 additions & 20 deletions staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,7 @@ import (
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
"k8s.io/apiserver/pkg/endpoints/metrics"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/registry/rest"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/apiserver/pkg/warning"
"k8s.io/klog/v2"
)
Expand Down Expand Up @@ -369,16 +367,6 @@ func dedupOwnerReferencesAndAddWarning(obj runtime.Object, requestContext contex
// TODO: remove the need for the namer LinkSetters by requiring objects implement either Object or List
// interfaces
func setObjectSelfLink(ctx context.Context, obj runtime.Object, req *http.Request, namer ScopeNamer) error {
if utilfeature.DefaultFeatureGate.Enabled(features.RemoveSelfLink) {
// Ensure that for empty lists we don't return <nil> items.
if meta.IsListType(obj) && meta.LenList(obj) == 0 {
if err := meta.SetList(obj, []runtime.Object{}); err != nil {
return err
}
}
return nil
}

// We only generate list links on objects that implement ListInterface - historically we duck typed this
// check via reflection, but as we move away from reflection we require that you not only carry Items but
// ListMeta into order to be identified as a list.
Expand All @@ -402,18 +390,10 @@ func setObjectSelfLink(ctx context.Context, obj runtime.Object, req *http.Reques
return fmt.Errorf("missing requestInfo")
}

count := 0
err = meta.EachListItem(obj, func(obj runtime.Object) error {
count++
return setSelfLink(obj, requestInfo, namer)
})

if count == 0 {
if err := meta.SetList(obj, []runtime.Object{}); err != nil {
return err
}
}

return err
}

Expand Down
17 changes: 13 additions & 4 deletions staging/src/k8s.io/apiserver/pkg/storage/cacher/cacher.go
Original file line number Diff line number Diff line change
Expand Up @@ -1308,7 +1308,16 @@ func (c *cacheWatcher) nextBookmarkTime(now time.Time, bookmarkFrequency time.Du
return heartbeatTime, true
}

func getEventObject(object runtime.Object) runtime.Object {
// getSafeFanoutObject returns an object that can be sent into fan-out
// and its metadata can be safely mutated in every watcher in parallel.
// We assume that every watcher will mutate the object in the same way.
//
// Two cases:
// 1. the CacheableObject object has thread-safe metadata changes. Equal mutations
// don't harm performance. Different mutations would harm performance, but we
// know they don't happen.
// 2. a non-CacheableObject is deep-copied and hence every mutation is safe.
func getSafeFanoutObject(object runtime.Object) runtime.Object {
if _, ok := object.(runtime.CacheableObject); ok {
// It is safe to return without deep-copy, because the underlying
// object was already deep-copied during construction.
Expand Down Expand Up @@ -1344,12 +1353,12 @@ func (c *cacheWatcher) convertToWatchEvent(event *watchCacheEvent) *watch.Event

switch {
case curObjPasses && !oldObjPasses:
return &watch.Event{Type: watch.Added, Object: getEventObject(event.Object)}
return &watch.Event{Type: watch.Added, Object: getSafeFanoutObject(event.Object)}
case curObjPasses && oldObjPasses:
return &watch.Event{Type: watch.Modified, Object: getEventObject(event.Object)}
return &watch.Event{Type: watch.Modified, Object: getSafeFanoutObject(event.Object)}
case !curObjPasses && oldObjPasses:
// return a delete event with the previous object content, but with the event's resource version
oldObj := getEventObject(event.PrevObject)
oldObj := getSafeFanoutObject(event.PrevObject)
updateResourceVersionIfNeeded(oldObj, c.versioner, event.ResourceVersion)
return &watch.Event{Type: watch.Deleted, Object: oldObj}
}
Expand Down