|
2 | 2 | // The .NET Foundation licenses this file to you under the MIT license.
|
3 | 3 |
|
4 | 4 | using System.Globalization;
|
5 |
| -using System.Net.Http.Json; |
6 | 5 | using System.Text;
|
7 |
| -using System.Text.Json; |
8 |
| -using System.Text.Json.Nodes; |
9 |
| -using System.Text.Json.Serialization; |
10 | 6 | using Aspire.Hosting;
|
11 | 7 | using Aspire.Hosting.ApplicationModel;
|
12 | 8 | using Aspire.Hosting.Redis;
|
13 | 9 | using Microsoft.Extensions.DependencyInjection;
|
14 |
| -using Microsoft.Extensions.Logging; |
15 |
| -using Polly; |
16 | 10 |
|
17 | 11 | namespace Aspire.Hosting;
|
18 | 12 |
|
@@ -230,223 +224,42 @@ public static IResourceBuilder<RedisResource> WithRedisInsight(this IResourceBui
|
230 | 224 |
|
231 | 225 | var resource = new RedisInsightResource(containerName);
|
232 | 226 | var resourceBuilder = builder.ApplicationBuilder.AddResource(resource)
|
233 |
| - .WithImage(RedisContainerImageTags.RedisInsightImage, RedisContainerImageTags.RedisInsightTag) |
234 |
| - .WithImageRegistry(RedisContainerImageTags.RedisInsightRegistry) |
235 |
| - .WithHttpEndpoint(targetPort: 5540, name: "http") |
236 |
| - .ExcludeFromManifest(); |
237 |
| - |
238 |
| - // We need to wait for all endpoints to be allocated before attempting to import databases |
239 |
| - var endpointsAllocatedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); |
240 |
| - |
241 |
| - builder.ApplicationBuilder.Eventing.Subscribe<AfterEndpointsAllocatedEvent>((e, ct) => |
242 |
| - { |
243 |
| - endpointsAllocatedTcs.TrySetResult(); |
244 |
| - return Task.CompletedTask; |
245 |
| - }); |
246 |
| - |
247 |
| - builder.ApplicationBuilder.Eventing.Subscribe<ResourceReadyEvent>(resource, async (e, ct) => |
248 |
| - { |
249 |
| - var redisInstances = builder.ApplicationBuilder.Resources.OfType<RedisResource>(); |
250 |
| - |
251 |
| - if (!redisInstances.Any()) |
| 227 | + .WithImage(RedisContainerImageTags.RedisInsightImage, RedisContainerImageTags.RedisInsightTag) |
| 228 | + .WithImageRegistry(RedisContainerImageTags.RedisInsightRegistry) |
| 229 | + .WithHttpEndpoint(targetPort: 5540, name: "http") |
| 230 | + .WithEnvironment(context => |
252 | 231 | {
|
253 |
| - // No-op if there are no Redis resources present. |
254 |
| - return; |
255 |
| - } |
256 |
| - |
257 |
| - // Wait for all endpoints to be allocated before attempting to import databases |
258 |
| - await endpointsAllocatedTcs.Task.ConfigureAwait(false); |
259 |
| - |
260 |
| - var redisInsightResource = builder.ApplicationBuilder.Resources.OfType<RedisInsightResource>().Single(); |
261 |
| - var insightEndpoint = redisInsightResource.PrimaryEndpoint; |
262 |
| - |
263 |
| - using var client = new HttpClient(); |
264 |
| - client.BaseAddress = new Uri($"{insightEndpoint.Scheme}://{insightEndpoint.Host}:{insightEndpoint.Port}"); |
| 232 | + var redisInstances = builder.ApplicationBuilder.Resources.OfType<RedisResource>(); |
265 | 233 |
|
266 |
| - var rls = e.Services.GetRequiredService<ResourceLoggerService>(); |
267 |
| - var resourceLogger = rls.GetLogger(resource); |
268 |
| - |
269 |
| - await ImportRedisDatabases(resourceLogger, redisInstances, client, ct).ConfigureAwait(false); |
270 |
| - }); |
271 |
| - |
272 |
| - resourceBuilder.WithRelationship(builder.Resource, "RedisInsight"); |
273 |
| - |
274 |
| - configureContainer?.Invoke(resourceBuilder); |
275 |
| - |
276 |
| - return builder; |
277 |
| - } |
278 |
| - |
279 |
| - static async Task ImportRedisDatabases(ILogger resourceLogger, IEnumerable<RedisResource> redisInstances, HttpClient client, CancellationToken cancellationToken) |
280 |
| - { |
281 |
| - var databasesPath = "/api/databases"; |
282 |
| - |
283 |
| - var pipeline = new ResiliencePipelineBuilder().AddRetry(new Polly.Retry.RetryStrategyOptions |
284 |
| - { |
285 |
| - Delay = TimeSpan.FromSeconds(2), |
286 |
| - MaxRetryAttempts = 5, |
287 |
| - }).Build(); |
288 |
| - |
289 |
| - await pipeline.ExecuteAsync(async (ctx) => |
290 |
| - { |
291 |
| - await InitializeRedisInsightSettings(client, resourceLogger, ctx).ConfigureAwait(false); |
292 |
| - }, cancellationToken).ConfigureAwait(false); |
293 |
| - |
294 |
| - using (var stream = new MemoryStream()) |
295 |
| - { |
296 |
| - // As part of configuring RedisInsight we need to factor in the possibility that the |
297 |
| - // container resource is being run with persistence turned on. In this case we need |
298 |
| - // to get the list of existing databases because we might need to delete some. |
299 |
| - var lookup = await pipeline.ExecuteAsync(async (ctx) => |
300 |
| - { |
301 |
| - var getDatabasesResponse = await client.GetFromJsonAsync<RedisDatabaseDto[]>(databasesPath, cancellationToken).ConfigureAwait(false); |
302 |
| - return getDatabasesResponse?.ToLookup( |
303 |
| - i => i.Name ?? throw new InvalidDataException("Database name is missing."), |
304 |
| - i => i.Id ?? throw new InvalidDataException("Database ID is missing.")); |
305 |
| - }, cancellationToken).ConfigureAwait(false); |
306 |
| - |
307 |
| - var databasesToDelete = new List<Guid>(); |
308 |
| - |
309 |
| - using var writer = new Utf8JsonWriter(stream); |
310 |
| - |
311 |
| - writer.WriteStartArray(); |
312 |
| - |
313 |
| - foreach (var redisResource in redisInstances) |
314 |
| - { |
315 |
| - if (lookup is { } && lookup.Contains(redisResource.Name)) |
| 234 | + if (!redisInstances.Any()) |
316 | 235 | {
|
317 |
| - // It is possible that there are multiple databases with |
318 |
| - // a conflicting name so we delete them all. This just keeps |
319 |
| - // track of the specific ID that we need to delete. |
320 |
| - databasesToDelete.AddRange(lookup[redisResource.Name]); |
| 236 | + // No-op if there are no Redis resources present. |
| 237 | + return; |
321 | 238 | }
|
322 | 239 |
|
323 |
| - if (redisResource.PrimaryEndpoint.IsAllocated) |
| 240 | + var counter = 1; |
| 241 | + |
| 242 | + foreach (var redisInstance in redisInstances) |
324 | 243 | {
|
325 |
| - var endpoint = redisResource.PrimaryEndpoint; |
326 |
| - writer.WriteStartObject(); |
327 |
| - |
328 |
| - writer.WriteString("host", redisResource.Name); |
329 |
| - writer.WriteNumber("port", endpoint.TargetPort!.Value); |
330 |
| - writer.WriteString("name", redisResource.Name); |
331 |
| - writer.WriteNumber("db", 0); |
332 |
| - writer.WriteNull("username"); |
333 |
| - if (redisResource.PasswordParameter is { } passwordParam) |
334 |
| - { |
335 |
| - writer.WriteString("password", passwordParam.Value); |
336 |
| - } |
337 |
| - else |
| 244 | + // RedisInsight assumes Redis is being accessed over a default Aspire container network and hardcodes the resource address |
| 245 | + context.EnvironmentVariables.Add($"RI_REDIS_HOST{counter}", redisInstance.Name); |
| 246 | + context.EnvironmentVariables.Add($"RI_REDIS_PORT{counter}", redisInstance.PrimaryEndpoint.TargetPort!.Value); |
| 247 | + context.EnvironmentVariables.Add($"RI_REDIS_ALIAS{counter}", redisInstance.Name); |
| 248 | + if (redisInstance.PasswordParameter is not null) |
338 | 249 | {
|
339 |
| - writer.WriteNull("password"); |
| 250 | + context.EnvironmentVariables.Add($"RI_REDIS_PASSWORD{counter}", redisInstance.PasswordParameter.Value); |
340 | 251 | }
|
341 |
| - writer.WriteString("connectionType", "STANDALONE"); |
342 |
| - writer.WriteEndObject(); |
343 |
| - } |
344 |
| - } |
345 |
| - writer.WriteEndArray(); |
346 |
| - await writer.FlushAsync(cancellationToken).ConfigureAwait(false); |
347 |
| - stream.Seek(0, SeekOrigin.Begin); |
348 |
| - |
349 |
| - var content = new MultipartFormDataContent(); |
350 |
| - |
351 |
| - var fileContent = new StreamContent(stream); |
352 |
| - |
353 |
| - content.Add(fileContent, "file", "RedisInsight_connections.json"); |
354 |
| - |
355 |
| - var apiUrl = $"{databasesPath}/import"; |
356 | 252 |
|
357 |
| - try |
358 |
| - { |
359 |
| - if (databasesToDelete.Any()) |
360 |
| - { |
361 |
| - await pipeline.ExecuteAsync(async (ctx) => |
362 |
| - { |
363 |
| - // Create a DELETE request to send to the existing instance of |
364 |
| - // RedisInsight with the IDs of the database to delete. |
365 |
| - var deleteContent = JsonContent.Create(new |
366 |
| - { |
367 |
| - ids = databasesToDelete |
368 |
| - }); |
369 |
| - |
370 |
| - var deleteRequest = new HttpRequestMessage(HttpMethod.Delete, databasesPath) |
371 |
| - { |
372 |
| - Content = deleteContent |
373 |
| - }; |
374 |
| - |
375 |
| - var deleteResponse = await client.SendAsync(deleteRequest, cancellationToken).ConfigureAwait(false); |
376 |
| - deleteResponse.EnsureSuccessStatusCode(); |
377 |
| - |
378 |
| - }, cancellationToken).ConfigureAwait(false); |
| 253 | + counter++; |
379 | 254 | }
|
| 255 | + }) |
| 256 | + .WithRelationship(builder.Resource, "RedisInsight") |
| 257 | + .ExcludeFromManifest(); |
380 | 258 |
|
381 |
| - await pipeline.ExecuteAsync(async (ctx) => |
382 |
| - { |
383 |
| - var response = await client.PostAsync(apiUrl, content, ctx) |
384 |
| - .ConfigureAwait(false); |
385 |
| - |
386 |
| - response.EnsureSuccessStatusCode(); |
387 |
| - }, cancellationToken).ConfigureAwait(false); |
388 |
| - |
389 |
| - } |
390 |
| - catch (Exception ex) |
391 |
| - { |
392 |
| - resourceLogger.LogError("Could not import Redis databases into RedisInsight. Reason: {reason}", ex.Message); |
393 |
| - } |
394 |
| - } |
395 |
| - } |
396 |
| - } |
397 |
| - |
398 |
| - /// <summary> |
399 |
| - /// Initializes the Redis Insight settings to work around https://github.com/RedisInsight/RedisInsight/issues/3452. |
400 |
| - /// Redis Insight requires the encryption property to be set if the Redis database connection contains a password. |
401 |
| - /// </summary> |
402 |
| - private static async Task InitializeRedisInsightSettings(HttpClient client, ILogger resourceLogger, CancellationToken ct) |
403 |
| - { |
404 |
| - if (await AreSettingsInitialized(client, ct).ConfigureAwait(false)) |
405 |
| - { |
406 |
| - return; |
407 |
| - } |
408 |
| - |
409 |
| - var jsonContent = JsonContent.Create(new |
410 |
| - { |
411 |
| - agreements = new |
412 |
| - { |
413 |
| - // all 4 are required to be set |
414 |
| - eula = false, |
415 |
| - analytics = false, |
416 |
| - notifications = false, |
417 |
| - encryption = false, |
418 |
| - } |
419 |
| - }); |
| 259 | + configureContainer?.Invoke(resourceBuilder); |
420 | 260 |
|
421 |
| - var response = await client.PatchAsync("/api/settings", jsonContent, ct).ConfigureAwait(false); |
422 |
| - if (!response.IsSuccessStatusCode) |
423 |
| - { |
424 |
| - resourceLogger.LogDebug("Could not initialize RedisInsight settings. Reason: {reason}", await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false)); |
| 261 | + return builder; |
425 | 262 | }
|
426 |
| - |
427 |
| - response.EnsureSuccessStatusCode(); |
428 |
| - } |
429 |
| - |
430 |
| - private static async Task<bool> AreSettingsInitialized(HttpClient client, CancellationToken ct) |
431 |
| - { |
432 |
| - var response = await client.GetAsync("/api/settings", ct).ConfigureAwait(false); |
433 |
| - response.EnsureSuccessStatusCode(); |
434 |
| - |
435 |
| - var content = await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false); |
436 |
| - |
437 |
| - var jsonResponse = JsonNode.Parse(content); |
438 |
| - var agreements = jsonResponse?["agreements"]; |
439 |
| - |
440 |
| - return agreements is not null; |
441 |
| - } |
442 |
| - |
443 |
| - private class RedisDatabaseDto |
444 |
| - { |
445 |
| - [JsonPropertyName("id")] |
446 |
| - public Guid? Id { get; set; } |
447 |
| - |
448 |
| - [JsonPropertyName("name")] |
449 |
| - public string? Name { get; set; } |
450 | 263 | }
|
451 | 264 |
|
452 | 265 | /// <summary>
|
|
0 commit comments