Skip to content

Commit db2218d

Browse files
committed
Stop the RetryWatcher when failing due to permissions issue
When the client does not have permission to watch a resource, the RetryWatcher continuously retried. In this case, it's better to send an error and stop retrying to let the caller handle this case since this is not a transient error that can be recovered without user intervention. This is particularly helpful in applications that leverage a user provided service account and the application needs to notify the user to set the correct permissions for the service account. This also accounts for invalid credentials from the watch client. Signed-off-by: mprahl <[email protected]>
1 parent 5b49afa commit db2218d

File tree

2 files changed

+65
-0
lines changed

2 files changed

+65
-0
lines changed

staging/src/k8s.io/client-go/tools/watch/retrywatcher.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,35 @@ func (rw *RetryWatcher) doReceive() (bool, time.Duration) {
126126
return false, 0
127127
}
128128

129+
// Check if the watch failed due to the client not having permission to watch the resource or the credentials
130+
// being invalid (e.g. expired token).
131+
if apierrors.IsForbidden(err) || apierrors.IsUnauthorized(err) {
132+
// Add more detail since the forbidden message returned by the Kubernetes API is just "unknown".
133+
klog.ErrorS(err, msg+": ensure the client has valid credentials and watch permissions on the resource")
134+
135+
if apiStatus, ok := err.(apierrors.APIStatus); ok {
136+
statusErr := apiStatus.Status()
137+
138+
sent := rw.send(watch.Event{
139+
Type: watch.Error,
140+
Object: &statusErr,
141+
})
142+
if !sent {
143+
// This likely means the RetryWatcher is stopping but return false so the caller to doReceive can
144+
// verify this and potentially retry.
145+
klog.Error("Failed to send the Unauthorized or Forbidden watch event")
146+
147+
return false, 0
148+
}
149+
} else {
150+
// This should never happen since apierrors only handles apierrors.APIStatus. Still, this is an
151+
// unrecoverable error, so still allow it to return true below.
152+
klog.ErrorS(err, msg+": encountered an unexpected Unauthorized or Forbidden error type")
153+
}
154+
155+
return true, 0
156+
}
157+
129158
klog.ErrorS(err, msg)
130159
// Retry
131160
return false, 0

staging/src/k8s.io/client-go/tools/watch/retrywatcher_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,42 @@ func TestRetryWatcher(t *testing.T) {
288288
},
289289
},
290290
},
291+
{
292+
name: "fails on Forbidden",
293+
initialRV: "5",
294+
watchClient: &cache.ListWatch{
295+
WatchFunc: func() func(options metav1.ListOptions) (watch.Interface, error) {
296+
return func(options metav1.ListOptions) (watch.Interface, error) {
297+
return nil, apierrors.NewForbidden(schema.GroupResource{}, "", errors.New("unknown"))
298+
}
299+
}(),
300+
},
301+
watchCount: 1,
302+
expected: []watch.Event{
303+
{
304+
Type: watch.Error,
305+
Object: &apierrors.NewForbidden(schema.GroupResource{}, "", errors.New("unknown")).ErrStatus,
306+
},
307+
},
308+
},
309+
{
310+
name: "fails on Unauthorized",
311+
initialRV: "5",
312+
watchClient: &cache.ListWatch{
313+
WatchFunc: func() func(options metav1.ListOptions) (watch.Interface, error) {
314+
return func(options metav1.ListOptions) (watch.Interface, error) {
315+
return nil, apierrors.NewUnauthorized("")
316+
}
317+
}(),
318+
},
319+
watchCount: 1,
320+
expected: []watch.Event{
321+
{
322+
Type: watch.Error,
323+
Object: &apierrors.NewUnauthorized("").ErrStatus,
324+
},
325+
},
326+
},
291327
{
292328
name: "recovers from timeout error",
293329
initialRV: "5",

0 commit comments

Comments
 (0)