-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
🌱 inmemory: fix watch to continue serving based on resourceVersion parameter #11695
base: main
Are you sure you want to change the base?
Changes from all commits
e08e606
3bac618
cce5bde
b4ed0fc
42309df
3054216
caf66a6
e2d871c
36fa992
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -50,6 +50,7 @@ import ( | |||||
"sigs.k8s.io/controller-runtime/pkg/client" | ||||||
|
||||||
inmemoryruntime "sigs.k8s.io/cluster-api/test/infrastructure/inmemory/pkg/runtime" | ||||||
inmemoryclient "sigs.k8s.io/cluster-api/test/infrastructure/inmemory/pkg/runtime/client" | ||||||
inmemoryportforward "sigs.k8s.io/cluster-api/test/infrastructure/inmemory/pkg/server/api/portforward" | ||||||
) | ||||||
|
||||||
|
@@ -315,6 +316,25 @@ func (h *apiServerHandler) apiV1List(req *restful.Request, resp *restful.Respons | |||||
return | ||||||
} | ||||||
|
||||||
h.log.V(3).Info(fmt.Sprintf("Serving List for %v", req.Request.URL), "resourceGroup", resourceGroup) | ||||||
|
||||||
list, err := h.apiV1list(ctx, req, *gvk, inmemoryClient) | ||||||
if err != nil { | ||||||
if status, ok := err.(apierrors.APIStatus); ok || errors.As(err, &status) { | ||||||
_ = resp.WriteHeaderAndEntity(int(status.Status().Code), status) | ||||||
return | ||||||
} | ||||||
_ = resp.WriteErrorString(http.StatusInternalServerError, err.Error()) | ||||||
return | ||||||
} | ||||||
|
||||||
if err := resp.WriteEntity(list); err != nil { | ||||||
_ = resp.WriteErrorString(http.StatusInternalServerError, err.Error()) | ||||||
return | ||||||
} | ||||||
} | ||||||
|
||||||
func (h *apiServerHandler) apiV1list(ctx context.Context, req *restful.Request, gvk schema.GroupVersionKind, inmemoryClient inmemoryclient.Client) (*unstructured.UnstructuredList, error) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. might be
Suggested change
(easier to distinguish from apiV1List with upper L) |
||||||
// Reads and returns the requested data. | ||||||
list := &unstructured.UnstructuredList{} | ||||||
list.SetAPIVersion(gvk.GroupVersion().String()) | ||||||
|
@@ -328,33 +348,23 @@ func (h *apiServerHandler) apiV1List(req *restful.Request, resp *restful.Respons | |||||
// TODO: The only field Selector which works is for `spec.nodeName` on pods. | ||||||
fieldSelector, err := fields.ParseSelector(req.QueryParameter("fieldSelector")) | ||||||
if err != nil { | ||||||
_ = resp.WriteErrorString(http.StatusInternalServerError, err.Error()) | ||||||
return | ||||||
return nil, err | ||||||
} | ||||||
if fieldSelector != nil { | ||||||
listOpts = append(listOpts, client.MatchingFieldsSelector{Selector: fieldSelector}) | ||||||
} | ||||||
|
||||||
labelSelector, err := labels.Parse(req.QueryParameter("labelSelector")) | ||||||
if err != nil { | ||||||
_ = resp.WriteErrorString(http.StatusInternalServerError, err.Error()) | ||||||
return | ||||||
return nil, err | ||||||
} | ||||||
if labelSelector != nil { | ||||||
listOpts = append(listOpts, client.MatchingLabelsSelector{Selector: labelSelector}) | ||||||
} | ||||||
if err := inmemoryClient.List(ctx, list, listOpts...); err != nil { | ||||||
if status, ok := err.(apierrors.APIStatus); ok || errors.As(err, &status) { | ||||||
_ = resp.WriteHeaderAndEntity(int(status.Status().Code), status) | ||||||
return | ||||||
} | ||||||
_ = resp.WriteHeaderAndEntity(http.StatusInternalServerError, err.Error()) | ||||||
return | ||||||
} | ||||||
if err := resp.WriteEntity(list); err != nil { | ||||||
_ = resp.WriteErrorString(http.StatusInternalServerError, err.Error()) | ||||||
return | ||||||
return nil, err | ||||||
} | ||||||
return list, nil | ||||||
} | ||||||
|
||||||
func (h *apiServerHandler) apiV1Watch(req *restful.Request, resp *restful.Response) { | ||||||
|
@@ -372,6 +382,8 @@ func (h *apiServerHandler) apiV1Watch(req *restful.Request, resp *restful.Respon | |||||
return | ||||||
} | ||||||
|
||||||
h.log.V(3).Info(fmt.Sprintf("Serving Watch for %v", req.Request.URL), "resourceGroup", resourceGroup) | ||||||
|
||||||
// If the request is a Watch handle it using watchForResource. | ||||||
err = h.watchForResource(req, resp, resourceGroup, *gvk) | ||||||
if err != nil { | ||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -20,20 +20,21 @@ import ( | |||||
"context" | ||||||
"fmt" | ||||||
"net/http" | ||||||
"strconv" | ||||||
"time" | ||||||
|
||||||
"github.com/emicklei/go-restful/v3" | ||||||
"github.com/pkg/errors" | ||||||
"k8s.io/apimachinery/pkg/runtime" | ||||||
"k8s.io/apimachinery/pkg/runtime/schema" | ||||||
"k8s.io/apimachinery/pkg/watch" | ||||||
ctrl "sigs.k8s.io/controller-runtime" | ||||||
"sigs.k8s.io/controller-runtime/pkg/client" | ||||||
) | ||||||
|
||||||
// Event records a lifecycle event for a Kubernetes object. | ||||||
type Event struct { | ||||||
Type watch.EventType `json:"type,omitempty"` | ||||||
Object runtime.Object `json:"object,omitempty"` | ||||||
Object client.Object `json:"object,omitempty"` | ||||||
} | ||||||
|
||||||
// WatchEventDispatcher dispatches events for a single resourceGroup. | ||||||
|
@@ -88,13 +89,15 @@ func (m *WatchEventDispatcher) OnGeneric(resourceGroup string, o client.Object) | |||||
|
||||||
func (h *apiServerHandler) watchForResource(req *restful.Request, resp *restful.Response, resourceGroup string, gvk schema.GroupVersionKind) (reterr error) { | ||||||
ctx := req.Request.Context() | ||||||
log := h.log.WithValues("resourceGroup", resourceGroup, "gvk", gvk.String()) | ||||||
ctx = ctrl.LoggerInto(ctx, log) | ||||||
queryTimeout := req.QueryParameter("timeoutSeconds") | ||||||
resourceVersion := req.QueryParameter("resourceVersion") | ||||||
c := h.manager.GetCache() | ||||||
i, err := c.GetInformerForKind(ctx, gvk) | ||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
h.log.Info(fmt.Sprintf("Serving Watch for %v", req.Request.URL)) | ||||||
// With an unbuffered event channel RemoveEventHandler could be blocked because it requires a lock on the informer. | ||||||
// When Run stops reading from the channel the informer could be blocked with an unbuffered chanel and then RemoveEventHandler never goes through. | ||||||
// 1000 is used to avoid deadlocks in clusters with a higher number of Machines/Nodes. | ||||||
|
@@ -115,7 +118,12 @@ func (h *apiServerHandler) watchForResource(req *restful.Request, resp *restful. | |||||
L: | ||||||
for { | ||||||
select { | ||||||
case <-events: | ||||||
case event, ok := <-events: | ||||||
if !ok { | ||||||
// End of results. | ||||||
break L | ||||||
} | ||||||
log.V(4).Info("Missed event", "eventType", event.Type, "objectName", event.Object.GetName(), "resourceVersion", event.Object.GetResourceVersion()) | ||||||
default: | ||||||
break L | ||||||
} | ||||||
|
@@ -124,11 +132,49 @@ func (h *apiServerHandler) watchForResource(req *restful.Request, resp *restful. | |||||
// Note: After we removed the handler, no new events will be written to the events channel. | ||||||
}() | ||||||
|
||||||
return watcher.Run(ctx, queryTimeout, resp) | ||||||
// Get at client to the resource group and list all relevant objects. | ||||||
inmemoryClient := h.manager.GetResourceGroup(resourceGroup).GetClient() | ||||||
list, err := h.apiV1list(ctx, req, gvk, inmemoryClient) | ||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
|
||||||
// If resourceVersion was set parse to uint64 which is the representation in the simulated apiserver. | ||||||
var parsedResourceVersion uint64 | ||||||
if resourceVersion != "" { | ||||||
parsedResourceVersion, err = strconv.ParseUint(resourceVersion, 10, 64) | ||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
} | ||||||
|
||||||
initialEvents := []Event{} | ||||||
|
||||||
// Loop over all items and fill the list of events with objects which have a newer resourceVersion. | ||||||
for _, obj := range list.Items { | ||||||
if resourceVersion != "" { | ||||||
objResourceVersion, err := strconv.ParseUint(obj.GetResourceVersion(), 10, 64) | ||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
if objResourceVersion <= parsedResourceVersion { | ||||||
chrischdi marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
continue | ||||||
} | ||||||
} | ||||||
eventType := watch.Modified | ||||||
// kube-apiserver emits all events as ADDED when no resourceVersion is given. | ||||||
if obj.GetGeneration() == 1 || resourceVersion == "" { | ||||||
eventType = watch.Added | ||||||
} | ||||||
initialEvents = append(initialEvents, Event{Type: eventType, Object: &obj}) | ||||||
} | ||||||
|
||||||
return watcher.Run(ctx, queryTimeout, initialEvents, resp) | ||||||
} | ||||||
|
||||||
// Run serves a series of encoded events via HTTP with Transfer-Encoding: chunked. | ||||||
func (m *WatchEventDispatcher) Run(ctx context.Context, timeout string, w http.ResponseWriter) error { | ||||||
func (m *WatchEventDispatcher) Run(ctx context.Context, timeout string, initialEvents []Event, w http.ResponseWriter) error { | ||||||
log := ctrl.LoggerFrom(ctx) | ||||||
flusher, ok := w.(http.Flusher) | ||||||
if !ok { | ||||||
return errors.New("can't start Watch: can't get http.Flusher") | ||||||
|
@@ -139,6 +185,16 @@ func (m *WatchEventDispatcher) Run(ctx context.Context, timeout string, w http.R | |||||
} | ||||||
w.Header().Set("Transfer-Encoding", "chunked") | ||||||
w.WriteHeader(http.StatusOK) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it okay that we don't flush after this like before? |
||||||
|
||||||
// Write all initial events. | ||||||
for _, event := range initialEvents { | ||||||
if err := resp.WriteEntity(event); err != nil { | ||||||
log.Error(err, "Writing old event", "eventType", event.Type, "objectName", event.Object.GetName(), "resourceVersion", event.Object.GetResourceVersion()) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what about using "initial event" instead of "old event"? |
||||||
_ = resp.WriteErrorString(http.StatusInternalServerError, err.Error()) | ||||||
} else { | ||||||
log.V(4).Info("Wrote old event", "eventType", event.Type, "objectName", event.Object.GetName(), "resourceVersion", event.Object.GetResourceVersion()) | ||||||
} | ||||||
} | ||||||
flusher.Flush() | ||||||
|
||||||
timeoutTimer, seconds, err := setTimer(timeout) | ||||||
|
@@ -149,6 +205,18 @@ func (m *WatchEventDispatcher) Run(ctx context.Context, timeout string, w http.R | |||||
ctx, cancel := context.WithTimeout(ctx, seconds) | ||||||
defer cancel() | ||||||
defer timeoutTimer.Stop() | ||||||
|
||||||
// Determine the highest written resourceVersion so we can filter out duplicated events from the channel. | ||||||
minResourceVersion := uint64(0) | ||||||
if len(initialEvents) > 0 { | ||||||
minResourceVersion, err = strconv.ParseUint(initialEvents[len(initialEvents)-1].Object.GetResourceVersion(), 10, 64) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of assuming an ordering of events by resource version (which I'm not sure we are enforcing somewhere), what about computing minResourceVersion when we go through initialEvents in the for loop above |
||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
minResourceVersion++ | ||||||
} | ||||||
|
||||||
var objResourceVersion uint64 | ||||||
for { | ||||||
select { | ||||||
case <-ctx.Done(): | ||||||
|
@@ -160,8 +228,25 @@ func (m *WatchEventDispatcher) Run(ctx context.Context, timeout string, w http.R | |||||
// End of results. | ||||||
return nil | ||||||
} | ||||||
|
||||||
// Parse and check if the object has a higher resource version than we allow. | ||||||
objResourceVersion, err = strconv.ParseUint(event.Object.GetResourceVersion(), 10, 64) | ||||||
if err != nil { | ||||||
log.Error(err, "Parsing object resource version", "eventType", event.Type, "objectName", event.Object.GetName(), "resourceVersion", event.Object.GetResourceVersion()) | ||||||
_ = resp.WriteErrorString(http.StatusInternalServerError, err.Error()) | ||||||
continue | ||||||
} | ||||||
|
||||||
// Skip objects which were already written. | ||||||
if objResourceVersion < minResourceVersion { | ||||||
continue | ||||||
} | ||||||
|
||||||
if err := resp.WriteEntity(event); err != nil { | ||||||
log.Error(err, "Writing event", "eventType", event.Type, "objectName", event.Object.GetName(), "resourceVersion", event.Object.GetResourceVersion()) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
_ = resp.WriteErrorString(http.StatusInternalServerError, err.Error()) | ||||||
} else { | ||||||
log.V(4).Info("Wrote event", "eventType", event.Type, "objectName", event.Object.GetName(), "resourceVersion", event.Object.GetResourceVersion()) | ||||||
} | ||||||
if len(m.events) == 0 { | ||||||
flusher.Flush() | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Q: is it correct that list ResourceVersion in the system and not the last resource version in the list of items?
Q: is is correct set set this value non matter of it is a "plain" list or a lis watch?
(from a quick check with kubectl yes to both, but I like a confirmation)