1
1
using System . Collections . Concurrent ;
2
2
using System . Diagnostics ;
3
3
using System . Net ;
4
+ using System . Net . Sockets ;
4
5
using System . Runtime . CompilerServices ;
6
+ using System . Text . RegularExpressions ;
5
7
6
8
using k8s ;
7
9
using k8s . Autorest ;
8
10
using k8s . Models ;
9
11
10
12
using KubeOps . Abstractions . Entities ;
13
+ using KubeOps . Operator . Watcher ;
11
14
using KubeOps . Transpiler ;
12
15
13
16
namespace KubeOps . KubernetesClient ;
@@ -19,6 +22,22 @@ public class KubernetesClient : IKubernetesClient
19
22
private const string DefaultNamespace = "default" ;
20
23
21
24
private static readonly ConcurrentDictionary < Type , EntityMetadata > MetadataCache = new ( ) ;
25
+ private static List < int ? > ResourceFailureCodes = ( ( int ? [ ] ) [ ( int ) HttpStatusCode . GatewayTimeout , ( int ) HttpStatusCode . Gone ] ) . ToList ( ) ;
26
+
27
+ /// <summary>
28
+ /// HACK to ge the last applicable resourceVersion from the exception.
29
+ /// </summary>
30
+ /// <example>
31
+ /// "too old resource version: 512122628 (544688086)".
32
+ /// </example>
33
+ private static string ? ResourceVersionFromException ( Exception ? ex )
34
+ {
35
+ if ( ex ? . Message is null ) return null ;
36
+
37
+ var pattern = @"^\s*too old resource version.*\(([a-zA-Z0-9_-]+)\)\s*$" ;
38
+ var match = Regex . Match ( ex . Message , pattern ) ;
39
+ return ( match . Groups . Count > 1 ) ? match . Groups [ 1 ] . Value : null ;
40
+ }
22
41
23
42
private readonly KubernetesClientConfiguration _clientConfig ;
24
43
private readonly IKubernetes _client ;
@@ -29,18 +48,14 @@ public class KubernetesClient : IKubernetesClient
29
48
/// The client will use the default configuration.
30
49
/// </summary>
31
50
public KubernetesClient ( )
32
- : this ( KubernetesClientConfiguration . BuildDefaultConfig ( ) )
33
- {
34
- }
51
+ : this ( KubernetesClientConfiguration . BuildDefaultConfig ( ) ) { }
35
52
36
53
/// <summary>
37
54
/// Create a new Kubernetes client for the given entity with a custom client configuration.
38
55
/// </summary>
39
56
/// <param name="clientConfig">The config for the underlying Kubernetes client.</param>
40
57
public KubernetesClient ( KubernetesClientConfiguration clientConfig )
41
- : this ( clientConfig , new Kubernetes ( clientConfig ) )
42
- {
43
- }
58
+ : this ( clientConfig , new Kubernetes ( clientConfig ) ) { }
44
59
45
60
/// <summary>
46
61
/// Create a new Kubernetes client for the given entity with a custom client configuration and client.
@@ -180,7 +195,7 @@ public string GetCurrentNamespace(string downwardApiEnvName = "POD_NAMESPACE")
180
195
}
181
196
182
197
/// <inheritdoc />
183
- public async Task < IList < TEntity > > ListAsync < TEntity > (
198
+ public async Task < ( string ? Version , IList < TEntity > Items ) > ListAsync < TEntity > (
184
199
string ? @namespace = null ,
185
200
string ? labelSelector = null ,
186
201
CancellationToken cancellationToken = default )
@@ -189,7 +204,7 @@ public async Task<IList<TEntity>> ListAsync<TEntity>(
189
204
ThrowIfDisposed ( ) ;
190
205
191
206
var metadata = GetMetadata < TEntity > ( ) ;
192
- return ( @namespace switch
207
+ var result = @namespace switch
193
208
{
194
209
null => await _client . CustomObjects . ListClusterCustomObjectAsync < EntityList < TEntity > > (
195
210
metadata . Group ?? string . Empty ,
@@ -204,17 +219,20 @@ public async Task<IList<TEntity>> ListAsync<TEntity>(
204
219
metadata . PluralName ,
205
220
labelSelector : labelSelector ,
206
221
cancellationToken : cancellationToken ) ,
207
- } ) . Items ;
222
+ } ;
223
+
224
+ return ( result . Metadata . ResourceVersion , result . Items ) ;
208
225
}
209
226
210
227
/// <inheritdoc />
211
- public IList < TEntity > List < TEntity > ( string ? @namespace = null , string ? labelSelector = null )
228
+ public ( string ? Version , IList < TEntity > Items ) List < TEntity > ( string ? @namespace = null ,
229
+ string ? labelSelector = null )
212
230
where TEntity : IKubernetesObject < V1ObjectMeta >
213
231
{
214
232
ThrowIfDisposed ( ) ;
215
233
216
234
var metadata = GetMetadata < TEntity > ( ) ;
217
- return ( @namespace switch
235
+ var result = @namespace switch
218
236
{
219
237
null => _client . CustomObjects . ListClusterCustomObject < EntityList < TEntity > > (
220
238
metadata . Group ?? string . Empty ,
@@ -227,7 +245,9 @@ public IList<TEntity> List<TEntity>(string? @namespace = null, string? labelSele
227
245
@namespace ,
228
246
metadata . PluralName ,
229
247
labelSelector : labelSelector ) ,
230
- } ) . Items ;
248
+ } ;
249
+
250
+ return ( result . Metadata . ResourceVersion , result . Items ) ;
231
251
}
232
252
233
253
/// <inheritdoc />
@@ -339,6 +359,51 @@ public async Task DeleteAsync<TEntity>(
339
359
}
340
360
}
341
361
362
+ /// <summary>
363
+ ///
364
+ /// </summary>
365
+ /// <param name="onEvent"></param>
366
+ /// <param name="namespace"></param>
367
+ /// <param name="resourceVersion"></param>
368
+ /// <param name="labelSelector"></param>
369
+ /// <param name="cancellationToken"></param>
370
+ /// <typeparam name="TEntity"></typeparam>
371
+ public async Task WatchSafeAsync < TEntity > (
372
+ Func < WatchEventType , TEntity ? , CancellationToken , Task > eventTask ,
373
+ string ? @namespace = null ,
374
+ string ? resourceVersion = null ,
375
+ string ? labelSelector = null ,
376
+ CancellationToken cancellationToken = default )
377
+ where TEntity : IKubernetesObject < V1ObjectMeta >
378
+ {
379
+ var currentVersion = resourceVersion ;
380
+ while ( ! cancellationToken . IsCancellationRequested )
381
+ {
382
+ try
383
+ {
384
+ await foreach ( var ( typ , e ) in WatchAsync < TEntity > ( @namespace , currentVersion , labelSelector , cancellationToken ) )
385
+ {
386
+ currentVersion = e . ResourceVersion ( ) ;
387
+ await eventTask ( typ , e , cancellationToken ) ;
388
+ }
389
+ }
390
+ catch ( OperationCanceledException ) when ( cancellationToken . IsCancellationRequested )
391
+ {
392
+ // OK, end the watch
393
+ }
394
+ catch ( KubernetesException cause ) when ( ResourceFailureCodes . Contains ( cause . Status . Code ) )
395
+ {
396
+ currentVersion = ResourceVersionFromException ( cause ) ;
397
+ if ( currentVersion == null ) break ; // bail out of watch
398
+ }
399
+ catch ( Exception cause ) when ( cause . All ( ) . Any ( e => e . Message . Contains ( "server reset the stream" )
400
+ || e is SocketException { ErrorCode : 104 } ) )
401
+ {
402
+ await Task . Delay ( TimeSpan . FromSeconds ( 1 ) , cancellationToken ) ;
403
+ }
404
+ }
405
+ }
406
+
342
407
/// <inheritdoc />
343
408
public Watcher < TEntity > Watch < TEntity > (
344
409
Action < WatchEventType , TEntity > onEvent ,
0 commit comments