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