@@ -28,6 +28,8 @@ public class NamespaceEventHandler
28
28
// Concurrent Federated Identity Credentials writes under the same managed identity are not supported
29
29
private static readonly SemaphoreSlim FederatedCredentialWriteSemaphore = new ( 1 , 1 ) ;
30
30
31
+ private Dictionary < string , UserAssignedIdentityResource > WorkloadAppCache = [ ] ;
32
+
31
33
public List < string > WorkloadAppPool ;
32
34
public string WorkloadAppIssuer ;
33
35
@@ -62,6 +64,57 @@ public NamespaceEventHandler(
62
64
. CreateLogger ( ) ;
63
65
}
64
66
67
+ public async Task SyncCredentials ( )
68
+ {
69
+ try
70
+ {
71
+ Logger . Information ( $ "Waiting for federated credential write semaphore") ;
72
+ await FederatedCredentialWriteSemaphore . WaitAsync ( ) ;
73
+ await _syncCredentials ( ) ;
74
+ }
75
+ finally
76
+ {
77
+ Logger . Information ( "Releasing federated credential write semaphore" ) ;
78
+ FederatedCredentialWriteSemaphore . Release ( ) ;
79
+ }
80
+ }
81
+
82
+ public async Task _syncCredentials ( )
83
+ {
84
+ Logger . Information ( "Syncing namespaced federated credentials, this may take a minute..." ) ;
85
+
86
+ var namespaces = await Client . ListNamespaceAsync ( ) ;
87
+ foreach ( var app in WorkloadAppPool )
88
+ {
89
+ var resourceId = UserAssignedIdentityResource . CreateResourceIdentifier ( SubscriptionId , ClusterGroup , app ) ;
90
+ var userAssignedIdentity = ArmClient . GetUserAssignedIdentityResource ( resourceId ) ;
91
+ var identityResource = await userAssignedIdentity . GetAsync ( ) ;
92
+ var fedCreds = userAssignedIdentity . GetFederatedIdentityCredentials ( ) ;
93
+ await foreach ( var item in fedCreds . GetAllAsync ( ) )
94
+ {
95
+ if ( ! namespaces . Items . Any ( ns => item . Data . Name == CreateFederatedIdentityCredentialName ( ns ) ) )
96
+ {
97
+ if ( ! string . IsNullOrEmpty ( WatchNamespace ) && item . Data . Name != CreateFederatedIdentityCredentialName ( WatchNamespace ) )
98
+ {
99
+ Logger . Information ( $ "Skipping delete federated credential '{ item . Data . Name } ' because it is not the watched namespace '{ WatchNamespace } '") ;
100
+ continue ;
101
+ }
102
+ // Only perform delete operations for namespace state that may have changed if the watcher was not running.
103
+ // Any create operations will be handled after initialization as the watch stream processes all active namespaces on startup
104
+ Logger . Information ( $ "Deleting federated identity credential '{ item . Data . Name } ' for managed identity '{ app } ' as the corresponding namespace no longer exists.") ;
105
+ WorkloadAppCache . Remove ( item . Data . Name ) ;
106
+ var lro = await item . DeleteAsync ( Azure . WaitUntil . Completed ) ;
107
+ }
108
+ else
109
+ {
110
+ WorkloadAppCache [ item . Data . Name ] = identityResource . Value ;
111
+ }
112
+ }
113
+ }
114
+
115
+ Logger . Information ( $ "Federated credential sync complete. Cached { WorkloadAppCache . Count } federated credentials.") ;
116
+ }
117
+
65
118
public async Task Watch ( CancellationToken cancellationToken )
66
119
{
67
120
string resourceVersion = null ;
@@ -125,6 +178,10 @@ public void HandleNamespaceEvent(WatchEventType eventType, V1Namespace ns)
125
178
Logger . Information ( $ "Skipping namespace '{ ns . Name ( ) } ' because it is not the watched namespace '{ WatchNamespace } '") ;
126
179
return ;
127
180
}
181
+ if ( ns . Status ? . Phase == "Terminating" )
182
+ {
183
+ return ;
184
+ }
128
185
129
186
using ( LogContext . PushProperty ( "namespace" , ns . Name ( ) ) )
130
187
{
@@ -139,24 +196,17 @@ public void HandleNamespaceEvent(WatchEventType eventType, V1Namespace ns)
139
196
}
140
197
} ) ;
141
198
}
142
- else if ( eventType == WatchEventType . Deleted )
143
- {
144
- DeleteFederatedIdentityCredential ( ns ) . ContinueWith ( t =>
145
- {
146
- Logger . Information ( "Releasing federated credential write semaphore" ) ;
147
- FederatedCredentialWriteSemaphore . Release ( ) ;
148
- if ( t . Exception != null )
149
- {
150
- Logger . Error ( t . Exception , "Error deleting federated identity credential." ) ;
151
- }
152
- } ) ;
153
- }
154
199
}
155
200
}
156
201
157
202
public string CreateFederatedIdentityCredentialName ( V1Namespace ns )
158
203
{
159
- return $ "stress-{ ns . Name ( ) } ";
204
+ return CreateFederatedIdentityCredentialName ( ns . Name ( ) ) ;
205
+ }
206
+
207
+ public string CreateFederatedIdentityCredentialName ( string ns )
208
+ {
209
+ return $ "stress-{ ns } ";
160
210
}
161
211
162
212
public async Task InitializeWorkloadIdForNamespace ( V1Namespace ns )
@@ -175,14 +225,21 @@ public async Task InitializeWorkloadIdForNamespace(V1Namespace ns)
175
225
var identityData = await selectedWorkloadIdentity . GetAsync ( ) ;
176
226
var selectedWorkloadAppId = identityData . Value . Data . ClientId . ToString ( ) ;
177
227
178
- var meta = new V1ObjectMeta ( ) {
228
+ var meta = new V1ObjectMeta ( )
229
+ {
179
230
Name = ns . Name ( ) ,
180
231
NamespaceProperty = ns . Name ( ) ,
181
232
Annotations = new Dictionary < string , string > ( ) {
182
233
{ "azure.workload.identity/client-id" , selectedWorkloadAppId }
183
234
}
184
235
} ;
185
236
var serviceAccount = new V1ServiceAccount ( metadata : meta ) ;
237
+ var allAccounts = await Client . ListNamespacedServiceAccountAsync ( ns . Name ( ) ) ;
238
+ if ( allAccounts . Items . Any ( sa => sa . Name ( ) == ns . Name ( ) ) )
239
+ {
240
+ Logger . Information ( $ "Service account '{ ns . Name ( ) } /{ ns . Name ( ) } ' already exists, skipping creation.") ;
241
+ return ;
242
+ }
186
243
await Client . CreateNamespacedServiceAccountAsync ( serviceAccount , ns . Name ( ) ) ;
187
244
Logger . Information ( $ "Created service account '{ ns . Name ( ) } /{ ns . Name ( ) } ' with workload client id '{ selectedWorkloadAppId } '") ;
188
245
}
@@ -200,6 +257,12 @@ public async Task<UserAssignedIdentityResource> CreateFederatedIdentityCredentia
200
257
Logger . Information ( $ "Waiting for federated credential write semaphore") ;
201
258
await FederatedCredentialWriteSemaphore . WaitAsync ( ) ;
202
259
260
+ if ( WorkloadAppCache . ContainsKey ( credentialName ) )
261
+ {
262
+ Logger . Information ( $ "Found cache entry for federated credential { credentialName } , returning identity { WorkloadAppCache [ credentialName ] . Data . ClientId } ") ;
263
+ return await WorkloadAppCache [ credentialName ] . GetAsync ( ) ;
264
+ }
265
+
203
266
foreach ( var workloadApp in WorkloadAppPool )
204
267
{
205
268
var userAssignedIdentityResourceId = UserAssignedIdentityResource . CreateResourceIdentifier ( SubscriptionId , ClusterGroup , workloadApp ) ;
@@ -246,50 +309,10 @@ public async Task<UserAssignedIdentityResource> CreateFederatedIdentityCredentia
246
309
Logger . Information ( $ "Creating/updating federated identity credential '{ credentialName } ' " +
247
310
$ "with subject '{ subject } ' for managed identity '{ selectedWorkloadApp } '") ;
248
311
var lro = await federatedIdentityCredential . UpdateAsync ( Azure . WaitUntil . Completed , fedCredData ) ;
312
+ WorkloadAppCache [ credentialName ] = selectedIdentity ;
249
313
Logger . Information ( $ "Created federated identity credential '{ lro . Value . Data . Name } '") ;
250
314
251
315
return selectedIdentity ;
252
316
}
253
-
254
- public async Task DeleteFederatedIdentityCredential ( V1Namespace ns )
255
- {
256
- var credentialName = CreateFederatedIdentityCredentialName ( ns ) ;
257
- var workloadApp = "" ;
258
- foreach ( var app in WorkloadAppPool )
259
- {
260
- var resourceId = UserAssignedIdentityResource . CreateResourceIdentifier ( SubscriptionId , ClusterGroup , app ) ;
261
- var userAssignedIdentity = ArmClient . GetUserAssignedIdentityResource ( resourceId ) ;
262
- var fedCreds = userAssignedIdentity . GetFederatedIdentityCredentials ( ) ;
263
- await foreach ( var item in fedCreds . GetAllAsync ( ) )
264
- {
265
- if ( item . Data . Name == credentialName )
266
- {
267
- workloadApp = app ;
268
- break ;
269
- }
270
- }
271
- if ( ! String . IsNullOrEmpty ( workloadApp ) )
272
- {
273
- break ;
274
- }
275
- }
276
-
277
- if ( string . IsNullOrEmpty ( workloadApp ) )
278
- {
279
- Logger . Warning ( $ "Federated identity credential '{ credentialName } ' not found in workload app pool. Skipping delete.") ;
280
- return ;
281
- }
282
-
283
- var federatedIdentityCredentialResourceId = FederatedIdentityCredentialResource . CreateResourceIdentifier (
284
- SubscriptionId , ClusterGroup , workloadApp , credentialName ) ;
285
- var federatedIdentityCredential = ArmClient . GetFederatedIdentityCredentialResource ( federatedIdentityCredentialResourceId ) ;
286
-
287
- Logger . Information ( $ "Waiting for federated credential write semaphore") ;
288
- await FederatedCredentialWriteSemaphore . WaitAsync ( ) ;
289
-
290
- Logger . Information ( $ "Deleting federated identity credential '{ credentialName } ' for managed identity '{ workloadApp } '") ;
291
- var lro = await federatedIdentityCredential . DeleteAsync ( Azure . WaitUntil . Completed ) ;
292
- Logger . Information ( $ "Deleted federated identity credential '{ credentialName } '") ;
293
- }
294
317
}
295
318
}
0 commit comments