@@ -2,16 +2,20 @@ package status
2
2
3
3
import (
4
4
"context"
5
+ "errors"
5
6
"fmt"
6
7
"sync"
8
+ "time"
7
9
8
10
"github.com/go-logr/logr"
9
11
apierrors "k8s.io/apimachinery/pkg/api/errors"
10
12
"k8s.io/apimachinery/pkg/types"
13
+ "k8s.io/apimachinery/pkg/util/wait"
11
14
"sigs.k8s.io/controller-runtime/pkg/client"
12
15
"sigs.k8s.io/gateway-api/apis/v1beta1"
13
16
14
17
ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1"
18
+ "github.com/nginxinc/nginx-gateway-fabric/internal/framework/controller"
15
19
)
16
20
17
21
//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 . Updater
@@ -64,15 +68,11 @@ type UpdaterConfig struct {
64
68
// (b) k8s API can become slow or even timeout. This will increase every update status API call.
65
69
// Making UpdaterImpl asynchronous will prevent it from adding variable delays to the event loop.
66
70
//
67
- // (3) It doesn't retry on failures. This means there is a chance that some resources will not have up-to-do statuses.
68
- // Statuses are important part of the Gateway API, so we need to ensure that the Gateway always keep the resources
69
- // statuses up-to-date.
70
- //
71
- // (4) It doesn't clear the statuses of a resources that are no longer handled by the Gateway. For example, if
71
+ // (3) It doesn't clear the statuses of a resources that are no longer handled by the Gateway. For example, if
72
72
// an HTTPRoute resource no longer has the parentRef to the Gateway resources, the Gateway must update the status
73
73
// of the resource to remove the status about the removed parentRef.
74
74
//
75
- // (5 ) If another controllers changes the status of the Gateway/HTTPRoute resource so that the information set by our
75
+ // (4 ) If another controllers changes the status of the Gateway/HTTPRoute resource so that the information set by our
76
76
// Gateway is removed, our Gateway will not restore the status until the EventLoop invokes the StatusUpdater as a
77
77
// result of processing some other new change to a resource(s).
78
78
// FIXME(pleshakov): Make updater production ready
@@ -179,6 +179,11 @@ func (upd *UpdaterImpl) updateGatewayAPI(ctx context.Context, statuses GatewayAP
179
179
180
180
if upd .cfg .UpdateGatewayClassStatus {
181
181
for nsname , gcs := range statuses .GatewayClassStatuses {
182
+ select {
183
+ case <- ctx .Done ():
184
+ return
185
+ default :
186
+ }
182
187
upd .writeStatuses (ctx , nsname , & v1beta1.GatewayClass {}, func (object client.Object ) {
183
188
gc := object .(* v1beta1.GatewayClass )
184
189
gc .Status = prepareGatewayClassStatus (gcs , upd .cfg .Clock .Now ())
@@ -188,6 +193,11 @@ func (upd *UpdaterImpl) updateGatewayAPI(ctx context.Context, statuses GatewayAP
188
193
}
189
194
190
195
for nsname , gs := range statuses .GatewayStatuses {
196
+ select {
197
+ case <- ctx .Done ():
198
+ return
199
+ default :
200
+ }
191
201
upd .writeStatuses (ctx , nsname , & v1beta1.Gateway {}, func (object client.Object ) {
192
202
gw := object .(* v1beta1.Gateway )
193
203
gw .Status = prepareGatewayStatus (gs , upd .cfg .PodIP , upd .cfg .Clock .Now ())
@@ -200,7 +210,6 @@ func (upd *UpdaterImpl) updateGatewayAPI(ctx context.Context, statuses GatewayAP
200
210
return
201
211
default :
202
212
}
203
-
204
213
upd .writeStatuses (ctx , nsname , & v1beta1.HTTPRoute {}, func (object client.Object ) {
205
214
hr := object .(* v1beta1.HTTPRoute )
206
215
// statuses.GatewayStatus is never nil when len(statuses.HTTPRouteStatuses) > 0
@@ -219,26 +228,19 @@ func (upd *UpdaterImpl) writeStatuses(
219
228
obj client.Object ,
220
229
statusSetter func (client.Object ),
221
230
) {
222
- // The function handles errors by reporting them in the logs.
223
- // We need to get the latest version of the resource.
224
- // Otherwise, the Update status API call can fail.
225
- // Note: the default client uses a cache for reads, so we're not making an unnecessary API call here.
226
- // the default is configurable in the Manager options.
227
- if err := upd .cfg .Client .Get (ctx , nsname , obj ); err != nil {
228
- if ! apierrors .IsNotFound (err ) {
229
- upd .cfg .Logger .Error (
230
- err ,
231
- "Failed to get the recent version the resource when updating status" ,
232
- "namespace" , nsname .Namespace ,
233
- "name" , nsname .Name ,
234
- "kind" , obj .GetObjectKind ().GroupVersionKind ().Kind )
235
- }
236
- return
237
- }
238
-
239
- statusSetter (obj )
240
-
241
- if err := upd .cfg .Client .Status ().Update (ctx , obj ); err != nil {
231
+ err := wait .ExponentialBackoffWithContext (
232
+ ctx ,
233
+ wait.Backoff {
234
+ Duration : time .Millisecond * 200 ,
235
+ Factor : 2 ,
236
+ Jitter : 0.5 ,
237
+ Steps : 4 ,
238
+ Cap : time .Millisecond * 3000 ,
239
+ },
240
+ // Function returns true if the condition is satisfied, or an error if the loop should be aborted.
241
+ NewRetryUpdateFunc (upd .cfg .Client , upd .cfg .Client .Status (), nsname , obj , upd .cfg .Logger , statusSetter ),
242
+ )
243
+ if err != nil && ! errors .Is (err , context .Canceled ) {
242
244
upd .cfg .Logger .Error (
243
245
err ,
244
246
"Failed to update status" ,
@@ -247,3 +249,57 @@ func (upd *UpdaterImpl) writeStatuses(
247
249
"kind" , obj .GetObjectKind ().GroupVersionKind ().Kind )
248
250
}
249
251
}
252
+
253
+ // NewRetryUpdateFunc returns a function which will be used in wait.ExponentialBackoffWithContext.
254
+ // The function will attempt to Update a kubernetes resource and will be retried in
255
+ // wait.ExponentialBackoffWithContext if an error occurs. Exported for testing purposes.
256
+ //
257
+ // wait.ExponentialBackoffWithContext will retry if this function returns nil as its error,
258
+ // which is what we want if we encounter an error from the functions we call. However,
259
+ // the linter will complain if we return nil if an error was found.
260
+ //
261
+ //nolint:nilerr
262
+ func NewRetryUpdateFunc (
263
+ getter controller.Getter ,
264
+ updater K8sUpdater ,
265
+ nsname types.NamespacedName ,
266
+ obj client.Object ,
267
+ logger logr.Logger ,
268
+ statusSetter func (client.Object ),
269
+ ) func (ctx context.Context ) (bool , error ) {
270
+ return func (ctx context.Context ) (bool , error ) {
271
+ // The function handles errors by reporting them in the logs.
272
+ // We need to get the latest version of the resource.
273
+ // Otherwise, the Update status API call can fail.
274
+ // Note: the default client uses a cache for reads, so we're not making an unnecessary API call here.
275
+ // the default is configurable in the Manager options.
276
+ if err := getter .Get (ctx , nsname , obj ); err != nil {
277
+ // apierrors.IsNotFound(err) can happen when the resource is deleted,
278
+ // so no need to retry or return an error.
279
+ if apierrors .IsNotFound (err ) {
280
+ return true , nil
281
+ }
282
+ logger .V (1 ).Info (
283
+ "Encountered error when getting resource to update status" ,
284
+ "error" , err ,
285
+ "namespace" , nsname .Namespace ,
286
+ "name" , nsname .Name ,
287
+ "kind" , obj .GetObjectKind ().GroupVersionKind ().Kind )
288
+ return false , nil
289
+ }
290
+
291
+ statusSetter (obj )
292
+
293
+ if err := updater .Update (ctx , obj ); err != nil {
294
+ logger .V (1 ).Info (
295
+ "Encountered error updating status" ,
296
+ "error" , err ,
297
+ "namespace" , nsname .Namespace ,
298
+ "name" , nsname .Name ,
299
+ "kind" , obj .GetObjectKind ().GroupVersionKind ().Kind )
300
+ return false , nil
301
+ }
302
+
303
+ return true , nil
304
+ }
305
+ }
0 commit comments