diff --git a/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs b/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs index 35b3a167956..88dd2625e0d 100644 --- a/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs +++ b/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs @@ -2,17 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; -using System.Net.Http.Json; using System.Text; -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; using Aspire.Hosting; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Redis; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Polly; namespace Aspire.Hosting; @@ -230,223 +224,42 @@ public static IResourceBuilder WithRedisInsight(this IResourceBui var resource = new RedisInsightResource(containerName); var resourceBuilder = builder.ApplicationBuilder.AddResource(resource) - .WithImage(RedisContainerImageTags.RedisInsightImage, RedisContainerImageTags.RedisInsightTag) - .WithImageRegistry(RedisContainerImageTags.RedisInsightRegistry) - .WithHttpEndpoint(targetPort: 5540, name: "http") - .ExcludeFromManifest(); - - // We need to wait for all endpoints to be allocated before attempting to import databases - var endpointsAllocatedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - builder.ApplicationBuilder.Eventing.Subscribe((e, ct) => - { - endpointsAllocatedTcs.TrySetResult(); - return Task.CompletedTask; - }); - - builder.ApplicationBuilder.Eventing.Subscribe(resource, async (e, ct) => - { - var redisInstances = builder.ApplicationBuilder.Resources.OfType(); - - if (!redisInstances.Any()) + .WithImage(RedisContainerImageTags.RedisInsightImage, RedisContainerImageTags.RedisInsightTag) + .WithImageRegistry(RedisContainerImageTags.RedisInsightRegistry) + .WithHttpEndpoint(targetPort: 5540, name: "http") + .WithEnvironment(context => { - // No-op if there are no Redis resources present. - return; - } - - // Wait for all endpoints to be allocated before attempting to import databases - await endpointsAllocatedTcs.Task.ConfigureAwait(false); - - var redisInsightResource = builder.ApplicationBuilder.Resources.OfType().Single(); - var insightEndpoint = redisInsightResource.PrimaryEndpoint; - - using var client = new HttpClient(); - client.BaseAddress = new Uri($"{insightEndpoint.Scheme}://{insightEndpoint.Host}:{insightEndpoint.Port}"); + var redisInstances = builder.ApplicationBuilder.Resources.OfType(); - var rls = e.Services.GetRequiredService(); - var resourceLogger = rls.GetLogger(resource); - - await ImportRedisDatabases(resourceLogger, redisInstances, client, ct).ConfigureAwait(false); - }); - - resourceBuilder.WithRelationship(builder.Resource, "RedisInsight"); - - configureContainer?.Invoke(resourceBuilder); - - return builder; - } - - static async Task ImportRedisDatabases(ILogger resourceLogger, IEnumerable redisInstances, HttpClient client, CancellationToken cancellationToken) - { - var databasesPath = "/api/databases"; - - var pipeline = new ResiliencePipelineBuilder().AddRetry(new Polly.Retry.RetryStrategyOptions - { - Delay = TimeSpan.FromSeconds(2), - MaxRetryAttempts = 5, - }).Build(); - - await pipeline.ExecuteAsync(async (ctx) => - { - await InitializeRedisInsightSettings(client, resourceLogger, ctx).ConfigureAwait(false); - }, cancellationToken).ConfigureAwait(false); - - using (var stream = new MemoryStream()) - { - // As part of configuring RedisInsight we need to factor in the possibility that the - // container resource is being run with persistence turned on. In this case we need - // to get the list of existing databases because we might need to delete some. - var lookup = await pipeline.ExecuteAsync(async (ctx) => - { - var getDatabasesResponse = await client.GetFromJsonAsync(databasesPath, cancellationToken).ConfigureAwait(false); - return getDatabasesResponse?.ToLookup( - i => i.Name ?? throw new InvalidDataException("Database name is missing."), - i => i.Id ?? throw new InvalidDataException("Database ID is missing.")); - }, cancellationToken).ConfigureAwait(false); - - var databasesToDelete = new List(); - - using var writer = new Utf8JsonWriter(stream); - - writer.WriteStartArray(); - - foreach (var redisResource in redisInstances) - { - if (lookup is { } && lookup.Contains(redisResource.Name)) + if (!redisInstances.Any()) { - // It is possible that there are multiple databases with - // a conflicting name so we delete them all. This just keeps - // track of the specific ID that we need to delete. - databasesToDelete.AddRange(lookup[redisResource.Name]); + // No-op if there are no Redis resources present. + return; } - if (redisResource.PrimaryEndpoint.IsAllocated) + var counter = 1; + + foreach (var redisInstance in redisInstances) { - var endpoint = redisResource.PrimaryEndpoint; - writer.WriteStartObject(); - - writer.WriteString("host", redisResource.Name); - writer.WriteNumber("port", endpoint.TargetPort!.Value); - writer.WriteString("name", redisResource.Name); - writer.WriteNumber("db", 0); - writer.WriteNull("username"); - if (redisResource.PasswordParameter is { } passwordParam) - { - writer.WriteString("password", passwordParam.Value); - } - else + // RedisInsight assumes Redis is being accessed over a default Aspire container network and hardcodes the resource address + context.EnvironmentVariables.Add($"RI_REDIS_HOST{counter}", redisInstance.Name); + context.EnvironmentVariables.Add($"RI_REDIS_PORT{counter}", redisInstance.PrimaryEndpoint.TargetPort!.Value); + context.EnvironmentVariables.Add($"RI_REDIS_ALIAS{counter}", redisInstance.Name); + if (redisInstance.PasswordParameter is not null) { - writer.WriteNull("password"); + context.EnvironmentVariables.Add($"RI_REDIS_PASSWORD{counter}", redisInstance.PasswordParameter.Value); } - writer.WriteString("connectionType", "STANDALONE"); - writer.WriteEndObject(); - } - } - writer.WriteEndArray(); - await writer.FlushAsync(cancellationToken).ConfigureAwait(false); - stream.Seek(0, SeekOrigin.Begin); - - var content = new MultipartFormDataContent(); - - var fileContent = new StreamContent(stream); - - content.Add(fileContent, "file", "RedisInsight_connections.json"); - - var apiUrl = $"{databasesPath}/import"; - try - { - if (databasesToDelete.Any()) - { - await pipeline.ExecuteAsync(async (ctx) => - { - // Create a DELETE request to send to the existing instance of - // RedisInsight with the IDs of the database to delete. - var deleteContent = JsonContent.Create(new - { - ids = databasesToDelete - }); - - var deleteRequest = new HttpRequestMessage(HttpMethod.Delete, databasesPath) - { - Content = deleteContent - }; - - var deleteResponse = await client.SendAsync(deleteRequest, cancellationToken).ConfigureAwait(false); - deleteResponse.EnsureSuccessStatusCode(); - - }, cancellationToken).ConfigureAwait(false); + counter++; } + }) + .WithRelationship(builder.Resource, "RedisInsight") + .ExcludeFromManifest(); - await pipeline.ExecuteAsync(async (ctx) => - { - var response = await client.PostAsync(apiUrl, content, ctx) - .ConfigureAwait(false); - - response.EnsureSuccessStatusCode(); - }, cancellationToken).ConfigureAwait(false); - - } - catch (Exception ex) - { - resourceLogger.LogError("Could not import Redis databases into RedisInsight. Reason: {reason}", ex.Message); - } - } - } - } - - /// - /// Initializes the Redis Insight settings to work around https://github.com/RedisInsight/RedisInsight/issues/3452. - /// Redis Insight requires the encryption property to be set if the Redis database connection contains a password. - /// - private static async Task InitializeRedisInsightSettings(HttpClient client, ILogger resourceLogger, CancellationToken ct) - { - if (await AreSettingsInitialized(client, ct).ConfigureAwait(false)) - { - return; - } - - var jsonContent = JsonContent.Create(new - { - agreements = new - { - // all 4 are required to be set - eula = false, - analytics = false, - notifications = false, - encryption = false, - } - }); + configureContainer?.Invoke(resourceBuilder); - var response = await client.PatchAsync("/api/settings", jsonContent, ct).ConfigureAwait(false); - if (!response.IsSuccessStatusCode) - { - resourceLogger.LogDebug("Could not initialize RedisInsight settings. Reason: {reason}", await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false)); + return builder; } - - response.EnsureSuccessStatusCode(); - } - - private static async Task AreSettingsInitialized(HttpClient client, CancellationToken ct) - { - var response = await client.GetAsync("/api/settings", ct).ConfigureAwait(false); - response.EnsureSuccessStatusCode(); - - var content = await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false); - - var jsonResponse = JsonNode.Parse(content); - var agreements = jsonResponse?["agreements"]; - - return agreements is not null; - } - - private class RedisDatabaseDto - { - [JsonPropertyName("id")] - public Guid? Id { get; set; } - - [JsonPropertyName("name")] - public string? Name { get; set; } } /// diff --git a/src/Aspire.Hosting.Redis/RedisContainerImageTags.cs b/src/Aspire.Hosting.Redis/RedisContainerImageTags.cs index 0c1e1516c5b..65d8a1a70b5 100644 --- a/src/Aspire.Hosting.Redis/RedisContainerImageTags.cs +++ b/src/Aspire.Hosting.Redis/RedisContainerImageTags.cs @@ -29,6 +29,6 @@ internal static class RedisContainerImageTags /// redis/redisinsight public const string RedisInsightImage = "redis/redisinsight"; - /// 2.66 - public const string RedisInsightTag = "2.66"; + /// 2.68 + public const string RedisInsightTag = "2.68"; } diff --git a/tests/Aspire.Hosting.Redis.Tests/AddRedisTests.cs b/tests/Aspire.Hosting.Redis.Tests/AddRedisTests.cs index 37f4a0a3d85..f33b259cf49 100644 --- a/tests/Aspire.Hosting.Redis.Tests/AddRedisTests.cs +++ b/tests/Aspire.Hosting.Redis.Tests/AddRedisTests.cs @@ -254,6 +254,67 @@ public void WithRedisInsightAddsWithRedisInsightResource() Assert.Single(builder.Resources.OfType()); } + [Fact] + public async Task WithRedisInsightProducesCorrectEnvironmentVariables() + { + var builder = DistributedApplication.CreateBuilder(); + var redis1 = builder.AddRedis("myredis1").WithRedisInsight(); + var redis2 = builder.AddRedis("myredis2").WithRedisInsight(); + using var app = builder.Build(); + + // Add fake allocated endpoints. + redis1.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5001)); + redis2.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5002)); + + await builder.Eventing.PublishAsync(new(app.Services, app.Services.GetRequiredService())); + + var redisInsight = Assert.Single(builder.Resources.OfType()); + var envs = await redisInsight.GetEnvironmentVariableValuesAsync(); + + Assert.Collection(envs, + (item) => + { + Assert.Equal("RI_REDIS_HOST1", item.Key); + Assert.Equal(redis1.Resource.Name, item.Value); + }, + (item) => + { + Assert.Equal("RI_REDIS_PORT1", item.Key); + Assert.Equal($"{redis1.Resource.PrimaryEndpoint.TargetPort!.Value}", item.Value); + }, + (item) => + { + Assert.Equal("RI_REDIS_ALIAS1", item.Key); + Assert.Equal(redis1.Resource.Name, item.Value); + }, + (item) => + { + Assert.Equal("RI_REDIS_PASSWORD1", item.Key); + Assert.Equal(redis1.Resource.PasswordParameter!.Value, item.Value); + }, + (item) => + { + Assert.Equal("RI_REDIS_HOST2", item.Key); + Assert.Equal(redis2.Resource.Name, item.Value); + }, + (item) => + { + Assert.Equal("RI_REDIS_PORT2", item.Key); + Assert.Equal($"{redis2.Resource.PrimaryEndpoint.TargetPort!.Value}", item.Value); + }, + (item) => + { + Assert.Equal("RI_REDIS_ALIAS2", item.Key); + Assert.Equal(redis2.Resource.Name, item.Value); + }, + (item) => + { + Assert.Equal("RI_REDIS_PASSWORD2", item.Key); + Assert.Equal(redis2.Resource.PasswordParameter!.Value, item.Value); + }); + + } + [Fact] public void WithRedisCommanderSupportsChangingContainerImageValues() { @@ -373,7 +434,7 @@ public async Task MultipleRedisInstanceProducesCorrectRedisHostsVariable() redis1.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5001)); redis2.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5002, "host2")); - await builder.Eventing.PublishAsync(new (app.Services, app.Services.GetRequiredService())); + await builder.Eventing.PublishAsync(new(app.Services, app.Services.GetRequiredService())); var commander = builder.Resources.Single(r => r.Name.EndsWith("-commander")); diff --git a/tests/Aspire.Hosting.Redis.Tests/RedisFunctionalTests.cs b/tests/Aspire.Hosting.Redis.Tests/RedisFunctionalTests.cs index 8af9d48a47b..b1e36bc1afd 100644 --- a/tests/Aspire.Hosting.Redis.Tests/RedisFunctionalTests.cs +++ b/tests/Aspire.Hosting.Redis.Tests/RedisFunctionalTests.cs @@ -14,15 +14,18 @@ using Microsoft.Extensions.Hosting; using StackExchange.Redis; using Xunit; -using Aspire.Hosting.Tests.Dcp; using System.Text.Json.Nodes; using Aspire.Hosting; -using Aspire.Components.Common.Tests; +using Polly; namespace Aspire.Hosting.Redis.Tests; public class RedisFunctionalTests(ITestOutputHelper testOutputHelper) { + private const UnixFileMode MountFilePermissions = + UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | + UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute; + [Fact] [RequiresDocker] [QuarantinedTest("https://github.com/dotnet/aspire/issues/7177")] @@ -124,112 +127,6 @@ public async Task VerifyRedisResource() [Fact] [RequiresDocker] - [QuarantinedTest("https://github.com/dotnet/aspire/issues/7291")] - public async Task VerifyDatabasesAreNotDuplicatedForPersistentRedisInsightContainer() - { - var randomResourceSuffix = Random.Shared.Next(10000).ToString(); - var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5)); - - var configure = (DistributedApplicationOptions options) => - { - options.ContainerRegistryOverride = ComponentTestConstants.AspireTestContainerRegistry; - }; - - using var builder1 = TestDistributedApplicationBuilder.Create(configure, testOutputHelper); - builder1.Configuration[$"DcpPublisher:ResourceNameSuffix"] = randomResourceSuffix; - - IResourceBuilder? redisInsightBuilder = null; - var redis1 = builder1.AddRedis("redisForInsightPersistence") - .WithRedisInsight(c => - { - redisInsightBuilder = c; - c.WithLifetime(ContainerLifetime.Persistent); - }); - - // Wire up an additional event subcription to ResourceReadyEvent on the RedisInsightResource - // instance. This works because the ResourceReadyEvent fires non-blocking sequential so the - // wire-up that WithRedisInsight does is guaranteed to execute before this one does. So we then - // use this to block pulling the list of databases until we know they've been updated. This - // will repeated below for the second app. - // - // Issue: https://github.com/dotnet/aspire/issues/6455 - Assert.NotNull(redisInsightBuilder); - var redisInsightsReady = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - builder1.Eventing.Subscribe(redisInsightBuilder.Resource, (evt, ct) => - { - redisInsightsReady.TrySetResult(); - return Task.CompletedTask; - }); - - using var app1 = builder1.Build(); - - await app1.StartAsync(cts.Token); - - await redisInsightsReady.Task.WaitAsync(cts.Token); - - using var client1 = app1.CreateHttpClient($"{redis1.Resource.Name}-insight", "http"); - var firstRunDatabases = await client1.GetFromJsonAsync("/api/databases", cts.Token); - - Assert.NotNull(firstRunDatabases); - Assert.Single(firstRunDatabases); - Assert.Equal($"{redis1.Resource.Name}", firstRunDatabases[0].Name); - - await app1.StopAsync(cts.Token); - - using var builder2 = TestDistributedApplicationBuilder.Create(configure, testOutputHelper); - builder2.Configuration[$"DcpPublisher:ResourceNameSuffix"] = randomResourceSuffix; - - var redis2 = builder2.AddRedis("redisForInsightPersistence") - .WithRedisInsight(c => - { - redisInsightBuilder = c; - c.WithLifetime(ContainerLifetime.Persistent); - }); - - // Wire up an additional event subcription to ResourceReadyEvent on the RedisInsightResource - // instance. This works because the ResourceReadyEvent fires non-blocking sequential so the - // wire-up that WithRedisInsight does is guaranteed to execute before this one does. So we then - // use this to block pulling the list of databases until we know they've been updated. This - // will repeated below for the second app. - Assert.NotNull(redisInsightBuilder); - redisInsightsReady = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - builder2.Eventing.Subscribe(redisInsightBuilder.Resource, (evt, ct) => - { - redisInsightsReady.TrySetResult(); - return Task.CompletedTask; - }); - - using var app2 = builder2.Build(); - await app2.StartAsync(cts.Token); - - await redisInsightsReady.Task.WaitAsync(cts.Token); - - using var client2 = app2.CreateHttpClient($"{redisInsightBuilder.Resource.Name}", "http"); - var secondRunDatabases = await client2.GetFromJsonAsync("/api/databases", cts.Token); - - Assert.NotNull(secondRunDatabases); - Assert.Single(secondRunDatabases); - Assert.Equal($"{redis2.Resource.Name}", secondRunDatabases[0].Name); - Assert.NotEqual(secondRunDatabases.Single().Id, firstRunDatabases.Single().Id); - - // HACK: This is a workaround for the fact that ApplicationExecutor is not a public type. What I have - // done here is I get the latest event from RNS for the insights instance which gives me the resource - // name as known from a DCP perspective. I then use the ApplicationExecutorProxy (introduced with this - // change to call the ApplicationExecutor stop method. The proxy is a public type with an internal - // constructor inside the Aspire.Hosting.Tests package. This is a short term solution for 9.0 to - // make sure that we have good test coverage for WithRedisInsight behavior, but we need a better - // long term solution in 9.x for folks that will want to do things like execute commands against - // resources to stop specific containers. - var latestEvent = await app2.ResourceNotifications.WaitForResourceHealthyAsync(redisInsightBuilder.Resource.Name, cts.Token); - var executorProxy = app2.Services.GetRequiredService(); - await executorProxy.StopResourceAsync(latestEvent.ResourceId, cts.Token); - - await app2.StopAsync(cts.Token); - } - - [Fact] - [RequiresDocker] - [QuarantinedTest("https://github.com/dotnet/aspire/issues/6099")] public async Task VerifyWithRedisInsightImportDatabases() { var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5)); @@ -241,28 +138,25 @@ public async Task VerifyWithRedisInsightImportDatabases() var redis2 = builder.AddRedis("redis-2").WithRedisInsight(c => redisInsightBuilder = c); Assert.NotNull(redisInsightBuilder); - // RedisInsight will import databases when it is ready, this task will run after the initial databases import - // so we will use that to know when the databases have been successfully imported - var redisInsightsReady = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - builder.Eventing.Subscribe(redisInsightBuilder.Resource, (evt, ct) => - { - redisInsightsReady.TrySetResult(); - return Task.CompletedTask; - }); - using var app = builder.Build(); await app.StartAsync(); - await redisInsightsReady.Task.WaitAsync(cts.Token); + var rns = app.Services.GetRequiredService(); + await rns.WaitForResourceAsync(redisInsightBuilder.Resource.Name, KnownResourceStates.Running).WaitAsync(cts.Token); var client = app.CreateHttpClient(redisInsightBuilder.Resource.Name, "http"); + // Accept EULA first; otherwise, /api/databases will not work properly. + await AcceptRedisInsightEula(client, cts.Token); + var response = await client.GetAsync("/api/databases", cts.Token); response.EnsureSuccessStatusCode(); var databases = await response.Content.ReadFromJsonAsync>(cts.Token); + Assert.NotNull(databases); + databases = [.. databases.OrderBy(d => d.Id)]; Assert.NotNull(databases); Assert.Collection(databases, db => @@ -270,16 +164,12 @@ public async Task VerifyWithRedisInsightImportDatabases() Assert.Equal(redis1.Resource.Name, db.Name); Assert.Equal(redis1.Resource.Name, db.Host); Assert.Equal(redis1.Resource.PrimaryEndpoint.TargetPort, db.Port); - Assert.Equal("STANDALONE", db.ConnectionType); - Assert.Equal(0, db.Db); }, db => { Assert.Equal(redis2.Resource.Name, db.Name); Assert.Equal(redis2.Resource.Name, db.Host); Assert.Equal(redis2.Resource.PrimaryEndpoint.TargetPort, db.Port); - Assert.Equal("STANDALONE", db.ConnectionType); - Assert.Equal(0, db.Db); }); foreach (var db in databases) @@ -537,7 +427,6 @@ public async Task PersistenceIsDisabledByDefault() [InlineData(false)] [InlineData(true)] [RequiresDocker] - [QuarantinedTest("https://github.com/dotnet/aspire/issues/7176")] public async Task RedisInsightWithDataShouldPersistStateBetweenUsages(bool useVolume) { var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10)); @@ -564,7 +453,13 @@ public async Task RedisInsightWithDataShouldPersistStateBetweenUsages(bool useVo } else { - bindMountPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + bindMountPath = Directory.CreateTempSubdirectory().FullName; + + if (!OperatingSystem.IsWindows()) + { + // Change permissions for non-root accounts (container user account) + File.SetUnixFileMode(bindMountPath, MountFilePermissions); + } redisInsightBuilder1.WithDataBindMount(bindMountPath); } @@ -573,16 +468,8 @@ public async Task RedisInsightWithDataShouldPersistStateBetweenUsages(bool useVo { await app.StartAsync(); - // RedisInsight will import databases when it is ready, this task will run after the initial databases import - // so we will use that to know when the databases have been successfully imported - var redisInsightsReady = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - builder1.Eventing.Subscribe(redisInsightBuilder1.Resource, (evt, ct) => - { - redisInsightsReady.TrySetResult(); - return Task.CompletedTask; - }); - - await redisInsightsReady.Task.WaitAsync(cts.Token); + var rns = app.Services.GetRequiredService(); + await rns.WaitForResourceAsync(redisInsightBuilder1.Resource.Name, KnownResourceStates.Running).WaitAsync(cts.Token); try { @@ -616,16 +503,8 @@ public async Task RedisInsightWithDataShouldPersistStateBetweenUsages(bool useVo { await app.StartAsync(); - // RedisInsight will import databases when it is ready, this task will run after the initial databases import - // so we will use that to know when the databases have been successfully imported - var redisInsightsReady = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - builder2.Eventing.Subscribe(redisInsightBuilder2.Resource, (evt, ct) => - { - redisInsightsReady.TrySetResult(); - return Task.CompletedTask; - }); - - await redisInsightsReady.Task.WaitAsync(cts.Token); + var rns = app.Services.GetRequiredService(); + await rns.WaitForResourceAsync(redisInsightBuilder2.Resource.Name, KnownResourceStates.Running).WaitAsync(cts.Token); try { @@ -662,20 +541,27 @@ public async Task RedisInsightWithDataShouldPersistStateBetweenUsages(bool useVo private static async Task EnsureRedisInsightEulaAccepted(HttpClient httpClient, CancellationToken ct) { - var response = await httpClient.GetAsync("/api/settings", ct); - response.EnsureSuccessStatusCode(); + var pipeline = new ResiliencePipelineBuilder() + .AddRetry(new() { MaxRetryAttempts = 10, BackoffType = DelayBackoffType.Linear, Delay = TimeSpan.FromSeconds(2) }) + .Build(); + + await pipeline.ExecuteAsync(async ct => + { + var response = await httpClient.GetAsync("/api/settings", ct); + response.EnsureSuccessStatusCode(); - var content = await response.Content.ReadAsStringAsync(ct); + var content = await response.Content.ReadAsStringAsync(ct); - var jo = JsonObject.Parse(content); - Assert.NotNull(jo); - var agreements = jo["agreements"]; + var jo = JsonNode.Parse(content); + Assert.NotNull(jo); + var agreements = jo["agreements"]; - Assert.NotNull(agreements); - Assert.False(agreements["analytics"]!.GetValue()); - Assert.False(agreements["notifications"]!.GetValue()); - Assert.False(agreements["encryption"]!.GetValue()); - Assert.True(agreements["eula"]!.GetValue()); + Assert.NotNull(agreements); + Assert.False(agreements["analytics"]!.GetValue()); + Assert.False(agreements["notifications"]!.GetValue()); + Assert.False(agreements["encryption"]!.GetValue()); + Assert.True(agreements["eula"]!.GetValue()); + }, ct); } static async Task AcceptRedisInsightEula(HttpClient client, CancellationToken ct) @@ -700,16 +586,6 @@ static async Task AcceptRedisInsightEula(HttpClient client, CancellationToken ct await EnsureRedisInsightEulaAccepted(client, ct); } - internal sealed class RedisInsightDatabaseModel - { - public string? Id { get; set; } - public string? Host { get; set; } - public int? Port { get; set; } - public string? Name { get; set; } - public int? Db { get; set; } - public string? ConnectionType { get; set; } - } - [Fact] [RequiresDocker] public async Task WithRedisCommanderShouldWorkWithPassword() @@ -744,4 +620,12 @@ public async Task WithRedisCommanderShouldWorkWithPassword() var httpResponse = await client.GetAsync(redisCommanderUrl!); httpResponse.EnsureSuccessStatusCode(); } + + internal sealed class RedisInsightDatabaseModel + { + public string? Id { get; set; } + public string? Host { get; set; } + public int? Port { get; set; } + public string? Name { get; set; } + } }