Skip to content

Commit ab6c404

Browse files
authored
Merge b9f3016 into 02198e5
2 parents 02198e5 + b9f3016 commit ab6c404

File tree

2 files changed

+90
-55
lines changed

2 files changed

+90
-55
lines changed

tools/stress-cluster/services/Stress.Watcher/src/NamespaceEventHandler.cs

+78-55
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ public class NamespaceEventHandler
2828
// Concurrent Federated Identity Credentials writes under the same managed identity are not supported
2929
private static readonly SemaphoreSlim FederatedCredentialWriteSemaphore = new(1, 1);
3030

31+
private Dictionary<string, UserAssignedIdentityResource> WorkloadAppCache = [];
32+
3133
public List<string> WorkloadAppPool;
3234
public string WorkloadAppIssuer;
3335

@@ -62,6 +64,57 @@ public NamespaceEventHandler(
6264
.CreateLogger();
6365
}
6466

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+
65118
public async Task Watch(CancellationToken cancellationToken)
66119
{
67120
string resourceVersion = null;
@@ -125,6 +178,10 @@ public void HandleNamespaceEvent(WatchEventType eventType, V1Namespace ns)
125178
Logger.Information($"Skipping namespace '{ns.Name()}' because it is not the watched namespace '{WatchNamespace}'");
126179
return;
127180
}
181+
if (ns.Status?.Phase == "Terminating")
182+
{
183+
return;
184+
}
128185

129186
using (LogContext.PushProperty("namespace", ns.Name()))
130187
{
@@ -139,24 +196,17 @@ public void HandleNamespaceEvent(WatchEventType eventType, V1Namespace ns)
139196
}
140197
});
141198
}
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-
}
154199
}
155200
}
156201

157202
public string CreateFederatedIdentityCredentialName(V1Namespace ns)
158203
{
159-
return $"stress-{ns.Name()}";
204+
return CreateFederatedIdentityCredentialName(ns.Name());
205+
}
206+
207+
public string CreateFederatedIdentityCredentialName(string ns)
208+
{
209+
return $"stress-{ns}";
160210
}
161211

162212
public async Task InitializeWorkloadIdForNamespace(V1Namespace ns)
@@ -175,14 +225,21 @@ public async Task InitializeWorkloadIdForNamespace(V1Namespace ns)
175225
var identityData = await selectedWorkloadIdentity.GetAsync();
176226
var selectedWorkloadAppId = identityData.Value.Data.ClientId.ToString();
177227

178-
var meta = new V1ObjectMeta(){
228+
var meta = new V1ObjectMeta()
229+
{
179230
Name = ns.Name(),
180231
NamespaceProperty = ns.Name(),
181232
Annotations = new Dictionary<string, string>(){
182233
{ "azure.workload.identity/client-id", selectedWorkloadAppId }
183234
}
184235
};
185236
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+
}
186243
await Client.CreateNamespacedServiceAccountAsync(serviceAccount, ns.Name());
187244
Logger.Information($"Created service account '{ns.Name()}/{ns.Name()}' with workload client id '{selectedWorkloadAppId}'");
188245
}
@@ -200,6 +257,12 @@ public async Task<UserAssignedIdentityResource> CreateFederatedIdentityCredentia
200257
Logger.Information($"Waiting for federated credential write semaphore");
201258
await FederatedCredentialWriteSemaphore.WaitAsync();
202259

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+
203266
foreach (var workloadApp in WorkloadAppPool)
204267
{
205268
var userAssignedIdentityResourceId = UserAssignedIdentityResource.CreateResourceIdentifier(SubscriptionId, ClusterGroup, workloadApp);
@@ -246,50 +309,10 @@ public async Task<UserAssignedIdentityResource> CreateFederatedIdentityCredentia
246309
Logger.Information($"Creating/updating federated identity credential '{credentialName}' " +
247310
$"with subject '{subject}' for managed identity '{selectedWorkloadApp}'");
248311
var lro = await federatedIdentityCredential.UpdateAsync(Azure.WaitUntil.Completed, fedCredData);
312+
WorkloadAppCache[credentialName] = selectedIdentity;
249313
Logger.Information($"Created federated identity credential '{lro.Value.Data.Name}'");
250314

251315
return selectedIdentity;
252316
}
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-
}
294317
}
295318
}

tools/stress-cluster/services/Stress.Watcher/src/Program.cs

+12
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Azure.ResourceManager;
1010
using dotenv.net;
1111
using YamlDotNet.RepresentationModel;
12+
using System.Security.Cryptography;
1213

1314
namespace Stress.Watcher
1415
{
@@ -79,6 +80,8 @@ static async Task Program(Options options)
7980
var namespaceEventHandler = new NamespaceEventHandler(
8081
client, armClient, workloadConfig.SubscriptionId, workloadConfig.ClusterGroup,
8182
workloadConfig.WorkloadAppPool, workloadConfig.WorkloadAppIssuer, options.Namespace);
83+
await namespaceEventHandler.SyncCredentials();
84+
_ = PollAndSyncCredentials(namespaceEventHandler, 288); // poll every 12 hours
8285

8386
var cts = new CancellationTokenSource();
8487
var taskList = new List<Task>
@@ -164,5 +167,14 @@ static WorkloadAuthConfig GetWorkloadConfigValues(Options options, Boolean isLoc
164167
ClusterGroup = clusterGroup
165168
};
166169
}
170+
171+
static async Task PollAndSyncCredentials(NamespaceEventHandler namespaceHandler, int minutes)
172+
{
173+
while (true)
174+
{
175+
await Task.Delay(TimeSpan.FromMinutes(minutes));
176+
await namespaceHandler.SyncCredentials();
177+
}
178+
}
167179
}
168180
}

0 commit comments

Comments
 (0)