@@ -5,7 +5,7 @@ package watcher
5
5
6
6
import (
7
7
"context"
8
- "net/http "
8
+ "fmt "
9
9
"testing"
10
10
"time"
11
11
@@ -54,13 +54,7 @@ func TestResourceNotFoundError(t *testing.T) {
54
54
// dynamicClient converts Status objects from the apiserver into errors.
55
55
// So we can just return the right error here to simulate an error from
56
56
// the apiserver.
57
- name := "" // unused by LIST requests
58
- // The apisevrer confusingly does not return apierrors.NewNotFound,
59
- // which has a nice constant for its error message.
60
- // err = apierrors.NewNotFound(exampleGR, name)
61
- // Instead it uses apierrors.NewGenericServerResponse, which uses
62
- // a hard-coded error message.
63
- err = apierrors .NewGenericServerResponse (http .StatusNotFound , "list" , exampleGR , name , "unused" , - 1 , false )
57
+ err = newGenericServerResponse (action , newNotFoundResourceStatusError (action ))
64
58
return true , nil , err
65
59
})
66
60
},
@@ -88,13 +82,7 @@ func TestResourceNotFoundError(t *testing.T) {
88
82
// dynamicClient converts Status objects from the apiserver into errors.
89
83
// So we can just return the right error here to simulate an error from
90
84
// the apiserver.
91
- name := "" // unused by LIST requests
92
- // The apisevrer confusingly does not return apierrors.NewNotFound,
93
- // which has a nice constant for its error message.
94
- // err = apierrors.NewNotFound(exampleGR, name)
95
- // Instead it uses apierrors.NewGenericServerResponse, which uses
96
- // a hard-coded error message.
97
- err = apierrors .NewGenericServerResponse (http .StatusNotFound , "list" , exampleGR , name , "unused" , - 1 , false )
85
+ err = newGenericServerResponse (action , newNotFoundResourceStatusError (action ))
98
86
return true , nil , err
99
87
})
100
88
},
@@ -110,7 +98,67 @@ func TestResourceNotFoundError(t *testing.T) {
110
98
t .Errorf ("Expected typed NotFound error, but got untyped NotFound error: %v" , err )
111
99
default :
112
100
// If we got this error, the test is probably broken.
113
- t .Errorf ("Expected untyped NotFound error, but got a different error: %v" , err )
101
+ t .Errorf ("Expected typed NotFound error, but got a different error: %v" , err )
102
+ }
103
+ },
104
+ },
105
+ {
106
+ name : "List resource forbidden error" ,
107
+ setup : func (fakeClient * dynamicfake.FakeDynamicClient ) {
108
+ fakeClient .PrependReactor ("list" , exampleGR .Resource , func (action clienttesting.Action ) (handled bool , ret runtime.Object , err error ) {
109
+ listAction := action .(clienttesting.ListAction )
110
+ if listAction .GetNamespace () != namespace {
111
+ assert .Fail (t , "Received unexpected LIST namespace: %s" , listAction .GetNamespace ())
112
+ return false , nil , nil
113
+ }
114
+ // dynamicClient converts Status objects from the apiserver into errors.
115
+ // So we can just return the right error here to simulate an error from
116
+ // the apiserver.
117
+ err = newGenericServerResponse (action , newForbiddenResourceStatusError (action ))
118
+ return true , nil , err
119
+ })
120
+ },
121
+ errorHandler : func (t * testing.T , err error ) {
122
+ switch {
123
+ case apierrors .IsForbidden (err ):
124
+ // If we got this error, something changed in the apiserver or
125
+ // client. If the client changed, it might be safe to stop parsing
126
+ // the error string.
127
+ t .Errorf ("Expected untyped Forbidden error, but got typed Forbidden error: %v" , err )
128
+ case containsForbiddenMessage (err ):
129
+ // This is the expected hack, because the Informer/Reflector
130
+ // doesn't wrap the error with "%w".
131
+ t .Logf ("Received expected untyped Forbidden error: %v" , err )
132
+ default :
133
+ // If we got this error, the test is probably broken.
134
+ t .Errorf ("Expected untyped Forbidden error, but got a different error: %v" , err )
135
+ }
136
+ },
137
+ },
138
+ {
139
+ name : "Watch resource forbidden error" ,
140
+ setup : func (fakeClient * dynamicfake.FakeDynamicClient ) {
141
+ fakeClient .PrependWatchReactor (exampleGR .Resource , func (action clienttesting.Action ) (handled bool , ret watch.Interface , err error ) {
142
+ // dynamicClient converts Status objects from the apiserver into errors.
143
+ // So we can just return the right error here to simulate an error from
144
+ // the apiserver.
145
+ err = newGenericServerResponse (action , newForbiddenResourceStatusError (action ))
146
+ return true , nil , err
147
+ })
148
+ },
149
+ errorHandler : func (t * testing.T , err error ) {
150
+ switch {
151
+ case apierrors .IsForbidden (err ):
152
+ // This is the expected behavior, because the
153
+ // Informer/Reflector DOES wrap watch errors
154
+ t .Logf ("Received expected untyped Forbidden error: %v" , err )
155
+ case containsForbiddenMessage (err ):
156
+ // If this happens, there was a regression.
157
+ // Watch errors are expected to be wrapped with "%w"
158
+ t .Errorf ("Expected typed Forbidden error, but got untyped Forbidden error: %v" , err )
159
+ default :
160
+ // If we got this error, the test is probably broken.
161
+ t .Errorf ("Expected typed Forbidden error, but got a different error: %v" , err )
114
162
}
115
163
},
116
164
},
@@ -164,3 +212,43 @@ func TestResourceNotFoundError(t *testing.T) {
164
212
})
165
213
}
166
214
}
215
+
216
+ // newForbiddenResourceStatusError emulates a Forbidden error from the apiserver
217
+ // for a namespace-scoped resource.
218
+ // https://github.com/kubernetes/apiserver/blob/master/pkg/endpoints/handlers/responsewriters/errors.go#L36
219
+ func newForbiddenResourceStatusError (action clienttesting.Action ) * apierrors.StatusError {
220
+ username := "unused"
221
+ verb := action .GetVerb ()
222
+ resource := action .GetResource ().Resource
223
+ if subresource := action .GetSubresource (); len (subresource ) > 0 {
224
+ resource = resource + "/" + subresource
225
+ }
226
+ apiGroup := action .GetResource ().Group
227
+ namespace := action .GetNamespace ()
228
+
229
+ // https://github.com/kubernetes/apiserver/blob/master/pkg/endpoints/handlers/responsewriters/errors.go#L51
230
+ err := fmt .Errorf ("User %q cannot %s resource %q in API group %q in the namespace %q" ,
231
+ username , verb , resource , apiGroup , namespace )
232
+
233
+ qualifiedResource := action .GetResource ().GroupResource ()
234
+ name := "" // unused by ListAndWatch
235
+ return apierrors .NewForbidden (qualifiedResource , name , err )
236
+ }
237
+
238
+ // newNotFoundResourceStatusError emulates a NotFOund error from the apiserver
239
+ // for a resource (not an object).
240
+ func newNotFoundResourceStatusError (action clienttesting.Action ) * apierrors.StatusError {
241
+ qualifiedResource := action .GetResource ().GroupResource ()
242
+ name := "" // unused by ListAndWatch
243
+ return apierrors .NewNotFound (qualifiedResource , name )
244
+ }
245
+
246
+ // newGenericServerResponse emulates a StatusError from the apiserver.
247
+ func newGenericServerResponse (action clienttesting.Action , statusError * apierrors.StatusError ) * apierrors.StatusError {
248
+ errorCode := int (statusError .ErrStatus .Code )
249
+ verb := action .GetVerb ()
250
+ qualifiedResource := action .GetResource ().GroupResource ()
251
+ name := statusError .ErrStatus .Details .Name
252
+ // https://github.com/kubernetes/apimachinery/blob/v0.24.0/pkg/api/errors/errors.go#L435
253
+ return apierrors .NewGenericServerResponse (errorCode , verb , qualifiedResource , name , statusError .Error (), - 1 , false )
254
+ }
0 commit comments