From 08dd9928cdf483947fd01d35ea77e8cd5d1294a2 Mon Sep 17 00:00:00 2001 From: Marcin Juraszek Date: Sun, 27 Oct 2024 17:05:28 -0700 Subject: [PATCH 01/44] Update Cosmos Hosting documentation with correct nuget package name (#6488) * Update Cosmos Hosting documentation with correct nuget package name * Revert automatic formatting updates --- src/Aspire.Hosting.Azure.CosmosDB/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Aspire.Hosting.Azure.CosmosDB/README.md b/src/Aspire.Hosting.Azure.CosmosDB/README.md index ff6cb2909b..c88f7afe7f 100644 --- a/src/Aspire.Hosting.Azure.CosmosDB/README.md +++ b/src/Aspire.Hosting.Azure.CosmosDB/README.md @@ -13,7 +13,7 @@ Provides extension methods and resource definitions for a .NET Aspire AppHost to In your AppHost project, install the .NET Aspire Azure Cosmos DB Hosting library with [NuGet](https://www.nuget.org): ```dotnetcli -dotnet add package Aspire.Hosting.Azure.Cosmos +dotnet add package Aspire.Hosting.Azure.CosmosDB ``` ## Configure Azure Provisioning for local development From be2a8955e7b9bc9e3ca0c397045322be40669b39 Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Mon, 28 Oct 2024 03:08:25 -0500 Subject: [PATCH 02/44] Update Azure.Provisioning to latest (#6390) * Update Azure.Provisioning to latest Respond to latest renames getting the API ready for GA release. Fix #6376 * Rename properties to match new type names. * Use IsAccessKeyAuthenticationDisabled property * Fix ContainerAppExtensions for new Azure.Provisioning version * Simplify BicepValueFormattableString * Simplify ReferenceExpression to BicepValue logic * Fix Azure Redis test for new property order * Update playground bicep * Fix issue in AspireV8ResourceNamePropertyResolver when resource name has a dash. In .NET Aspire 8.x, when the Aspire resource name has dashes, we used those dashes in the Azure resource name. But we are incorrectly converting them to underscores. Fix #6474 * Allow customizing Azure ContainerApp resources with ProvisioningBuildOptions. Fix #6496 --------- Co-authored-by: Jose Perez Rodriguez --- Directory.Packages.props | 2 +- .../AzureContainerApps.AppHost/Program.cs | 2 +- .../BicepSample.AppHost/redis.module.bicep | 2 +- playground/cdk/CdkSample.AppHost/Program.cs | 23 ++-- .../cdk/CdkSample.AppHost/cache.module.bicep | 2 +- .../AzureContainerAppsInfrastructure.cs | 128 +++++++----------- .../ContainerAppExtensions.cs | 42 +++--- .../AzureApplicationInsightsExtensions.cs | 9 +- .../AzureOpenAIExtensions.cs | 15 +- .../AzureCosmosDBExtensions.cs | 2 +- .../AzureEventHubsExtensions.cs | 5 +- ...AzureFunctionsProjectResourceExtensions.cs | 2 +- .../AzureKeyVaultResourceExtensions.cs | 9 +- .../AzurePostgresExtensions.cs | 18 +-- .../AzureRedisExtensions.cs | 22 ++- .../AzureServiceBusExtensions.cs | 9 +- .../AzureSqlExtensions.cs | 6 +- .../AzureStorageExtensions.cs | 6 +- .../AzureWebPubSubExtensions.cs | 10 +- .../AspireV8ResourceNamePropertyResolver.cs | 26 +++- .../AzureProvisioningOptions.cs | 4 +- .../AzureProvisioningResource.cs | 10 +- .../AzureProvisioningResourceExtensions.cs | 6 +- .../AzureResourceExtensions.cs | 2 +- .../AzureResourceInfrastructure.cs | 2 +- .../Provisioners/AzureProvisioner.cs | 4 +- .../PublicAPI.Unshipped.txt | 8 +- .../Utils/BicepIdentifierHelpers.cs | 2 +- .../AzureBicepResourceTests.cs | 41 +++--- .../AzureContainerAppsTests.cs | 98 +++++++++++++- .../AzureRedisExtensionsTests.cs | 3 +- .../AzureResourceOptionsTests.cs | 60 +++++++- .../AzureWebPubSubExtensionsTests.cs | 28 ++-- .../Schema/SchemaTests.cs | 4 +- 34 files changed, 350 insertions(+), 262 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 4340de5f40..6e7fb51c15 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,7 +7,7 @@ true true 3.10.0 - 1.0.0-beta.1 + 1.0.0 diff --git a/playground/AzureContainerApps/AzureContainerApps.AppHost/Program.cs b/playground/AzureContainerApps/AzureContainerApps.AppHost/Program.cs index f955d65df0..b8d590e650 100644 --- a/playground/AzureContainerApps/AzureContainerApps.AppHost/Program.cs +++ b/playground/AzureContainerApps/AzureContainerApps.AppHost/Program.cs @@ -42,7 +42,7 @@ app.ConfigureCustomDomain(customDomain, certificateName); // Scale to 0 - app.Template.Value!.Scale.Value!.MinReplicas = 0; + app.Template.Scale.MinReplicas = 0; }); #if !SKIP_DASHBOARD_REFERENCE diff --git a/playground/bicep/BicepSample.AppHost/redis.module.bicep b/playground/bicep/BicepSample.AppHost/redis.module.bicep index 969e501807..e4b434c67a 100644 --- a/playground/bicep/BicepSample.AppHost/redis.module.bicep +++ b/playground/bicep/BicepSample.AppHost/redis.module.bicep @@ -15,11 +15,11 @@ resource redis 'Microsoft.Cache/redis@2024-03-01' = { capacity: 1 } enableNonSslPort: false + disableAccessKeyAuthentication: true minimumTlsVersion: '1.2' redisConfiguration: { 'aad-enabled': 'true' } - disableAccessKeyAuthentication: 'true' } tags: { 'aspire-resource-name': 'redis' diff --git a/playground/cdk/CdkSample.AppHost/Program.cs b/playground/cdk/CdkSample.AppHost/Program.cs index c63e19b819..db6bc49993 100644 --- a/playground/cdk/CdkSample.AppHost/Program.cs +++ b/playground/cdk/CdkSample.AppHost/Program.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Azure.Provisioning.ApplicationInsights; -using Azure.Provisioning.Expressions; using Azure.Provisioning.KeyVault; using Azure.Provisioning.OperationalInsights; using Azure.Provisioning.ServiceBus; @@ -17,7 +16,7 @@ var storage = builder.AddAzureStorage("storage") .ConfigureInfrastructure(infrastructure => { - var account = infrastructure.GetResources().OfType().Single(); + var account = infrastructure.GetProvisionableResources().OfType().Single(); account.Sku = new StorageSku() { Name = sku.AsProvisioningParameter(infrastructure) }; account.Location = locationOverride.AsProvisioningParameter(infrastructure); }); @@ -30,7 +29,7 @@ var keyvault = builder.AddAzureKeyVault("mykv") .ConfigureInfrastructure(infrastructure => { - var keyVault = infrastructure.GetResources().OfType().Single(); + var keyVault = infrastructure.GetProvisionableResources().OfType().Single(); var secret = new KeyVaultSecret("mysecret") { Parent = keyVault, @@ -55,26 +54,22 @@ .AddQueue("queue1") .ConfigureInfrastructure(infrastructure => { - var queue = infrastructure.GetResources().OfType().Single(q => q.IdentifierName == "queue1"); + var queue = infrastructure.GetProvisionableResources().OfType().Single(q => q.BicepIdentifier == "queue1"); queue.MaxDeliveryCount = 5; - queue.LockDuration = new StringLiteral("PT5M"); - // TODO: this should be - // queue.LockDuration = TimeSpan.FromMinutes(5); + queue.LockDuration = TimeSpan.FromMinutes(5); }) .AddTopic("topic1") .ConfigureInfrastructure(infrastructure => { - var topic = infrastructure.GetResources().OfType().Single(q => q.IdentifierName == "topic1"); + var topic = infrastructure.GetProvisionableResources().OfType().Single(q => q.BicepIdentifier == "topic1"); topic.EnablePartitioning = true; }) .AddTopic("topic2") .AddSubscription("topic1", "subscription1") .ConfigureInfrastructure(infrastructure => { - var subscription = infrastructure.GetResources().OfType().Single(q => q.IdentifierName == "subscription1"); - subscription.LockDuration = new StringLiteral("PT5M"); - // TODO: this should be - //subscription.LockDuration = TimeSpan.FromMinutes(5); + var subscription = infrastructure.GetProvisionableResources().OfType().Single(q => q.BicepIdentifier == "subscription1"); + subscription.LockDuration = TimeSpan.FromMinutes(5); subscription.RequiresSession = true; }) .AddSubscription("topic1", "subscription2") @@ -89,7 +84,7 @@ var logAnalyticsWorkspace = builder.AddAzureLogAnalyticsWorkspace("logAnalyticsWorkspace") .ConfigureInfrastructure(infrastructure => { - var logAnalyticsWorkspace = infrastructure.GetResources().OfType().Single(); + var logAnalyticsWorkspace = infrastructure.GetProvisionableResources().OfType().Single(); logAnalyticsWorkspace.Sku = new OperationalInsightsWorkspaceSku() { Name = OperationalInsightsWorkspaceSkuName.PerNode @@ -99,7 +94,7 @@ var appInsights = builder.AddAzureApplicationInsights("appInsights", logAnalyticsWorkspace) .ConfigureInfrastructure(infrastructure => { - var appInsights = infrastructure.GetResources().OfType().Single(); + var appInsights = infrastructure.GetProvisionableResources().OfType().Single(); appInsights.IngestionMode = ComponentIngestionMode.LogAnalytics; }); diff --git a/playground/cdk/CdkSample.AppHost/cache.module.bicep b/playground/cdk/CdkSample.AppHost/cache.module.bicep index 83b2ee2c22..ab540b7b74 100644 --- a/playground/cdk/CdkSample.AppHost/cache.module.bicep +++ b/playground/cdk/CdkSample.AppHost/cache.module.bicep @@ -15,11 +15,11 @@ resource cache 'Microsoft.Cache/redis@2024-03-01' = { capacity: 1 } enableNonSslPort: false + disableAccessKeyAuthentication: true minimumTlsVersion: '1.2' redisConfiguration: { 'aad-enabled': 'true' } - disableAccessKeyAuthentication: 'true' } tags: { 'aspire-resource-name': 'cache' diff --git a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs index 7d4426ada9..243b1d561d 100644 --- a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs +++ b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; -using System.Text; using System.Text.RegularExpressions; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Lifecycle; @@ -12,6 +11,7 @@ using Azure.Provisioning.KeyVault; using Azure.Provisioning.Resources; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; namespace Aspire.Hosting.Azure; @@ -19,7 +19,10 @@ namespace Aspire.Hosting.Azure; /// Represents the infrastructure for Azure Container Apps within the Aspire Hosting environment. /// Implements the interface to provide lifecycle hooks for distributed applications. /// -internal sealed class AzureContainerAppsInfrastructure(ILogger logger, DistributedApplicationExecutionContext executionContext) : IDistributedApplicationLifecycleHook +internal sealed class AzureContainerAppsInfrastructure( + ILogger logger, + IOptions provisioningOptions, + DistributedApplicationExecutionContext executionContext) : IDistributedApplicationLifecycleHook { public async Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default) { @@ -49,7 +52,7 @@ public async Task BeforeStartAsync(DistributedApplicationModel appModel, Cancell continue; } - var containerApp = await containerAppEnvironmentContext.CreateContainerAppAsync(r, executionContext, cancellationToken).ConfigureAwait(false); + var containerApp = await containerAppEnvironmentContext.CreateContainerAppAsync(r, provisioningOptions.Value, executionContext, cancellationToken).ConfigureAwait(false); r.Annotations.Add(new DeploymentTargetAnnotation(containerApp)); } @@ -75,11 +78,12 @@ IManifestExpressionProvider clientId private readonly Dictionary _containerApps = []; - public async Task CreateContainerAppAsync(IResource resource, DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken) + public async Task CreateContainerAppAsync(IResource resource, AzureProvisioningOptions provisioningOptions, DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken) { var context = await ProcessResourceAsync(resource, executionContext, cancellationToken).ConfigureAwait(false); var provisioningResource = new AzureProvisioningResource(resource.Name, context.BuildContainerApp); + provisioningResource.ProvisioningBuildOptions = provisioningOptions.ProvisioningBuildOptions; provisioningResource.Annotations.Add(new ManifestPublishingCallbackAnnotation(provisioningResource.WriteToManifest)); @@ -143,7 +147,7 @@ public void BuildContainerApp(AzureResourceInfrastructure c) containerImageParam = AllocateContainerImageParameter(); } - var containerAppResource = new ContainerApp(Infrastructure.NormalizeIdentifierName(resource.Name)) + var containerAppResource = new ContainerApp(Infrastructure.NormalizeBicepIdentifier(resource.Name)) { Name = resource.Name.ToLowerInvariant() }; @@ -180,7 +184,7 @@ public void BuildContainerApp(AzureResourceInfrastructure c) var containerAppContainer = new ContainerAppContainer(); template.Containers = [containerAppContainer]; - containerAppContainer.Image = containerImageParam is null ? containerImageName : containerImageParam; + containerAppContainer.Image = containerImageParam is null ? containerImageName! : containerImageParam; containerAppContainer.Name = resource.Name; AddEnvironmentVariablesAndCommandLineArgs(containerAppContainer); @@ -497,7 +501,8 @@ private async Task ProcessEnvironmentAsync(DistributedApplicationExecutionContex { var managedIdentityParameter = AllocateManagedIdentityIdParameter(); secret.Identity = managedIdentityParameter; - secret.KeyVaultUri = new BicepValue(argValue.Expression!); + // TODO: this should be able to use ToUri(), but it hit an issue + secret.KeyVaultUri = new BicepValue(((BicepExpression?)argValue)!); } else { @@ -531,7 +536,6 @@ private static BicepValue ResolveValue(object val) { BicepValue s => s, string s => s, - BicepValueFormattableString fs => Interpolate(fs), ProvisioningParameter p => p, _ => throw new NotSupportedException("Unsupported value type " + val.GetType()) }; @@ -698,7 +702,7 @@ BicepValue GetHostValue(string? prefix = null, string? suffix = null) args[index++] = val; } - return (new BicepValueFormattableString(expr.Format, args), finalSecretType); + return (Interpolate(expr.Format, args), finalSecretType); } @@ -714,7 +718,7 @@ private BicepValue AllocateKeyVaultSecretUriReference(BicepSecretOutputR { // We resolve the keyvault that represents the storage for secret outputs var parameter = AllocateParameter(SecretOutputExpression.GetSecretOutputKeyVault(secretOutputReference.Resource)); - kv = KeyVaultService.FromExisting($"{parameter.IdentifierName}_kv"); + kv = KeyVaultService.FromExisting($"{parameter.BicepIdentifier}_kv"); kv.Name = parameter; KeyVaultRefs[secretOutputReference.Resource.Name] = kv; @@ -723,19 +727,15 @@ private BicepValue AllocateKeyVaultSecretUriReference(BicepSecretOutputR if (!KeyVaultSecretRefs.TryGetValue(secretOutputReference.ValueExpression, out var secret)) { // Now we resolve the secret - var secretIdentifierName = Infrastructure.NormalizeIdentifierName($"{kv.IdentifierName}_{secretOutputReference.Name}"); - secret = KeyVaultSecret.FromExisting(secretIdentifierName); + var secretBicepIdentifier = Infrastructure.NormalizeBicepIdentifier($"{kv.BicepIdentifier}_{secretOutputReference.Name}"); + secret = KeyVaultSecret.FromExisting(secretBicepIdentifier); secret.Name = secretOutputReference.Name; secret.Parent = kv; KeyVaultSecretRefs[secretOutputReference.ValueExpression] = secret; } - // TODO: There should be a better way to do this? - return new MemberExpression( - new MemberExpression( - new IdentifierExpression(secret.IdentifierName), "properties"), - "secretUri"); + return secret.Properties.SecretUri; } private ProvisioningParameter AllocateContainerImageParameter() @@ -895,81 +895,45 @@ private void AddContainerRegistryParameters(ContainerAppConfiguration app) } } - // REVIEW: BicepFunction.Interpolate is buggy and doesn't handle nested formattable strings correctly - // This is a workaround to handle nested formattable strings until the bug is fixed. - private static BicepValue Interpolate(BicepValueFormattableString text) + private static BicepValue Interpolate(string format, object[] args) { - var formatStringBuilder = new StringBuilder(); - var arguments = new List>(); + var bicepStringBuilder = new BicepStringBuilder(); - void ProcessFormattableString(BicepValueFormattableString formattableString, int argumentIndex) - { - var span = formattableString.Format.AsSpan(); - var skip = 0; - - foreach (var match in Regex.EnumerateMatches(span, @"{\d+}")) - { - formatStringBuilder.Append(span[..(match.Index - skip)]); + var span = format.AsSpan(); + var skip = 0; + var argumentIndex = 0; - var argument = formattableString.GetArgument(argumentIndex); + foreach (var match in Regex.EnumerateMatches(span, @"{\d+}")) + { + bicepStringBuilder.Append(span[..(match.Index - skip)].ToString()); - if (argument is BicepValueFormattableString nested) - { - // Inline the nested formattable string - ProcessFormattableString(nested, 0); - } - else - { - formatStringBuilder.Append(CultureInfo.InvariantCulture, $"{{{arguments.Count}}}"); - if (argument is BicepValue bicepValue) - { - arguments.Add(bicepValue); - } - else if (argument is string s) - { - arguments.Add(s); - } - else if (argument is ProvisioningParameter provisioningParameter) - { - arguments.Add(provisioningParameter); - } - else - { - throw new NotSupportedException($"{argument} is not supported"); - } - } + var argument = args[argumentIndex]; - argumentIndex++; - span = span[(match.Index + match.Length - skip)..]; - skip = match.Index + match.Length; + if (argument is BicepValue bicepValue) + { + bicepStringBuilder.Append($"{bicepValue}"); + } + else if (argument is string s) + { + bicepStringBuilder.Append(s); + } + else if (argument is ProvisioningParameter provisioningParameter) + { + bicepStringBuilder.Append($"{provisioningParameter}"); + } + else + { + throw new NotSupportedException($"{argument} is not supported"); } - formatStringBuilder.Append(span); - } - - ProcessFormattableString(text, 0); - - var formatString = formatStringBuilder.ToString(); - - if (formatString == "{0}") - { - return arguments[0]; + argumentIndex++; + span = span[(match.Index + match.Length - skip)..]; + skip = match.Index + match.Length; } - return BicepFunction.Interpolate(new BicepValueFormattableString(formatString, [.. arguments])); - } + bicepStringBuilder.Append(span.ToString()); - /// - /// A custom FormattableString implementation that allows us to inline nested formattable strings. - /// - private sealed class BicepValueFormattableString(string formatString, object[] values) : FormattableString - { - public override int ArgumentCount => values.Length; - public override string Format => formatString; - public override object? GetArgument(int index) => values[index]; - public override object?[] GetArguments() => values; - public override string ToString(IFormatProvider? formatProvider) => Format; - public override string ToString() => formatString; + return bicepStringBuilder.Build(); } /// diff --git a/src/Aspire.Hosting.Azure.AppContainers/ContainerAppExtensions.cs b/src/Aspire.Hosting.Azure.AppContainers/ContainerAppExtensions.cs index f1f004d121..bce9c2ba7c 100644 --- a/src/Aspire.Hosting.Azure.AppContainers/ContainerAppExtensions.cs +++ b/src/Aspire.Hosting.Azure.AppContainers/ContainerAppExtensions.cs @@ -1,12 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Azure; +using Azure.Provisioning; using Azure.Provisioning.AppContainers; using Azure.Provisioning.Expressions; -using Azure.Provisioning; -using System.Diagnostics.CodeAnalysis; -using Aspire.Hosting.Azure; namespace Aspire.Hosting; @@ -62,40 +62,40 @@ public static void ConfigureCustomDomain(this ContainerApp app, IResourceBuilder throw new ArgumentException("Cannot configure custom domain when resource is not parented by ResourceModuleConstruct.", nameof(app)); } - var containerAppManagedEnvironmentIdParameter = module.GetResources().OfType().Single( - p => p.IdentifierName == "outputs_azure_container_apps_environment_id"); - var certificatNameParameter = certificateName.AsProvisioningParameter(module); + var containerAppManagedEnvironmentIdParameter = module.GetProvisionableResources().OfType().Single( + p => p.BicepIdentifier == "outputs_azure_container_apps_environment_id"); + var certificateNameParameter = certificateName.AsProvisioningParameter(module); var customDomainParameter = customDomain.AsProvisioningParameter(module); var bindingTypeConditional = new ConditionalExpression( new BinaryExpression( - new IdentifierExpression(certificatNameParameter.IdentifierName), - BinaryOperator.NotEqual, - new StringLiteral(string.Empty)), - new StringLiteral("SniEnabled"), - new StringLiteral("Disabled") + new IdentifierExpression(certificateNameParameter.BicepIdentifier), + BinaryBicepOperator.NotEqual, + new StringLiteralExpression(string.Empty)), + new StringLiteralExpression("SniEnabled"), + new StringLiteralExpression("Disabled") ); var certificateOrEmpty = new ConditionalExpression( new BinaryExpression( - new IdentifierExpression(certificatNameParameter.IdentifierName), - BinaryOperator.NotEqual, - new StringLiteral(string.Empty)), - new InterpolatedString( - "{0}/managedCertificates/{1}", + new IdentifierExpression(certificateNameParameter.BicepIdentifier), + BinaryBicepOperator.NotEqual, + new StringLiteralExpression(string.Empty)), + new InterpolatedStringExpression( [ - new IdentifierExpression(containerAppManagedEnvironmentIdParameter.IdentifierName), - new IdentifierExpression(certificatNameParameter.IdentifierName) + new IdentifierExpression(containerAppManagedEnvironmentIdParameter.BicepIdentifier), + new StringLiteralExpression("/managedCertificates/"), + new IdentifierExpression(certificateNameParameter.BicepIdentifier) ]), - new NullLiteral() + new NullLiteralExpression() ); - app.Configuration.Value!.Ingress!.Value!.CustomDomains = new BicepList() + app.Configuration.Ingress.CustomDomains = new BicepList() { new ContainerAppCustomDomain() { BindingType = bindingTypeConditional, - Name = new IdentifierExpression(customDomainParameter.IdentifierName), + Name = customDomainParameter, CertificateId = certificateOrEmpty } }; diff --git a/src/Aspire.Hosting.Azure.ApplicationInsights/AzureApplicationInsightsExtensions.cs b/src/Aspire.Hosting.Azure.ApplicationInsights/AzureApplicationInsightsExtensions.cs index 04c06574ef..06d7d191be 100644 --- a/src/Aspire.Hosting.Azure.ApplicationInsights/AzureApplicationInsightsExtensions.cs +++ b/src/Aspire.Hosting.Azure.ApplicationInsights/AzureApplicationInsightsExtensions.cs @@ -5,7 +5,6 @@ using Aspire.Hosting.Azure; using Azure.Provisioning; using Azure.Provisioning.ApplicationInsights; -using Azure.Provisioning.Expressions; using Azure.Provisioning.OperationalInsights; namespace Aspire.Hosting; @@ -41,13 +40,13 @@ public static IResourceBuilder AddAzureApplica { var appTypeParameter = new ProvisioningParameter("applicationType", typeof(string)) { - Value = new StringLiteral("web") + Value = "web" }; infrastructure.Add(appTypeParameter); var kindParameter = new ProvisioningParameter("kind", typeof(string)) { - Value = new StringLiteral("web") + Value = "web" }; infrastructure.Add(kindParameter); @@ -67,7 +66,7 @@ public static IResourceBuilder AddAzureApplica else if (builder.ExecutionContext.IsRunMode) { // ... otherwise if we are in run mode, the provisioner expects us to create one ourselves. - var autoInjectedLogAnalyticsWorkspaceName = $"law_{appInsights.IdentifierName}"; + var autoInjectedLogAnalyticsWorkspaceName = $"law_{appInsights.BicepIdentifier}"; var autoInjectedLogAnalyticsWorkspace = new OperationalInsightsWorkspace(autoInjectedLogAnalyticsWorkspaceName) { Sku = new OperationalInsightsWorkspaceSku() @@ -89,7 +88,7 @@ public static IResourceBuilder AddAzureApplica infrastructure.AspireResource.Parameters.TryAdd(AzureBicepResource.KnownParameters.LogAnalyticsWorkspaceId, null); var logAnalyticsWorkspaceParameter = new ProvisioningParameter(AzureBicepResource.KnownParameters.LogAnalyticsWorkspaceId, typeof(string)) { - Value = new StringLiteral("web") + Value = "web" }; infrastructure.Add(kindParameter); appInsights.WorkspaceResourceId = logAnalyticsWorkspaceParameter; diff --git a/src/Aspire.Hosting.Azure.CognitiveServices/AzureOpenAIExtensions.cs b/src/Aspire.Hosting.Azure.CognitiveServices/AzureOpenAIExtensions.cs index e9e3b85b69..27b98ffda8 100644 --- a/src/Aspire.Hosting.Azure.CognitiveServices/AzureOpenAIExtensions.cs +++ b/src/Aspire.Hosting.Azure.CognitiveServices/AzureOpenAIExtensions.cs @@ -5,7 +5,6 @@ using Aspire.Hosting.Azure; using Azure.Provisioning; using Azure.Provisioning.CognitiveServices; -using Azure.Provisioning.Expressions; using static Azure.Provisioning.Expressions.BicepFunction; namespace Aspire.Hosting; @@ -47,17 +46,7 @@ public static IResourceBuilder AddAzureOpenAI(this IDistrib infrastructure.Add(new ProvisioningOutput("connectionString", typeof(string)) { - Value = new InterpolatedString( - "Endpoint={0}", - [ - new MemberExpression( - new MemberExpression( - new IdentifierExpression(cogServicesAccount.IdentifierName), - "properties"), - "endpoint") - ]) - // TODO This should be - // Value = BicepFunction.Interpolate($"Endpoint={cogServicesAccount.Endpoint}") + Value = Interpolate($"Endpoint={cogServicesAccount.Properties.Endpoint}") }); var principalTypeParameter = new ProvisioningParameter(AzureBicepResource.KnownParameters.PrincipalType, typeof(string)); @@ -71,7 +60,7 @@ public static IResourceBuilder AddAzureOpenAI(this IDistrib var cdkDeployments = new List(); foreach (var deployment in resource.Deployments) { - var cdkDeployment = new CognitiveServicesAccountDeployment(Infrastructure.NormalizeIdentifierName(deployment.Name)) + var cdkDeployment = new CognitiveServicesAccountDeployment(Infrastructure.NormalizeBicepIdentifier(deployment.Name)) { Name = deployment.Name, Parent = cogServicesAccount, diff --git a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs index 2f75161e4f..6fbb1067b3 100644 --- a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs +++ b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs @@ -65,7 +65,7 @@ public static IResourceBuilder AddAzureCosmosDB(this IDis List cosmosSqlDatabases = new List(); foreach (var databaseName in azureResource.Databases) { - var cosmosSqlDatabase = new CosmosDBSqlDatabase(Infrastructure.NormalizeIdentifierName(databaseName)) + var cosmosSqlDatabase = new CosmosDBSqlDatabase(Infrastructure.NormalizeBicepIdentifier(databaseName)) { Parent = cosmosAccount, Name = databaseName, diff --git a/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs b/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs index 2074fcb0d1..8cb4934eb1 100644 --- a/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs +++ b/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs @@ -9,7 +9,6 @@ using Azure.Messaging.EventHubs.Producer; using Azure.Provisioning; using Azure.Provisioning.EventHubs; -using Azure.Provisioning.Expressions; using Microsoft.Extensions.DependencyInjection; namespace Aspire.Hosting; @@ -34,7 +33,7 @@ public static IResourceBuilder AddAzureEventHubs( { var skuParameter = new ProvisioningParameter("sku", typeof(string)) { - Value = new StringLiteral("Standard") + Value = "Standard" }; infrastructure.Add(skuParameter); @@ -58,7 +57,7 @@ public static IResourceBuilder AddAzureEventHubs( foreach (var hub in azureResource.Hubs) { - var hubResource = new EventHub(Infrastructure.NormalizeIdentifierName(hub)) + var hubResource = new EventHub(Infrastructure.NormalizeBicepIdentifier(hub)) { Parent = eventHubsNamespace, Name = hub diff --git a/src/Aspire.Hosting.Azure.Functions/AzureFunctionsProjectResourceExtensions.cs b/src/Aspire.Hosting.Azure.Functions/AzureFunctionsProjectResourceExtensions.cs index 7030369bc9..7d07dd878a 100644 --- a/src/Aspire.Hosting.Azure.Functions/AzureFunctionsProjectResourceExtensions.cs +++ b/src/Aspire.Hosting.Azure.Functions/AzureFunctionsProjectResourceExtensions.cs @@ -54,7 +54,7 @@ public static class AzureFunctionsProjectResourceExtensions var principalTypeParameter = new ProvisioningParameter(AzureBicepResource.KnownParameters.PrincipalType, typeof(string)); var principalIdParameter = new ProvisioningParameter(AzureBicepResource.KnownParameters.PrincipalId, typeof(string)); - var storageAccount = infrastructure.GetResources().OfType().FirstOrDefault(r => r.IdentifierName == storageResourceName) + var storageAccount = infrastructure.GetProvisionableResources().OfType().FirstOrDefault(r => r.BicepIdentifier == storageResourceName) ?? throw new InvalidOperationException($"Could not find storage account with '{storageResourceName}' name."); infrastructure.Add(storageAccount.CreateRoleAssignment(StorageBuiltInRole.StorageAccountContributor, principalTypeParameter, principalIdParameter)); }; diff --git a/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResourceExtensions.cs b/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResourceExtensions.cs index 36346cd269..ae3f5eb949 100644 --- a/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResourceExtensions.cs +++ b/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResourceExtensions.cs @@ -43,14 +43,7 @@ public static IResourceBuilder AddAzureKeyVault(this IDis infrastructure.Add(new ProvisioningOutput("vaultUri", typeof(string)) { - Value = - new MemberExpression( - new MemberExpression( - new IdentifierExpression(keyVault.IdentifierName), - "properties"), - "vaultUri") - // TODO: this should be - //Value = keyVault.VaultUri + Value = keyVault.Properties.VaultUri }); keyVault.Tags["aspire-resource-name"] = infrastructure.AspireResource.Name; diff --git a/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresExtensions.cs b/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresExtensions.cs index 961cc36b41..f8d4b4f996 100644 --- a/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresExtensions.cs +++ b/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresExtensions.cs @@ -157,7 +157,7 @@ public static IResourceBuilder AddAzurePost var principalTypeParameter = new ProvisioningParameter(AzureBicepResource.KnownParameters.PrincipalType, typeof(string)); var principalNameParameter = new ProvisioningParameter(AzureBicepResource.KnownParameters.PrincipalName, typeof(string)); - var admin = new PostgreSqlFlexibleServerActiveDirectoryAdministrator($"{postgres.IdentifierName}_admin") + var admin = new PostgreSqlFlexibleServerActiveDirectoryAdministrator($"{postgres.BicepIdentifier}_admin") { Parent = postgres, Name = principalIdParameter, @@ -167,7 +167,7 @@ public static IResourceBuilder AddAzurePost // This is a workaround for a bug in the API that requires the parent to be fully resolved admin.DependsOn.Add(postgres); - foreach (var firewall in infrastructure.GetResources().OfType()) + foreach (var firewall in infrastructure.GetProvisionableResources().OfType()) { admin.DependsOn.Add(firewall); } @@ -355,7 +355,7 @@ public static IResourceBuilder WithPassword RemoveActiveDirectoryAuthResources(infrastructure); - var postgres = infrastructure.GetResources().OfType().FirstOrDefault(r => r.IdentifierName == azureResource.GetBicepIdentifier()) + var postgres = infrastructure.GetProvisionableResources().OfType().FirstOrDefault(r => r.BicepIdentifier == azureResource.GetBicepIdentifier()) ?? throw new InvalidOperationException($"Could not find a PostgreSqlFlexibleServer with name {azureResource.Name}."); var administratorLogin = new ProvisioningParameter("administratorLogin", typeof(string)); @@ -393,7 +393,7 @@ public static IResourceBuilder WithPassword foreach (var database in azureResource.Databases) { - var dbSecret = new KeyVaultSecret(Infrastructure.NormalizeIdentifierName(database.Key + "_connectionString")) + var dbSecret = new KeyVaultSecret(Infrastructure.NormalizeBicepIdentifier(database.Key + "_connectionString")) { Parent = keyVault, Name = AzurePostgresFlexibleServerResource.GetDatabaseKeyVaultSecretName(database.Key), @@ -417,7 +417,7 @@ private static PostgreSqlFlexibleServer CreatePostgreSqlFlexibleServer(AzureReso Name = "Standard_B1ms", Tier = PostgreSqlFlexibleServerSkuTier.Burstable }, - Version = new StringLiteral("16"), + Version = new StringLiteralExpression("16"), HighAvailability = new PostgreSqlFlexibleServerHighAvailability() { Mode = PostgreSqlFlexibleServerHighAvailabilityMode.Disabled @@ -455,9 +455,9 @@ private static PostgreSqlFlexibleServer CreatePostgreSqlFlexibleServer(AzureReso foreach (var databaseNames in databases) { - var identifierName = Infrastructure.NormalizeIdentifierName(databaseNames.Key); + var bicepIdentifier = Infrastructure.NormalizeBicepIdentifier(databaseNames.Key); var databaseName = databaseNames.Value; - var pgsqlDatabase = new PostgreSqlFlexibleServerDatabase(identifierName) + var pgsqlDatabase = new PostgreSqlFlexibleServerDatabase(bicepIdentifier) { Parent = postgres, Name = databaseName @@ -480,13 +480,13 @@ private static IResourceBuilder RemoveActiv private static void RemoveActiveDirectoryAuthResources(AzureResourceInfrastructure infrastructure) { var resourcesToRemove = new List(); - foreach (var resource in infrastructure.GetResources()) + foreach (var resource in infrastructure.GetProvisionableResources()) { if (resource is PostgreSqlFlexibleServerActiveDirectoryAdministrator) { resourcesToRemove.Add(resource); } - else if (resource is ProvisioningOutput output && output.IdentifierName == "connectionString") + else if (resource is ProvisioningOutput output && output.BicepIdentifier == "connectionString") { resourcesToRemove.Add(resource); } diff --git a/src/Aspire.Hosting.Azure.Redis/AzureRedisExtensions.cs b/src/Aspire.Hosting.Azure.Redis/AzureRedisExtensions.cs index 97f76c422e..3b2a0c20d8 100644 --- a/src/Aspire.Hosting.Azure.Redis/AzureRedisExtensions.cs +++ b/src/Aspire.Hosting.Azure.Redis/AzureRedisExtensions.cs @@ -129,14 +129,11 @@ public static IResourceBuilder AddAzureRedis( { IsAadEnabled = "true" }; - - // TODO: This property should be available from the CDK in the latest version. - var disableAccessKeys = BicepValue.DefineProperty(redis, "DisableAccessKeyAuthentication", ["properties", "disableAccessKeyAuthentication"], isOutput: false, isRequired: false); - disableAccessKeys.Assign("true"); + redis.IsAccessKeyAuthenticationDisabled = true; var principalIdParameter = new ProvisioningParameter(AzureBicepResource.KnownParameters.PrincipalId, typeof(string)); var principalNameParameter = new ProvisioningParameter(AzureBicepResource.KnownParameters.PrincipalName, typeof(string)); - infrastructure.Add(new RedisCacheAccessPolicyAssignment($"{redis.IdentifierName}_contributor") + infrastructure.Add(new RedisCacheAccessPolicyAssignment($"{redis.BicepIdentifier}_contributor") { Parent = redis, AccessPolicyName = "Data Contributor", @@ -233,7 +230,7 @@ public static IResourceBuilder WithAccessKeyAuthenticat { RemoveActiveDirectoryAuthResources(infrastructure); - var redis = infrastructure.GetResources().OfType().FirstOrDefault(r => r.IdentifierName == builder.Resource.GetBicepIdentifier()) + var redis = infrastructure.GetProvisionableResources().OfType().FirstOrDefault(r => r.BicepIdentifier == builder.Resource.GetBicepIdentifier()) ?? throw new InvalidOperationException($"Could not find a RedisResource with name {builder.Resource.Name}."); var kvNameParam = new ProvisioningParameter("keyVaultName", typeof(string)); @@ -243,11 +240,8 @@ public static IResourceBuilder WithAccessKeyAuthenticat keyVault.Name = kvNameParam; infrastructure.Add(keyVault); - redis.RedisConfiguration.Value!.IsAadEnabled.Kind = BicepValueKind.Unset; - - // TODO: This property should be available from the CDK in the latest version. - var disableAccessKeys = BicepValue.DefineProperty(redis, "DisableAccessKeyAuthentication", ["properties", "disableAccessKeyAuthentication"], isOutput: false, isRequired: false); - disableAccessKeys.Kind = BicepValueKind.Unset; + redis.RedisConfiguration.IsAadEnabled.ClearValue(); + redis.IsAccessKeyAuthenticationDisabled.ClearValue(); var secret = new KeyVaultSecret("connectionString") { @@ -292,14 +286,14 @@ private static IResourceBuilder RemoveActiveDirectoryPa private static void RemoveActiveDirectoryAuthResources(AzureResourceInfrastructure infrastructure) { var resourcesToRemove = new List(); - foreach (var resource in infrastructure.GetResources()) + foreach (var resource in infrastructure.GetProvisionableResources()) { if (resource is RedisCacheAccessPolicyAssignment accessPolicy && - accessPolicy.IdentifierName == $"{infrastructure.AspireResource.GetBicepIdentifier()}_contributor") + accessPolicy.BicepIdentifier == $"{infrastructure.AspireResource.GetBicepIdentifier()}_contributor") { resourcesToRemove.Add(resource); } - else if (resource is ProvisioningOutput output && output.IdentifierName == "connectionString") + else if (resource is ProvisioningOutput output && output.BicepIdentifier == "connectionString") { resourcesToRemove.Add(resource); } diff --git a/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs b/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs index ec7c4b7720..92c19b6575 100644 --- a/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs +++ b/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs @@ -4,7 +4,6 @@ using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Azure; using Azure.Provisioning; -using Azure.Provisioning.Expressions; using Azure.Provisioning.ServiceBus; namespace Aspire.Hosting; @@ -28,7 +27,7 @@ public static IResourceBuilder AddAzureServiceBus(this { var skuParameter = new ProvisioningParameter("sku", typeof(string)) { - Value = new StringLiteral("Standard") + Value = "Standard" }; infrastructure.Add(skuParameter); @@ -53,7 +52,7 @@ public static IResourceBuilder AddAzureServiceBus(this foreach (var queue in azureResource.Queues) { - var queueResource = new ServiceBusQueue(Infrastructure.NormalizeIdentifierName(queue)) + var queueResource = new ServiceBusQueue(Infrastructure.NormalizeBicepIdentifier(queue)) { Parent = serviceBusNamespace, Name = queue @@ -63,7 +62,7 @@ public static IResourceBuilder AddAzureServiceBus(this var topicDictionary = new Dictionary(); foreach (var topic in azureResource.Topics) { - var topicResource = new ServiceBusTopic(Infrastructure.NormalizeIdentifierName(topic)) + var topicResource = new ServiceBusTopic(Infrastructure.NormalizeBicepIdentifier(topic)) { Parent = serviceBusNamespace, Name = topic @@ -74,7 +73,7 @@ public static IResourceBuilder AddAzureServiceBus(this foreach (var subscription in azureResource.Subscriptions) { var topic = topicDictionary[subscription.TopicName]; - var subscriptionResource = new ServiceBusSubscription(Infrastructure.NormalizeIdentifierName(subscription.Name)) + var subscriptionResource = new ServiceBusSubscription(Infrastructure.NormalizeBicepIdentifier(subscription.Name)) { Parent = topic, Name = subscription.Name diff --git a/src/Aspire.Hosting.Azure.Sql/AzureSqlExtensions.cs b/src/Aspire.Hosting.Azure.Sql/AzureSqlExtensions.cs index 5ee8df25f9..99a2880ec9 100644 --- a/src/Aspire.Hosting.Azure.Sql/AzureSqlExtensions.cs +++ b/src/Aspire.Hosting.Azure.Sql/AzureSqlExtensions.cs @@ -243,7 +243,7 @@ private static void CreateSqlServer( // When in run mode we inject the users identity and we need to specify // the principalType. var principalTypeParameter = new ProvisioningParameter(AzureBicepResource.KnownParameters.PrincipalType, typeof(string)); - sqlServer.Administrators.Value!.PrincipalType = principalTypeParameter; + sqlServer.Administrators.PrincipalType = principalTypeParameter; infrastructure.Add(new SqlFirewallRule("sqlFirewallRule_AllowAllIps") { @@ -256,9 +256,9 @@ private static void CreateSqlServer( foreach (var databaseNames in databases) { - var identifierName = Infrastructure.NormalizeIdentifierName(databaseNames.Key); + var bicepIdentifier = Infrastructure.NormalizeBicepIdentifier(databaseNames.Key); var databaseName = databaseNames.Value; - var sqlDatabase = new SqlDatabase(identifierName) + var sqlDatabase = new SqlDatabase(bicepIdentifier) { Parent = sqlServer, Name = databaseName diff --git a/src/Aspire.Hosting.Azure.Storage/AzureStorageExtensions.cs b/src/Aspire.Hosting.Azure.Storage/AzureStorageExtensions.cs index 347822fb0e..d34f2f466b 100644 --- a/src/Aspire.Hosting.Azure.Storage/AzureStorageExtensions.cs +++ b/src/Aspire.Hosting.Azure.Storage/AzureStorageExtensions.cs @@ -65,9 +65,9 @@ public static IResourceBuilder AddAzureStorage(this IDistr infrastructure.Add(storageAccount.CreateRoleAssignment(StorageBuiltInRole.StorageTableDataContributor, principalTypeParameter, principalIdParameter)); infrastructure.Add(storageAccount.CreateRoleAssignment(StorageBuiltInRole.StorageQueueDataContributor, principalTypeParameter, principalIdParameter)); - infrastructure.Add(new ProvisioningOutput("blobEndpoint", typeof(string)) { Value = storageAccount.PrimaryEndpoints.Value!.BlobUri }); - infrastructure.Add(new ProvisioningOutput("queueEndpoint", typeof(string)) { Value = storageAccount.PrimaryEndpoints.Value!.QueueUri }); - infrastructure.Add(new ProvisioningOutput("tableEndpoint", typeof(string)) { Value = storageAccount.PrimaryEndpoints.Value!.TableUri }); + infrastructure.Add(new ProvisioningOutput("blobEndpoint", typeof(string)) { Value = storageAccount.PrimaryEndpoints.BlobUri }); + infrastructure.Add(new ProvisioningOutput("queueEndpoint", typeof(string)) { Value = storageAccount.PrimaryEndpoints.QueueUri }); + infrastructure.Add(new ProvisioningOutput("tableEndpoint", typeof(string)) { Value = storageAccount.PrimaryEndpoints.TableUri }); }; var resource = new AzureStorageResource(name, configureInfrastructure); diff --git a/src/Aspire.Hosting.Azure.WebPubSub/AzureWebPubSubExtensions.cs b/src/Aspire.Hosting.Azure.WebPubSub/AzureWebPubSubExtensions.cs index fc2e5a7ccd..a4237f5705 100644 --- a/src/Aspire.Hosting.Azure.WebPubSub/AzureWebPubSubExtensions.cs +++ b/src/Aspire.Hosting.Azure.WebPubSub/AzureWebPubSubExtensions.cs @@ -31,7 +31,7 @@ public static IResourceBuilder AddAzureWebPubSub(this ID // Supported values are Free_F1 Standard_S1 Premium_P1 var skuParameter = new ProvisioningParameter("sku", typeof(string)) { - Value = new StringLiteral("Free_F1") + Value = "Free_F1" }; infrastructure.Add(skuParameter); @@ -66,14 +66,14 @@ public static IResourceBuilder AddAzureWebPubSub(this ID var hubBuilder = setting.Value; var hubResource = hubBuilder; - var hub = new WebPubSubHub(Infrastructure.NormalizeIdentifierName(hubResource.Name)) + var hub = new WebPubSubHub(Infrastructure.NormalizeBicepIdentifier(hubResource.Name)) { Name = setting.Key, Parent = service, Properties = new WebPubSubHubProperties() }; - var hubProperties = hub.Properties.Value!; + var hubProperties = hub.Properties; // invoke the configure from AddEventHandler for (var i = 0; i < hubResource.EventHandlers.Count; i++) @@ -92,7 +92,7 @@ public static IResourceBuilder AddAzureWebPubSub(this ID // otherwise add parameter to the construct var parameter = new ProvisioningParameter($"{hubName}_url_{i}", typeof(string)); infrastructure.Add(parameter); - resource.Parameters[parameter.IdentifierName] = urlExpression; + resource.Parameters[parameter.BicepIdentifier] = urlExpression; urlParameter = parameter; } @@ -183,7 +183,7 @@ private static WebPubSubEventHandler GetWebPubSubEventHandler(BicepValue if (authSettings != null) { - handler.Auth = new BicepValue(authSettings); + handler.Auth = authSettings; } return handler; } diff --git a/src/Aspire.Hosting.Azure/AspireV8ResourceNamePropertyResolver.cs b/src/Aspire.Hosting.Azure/AspireV8ResourceNamePropertyResolver.cs index a2c7f776dc..05488e39af 100644 --- a/src/Aspire.Hosting.Azure/AspireV8ResourceNamePropertyResolver.cs +++ b/src/Aspire.Hosting.Azure/AspireV8ResourceNamePropertyResolver.cs @@ -16,9 +16,29 @@ namespace Aspire.Hosting.Azure; public sealed class AspireV8ResourceNamePropertyResolver : DynamicResourceNamePropertyResolver { /// - public override BicepValue? ResolveName(ProvisioningContext context, Resource resource, ResourceNameRequirements requirements) + public override BicepValue? ResolveName(ProvisioningBuildOptions options, ProvisionableResource resource, ResourceNameRequirements requirements) { - var suffix = GetUniqueSuffix(context, resource); - return BicepFunction.ToLower(BicepFunction.Take(BicepFunction.Interpolate($"{resource.IdentifierName}{suffix}"), 24)); + var suffix = GetUniqueSuffix(options, resource); + var prefix = GetNamePrefix(resource); + + return BicepFunction.ToLower(BicepFunction.Take(BicepFunction.Interpolate($"{prefix}{suffix}"), 24)); + } + + /// + /// Use the 'aspire-resource-name' tag to get the prefix for the resource name, if available. + /// + /// + /// The BicepIdentifier has already had any dashes changed to underscores, which we don't want to use since .NET Aspire 8.x used the dashes. + /// + private static string GetNamePrefix(ProvisionableResource resource) + { + BicepValue? aspireResourceName = null; + if (resource.ProvisionableProperties.TryGetValue("Tags", out var tags) && + tags is BicepDictionary tagDictionary) + { + tagDictionary.TryGetValue("aspire-resource-name", out aspireResourceName); + } + + return aspireResourceName?.Value ?? resource.BicepIdentifier; } } diff --git a/src/Aspire.Hosting.Azure/AzureProvisioningOptions.cs b/src/Aspire.Hosting.Azure/AzureProvisioningOptions.cs index 8084b6e4ed..ea778c7bf1 100644 --- a/src/Aspire.Hosting.Azure/AzureProvisioningOptions.cs +++ b/src/Aspire.Hosting.Azure/AzureProvisioningOptions.cs @@ -15,8 +15,8 @@ namespace Aspire.Hosting.Azure; public sealed class AzureProvisioningOptions { /// - /// Gets the which contains common settings and + /// Gets the which contains common settings and /// functionality for building Azure resources. /// - public ProvisioningContext ProvisioningContext { get; } = new ProvisioningContext(); + public ProvisioningBuildOptions ProvisioningBuildOptions { get; } = new ProvisioningBuildOptions(); } diff --git a/src/Aspire.Hosting.Azure/AzureProvisioningResource.cs b/src/Aspire.Hosting.Azure/AzureProvisioningResource.cs index 8122c30a54..c5c18228be 100644 --- a/src/Aspire.Hosting.Azure/AzureProvisioningResource.cs +++ b/src/Aspire.Hosting.Azure/AzureProvisioningResource.cs @@ -20,10 +20,10 @@ public class AzureProvisioningResource(string name, Action ConfigureInfrastructure { get; internal set; } = configureInfrastructure; /// - /// Gets or sets the which contains common settings and + /// Gets or sets the which contains common settings and /// functionality for building Azure resources. /// - public ProvisioningContext? ProvisioningContext { get; set; } + public ProvisioningBuildOptions? ProvisioningBuildOptions { get; set; } /// public override BicepTemplateFile GetBicepTemplateFile(string? directory = null, bool deleteTemporaryFileOnDispose = true) @@ -38,8 +38,8 @@ public override BicepTemplateFile GetBicepTemplateFile(string? directory = null, // put them into a dictionary for quick lookup so we don't need to scan // through the parameter enumerable each time. var infrastructureParameters = infrastructure.GetParameters(); - var distinctInfrastructureParameters = infrastructureParameters.DistinctBy(p => p.IdentifierName); - var distinctInfrastructureParametersLookup = distinctInfrastructureParameters.ToDictionary(p => p.IdentifierName); + var distinctInfrastructureParameters = infrastructureParameters.DistinctBy(p => p.BicepIdentifier); + var distinctInfrastructureParametersLookup = distinctInfrastructureParameters.ToDictionary(p => p.BicepIdentifier); foreach (var aspireParameter in this.Parameters) { @@ -56,7 +56,7 @@ public override BicepTemplateFile GetBicepTemplateFile(string? directory = null, var generationPath = Directory.CreateTempSubdirectory("aspire").FullName; var moduleSourcePath = Path.Combine(generationPath, "main.bicep"); - var plan = infrastructure.Build(ProvisioningContext); + var plan = infrastructure.Build(ProvisioningBuildOptions); var compilation = plan.Compile(); Debug.Assert(compilation.Count == 1); var compiledBicep = compilation.First(); diff --git a/src/Aspire.Hosting.Azure/AzureProvisioningResourceExtensions.cs b/src/Aspire.Hosting.Azure/AzureProvisioningResourceExtensions.cs index d6c0472d5b..33b8d27f00 100644 --- a/src/Aspire.Hosting.Azure/AzureProvisioningResourceExtensions.cs +++ b/src/Aspire.Hosting.Azure/AzureProvisioningResourceExtensions.cs @@ -69,11 +69,11 @@ public static ProvisioningParameter AsProvisioningParameter(this IResourceBuilde ArgumentNullException.ThrowIfNull(parameterResourceBuilder); ArgumentNullException.ThrowIfNull(infrastructure); - parameterName ??= Infrastructure.NormalizeIdentifierName(parameterResourceBuilder.Resource.Name); + parameterName ??= Infrastructure.NormalizeBicepIdentifier(parameterResourceBuilder.Resource.Name); infrastructure.AspireResource.Parameters[parameterName] = parameterResourceBuilder.Resource; - var parameter = infrastructure.GetParameters().FirstOrDefault(p => p.IdentifierName == parameterName); + var parameter = infrastructure.GetParameters().FirstOrDefault(p => p.BicepIdentifier == parameterName); if (parameter is null) { parameter = new ProvisioningParameter(parameterName, typeof(string)) @@ -112,7 +112,7 @@ public static ProvisioningParameter AsProvisioningParameter(this BicepOutputRefe infrastructure.AspireResource.Parameters[parameterName] = outputReference; - var parameter = infrastructure.GetParameters().FirstOrDefault(p => p.IdentifierName == parameterName); + var parameter = infrastructure.GetParameters().FirstOrDefault(p => p.BicepIdentifier == parameterName); if (parameter is null) { parameter = new ProvisioningParameter(parameterName, typeof(string)); diff --git a/src/Aspire.Hosting.Azure/AzureResourceExtensions.cs b/src/Aspire.Hosting.Azure/AzureResourceExtensions.cs index 1d8b2fc2c5..ac9a3b73b9 100644 --- a/src/Aspire.Hosting.Azure/AzureResourceExtensions.cs +++ b/src/Aspire.Hosting.Azure/AzureResourceExtensions.cs @@ -30,5 +30,5 @@ public static IResourceBuilder PublishAsConnectionString(this IResourceBui /// The Azure resource. /// A valid Bicep identifier. public static string GetBicepIdentifier(this IAzureResource resource) => - Infrastructure.NormalizeIdentifierName(resource.Name); + Infrastructure.NormalizeBicepIdentifier(resource.Name); } diff --git a/src/Aspire.Hosting.Azure/AzureResourceInfrastructure.cs b/src/Aspire.Hosting.Azure/AzureResourceInfrastructure.cs index bb8e480848..5396171d0c 100644 --- a/src/Aspire.Hosting.Azure/AzureResourceInfrastructure.cs +++ b/src/Aspire.Hosting.Azure/AzureResourceInfrastructure.cs @@ -30,5 +30,5 @@ internal AzureResourceInfrastructure(AzureProvisioningResource resource, string /// public AzureProvisioningResource AspireResource { get; } - internal IEnumerable GetParameters() => GetResources().OfType(); + internal IEnumerable GetParameters() => GetProvisionableResources().OfType(); } diff --git a/src/Aspire.Hosting.Azure/Provisioning/Provisioners/AzureProvisioner.cs b/src/Aspire.Hosting.Azure/Provisioning/Provisioners/AzureProvisioner.cs index 8e1aaf1c36..ee690f8cee 100644 --- a/src/Aspire.Hosting.Azure/Provisioning/Provisioners/AzureProvisioner.cs +++ b/src/Aspire.Hosting.Azure/Provisioning/Provisioners/AzureProvisioner.cs @@ -83,12 +83,12 @@ public async Task BeforeStartAsync(DistributedApplicationModel appModel, Cancell return; } - // set the ProvisioningContext on the resource, if necessary + // set the ProvisioningBuildOptions on the resource, if necessary foreach (var r in azureResources) { if (r.AzureResource is AzureProvisioningResource provisioningResource) { - provisioningResource.ProvisioningContext = provisioningOptions.Value.ProvisioningContext; + provisioningResource.ProvisioningBuildOptions = provisioningOptions.Value.ProvisioningBuildOptions; } } diff --git a/src/Aspire.Hosting.Azure/PublicAPI.Unshipped.txt b/src/Aspire.Hosting.Azure/PublicAPI.Unshipped.txt index 005fda52ce..830233db18 100644 --- a/src/Aspire.Hosting.Azure/PublicAPI.Unshipped.txt +++ b/src/Aspire.Hosting.Azure/PublicAPI.Unshipped.txt @@ -17,18 +17,18 @@ Aspire.Hosting.Azure.AspireV8ResourceNamePropertyResolver Aspire.Hosting.Azure.AspireV8ResourceNamePropertyResolver.AspireV8ResourceNamePropertyResolver() -> void Aspire.Hosting.Azure.AzureProvisioningOptions Aspire.Hosting.Azure.AzureProvisioningOptions.AzureProvisioningOptions() -> void -Aspire.Hosting.Azure.AzureProvisioningOptions.ProvisioningContext.get -> Azure.Provisioning.ProvisioningContext! +Aspire.Hosting.Azure.AzureProvisioningOptions.ProvisioningBuildOptions.get -> Azure.Provisioning.ProvisioningBuildOptions! Aspire.Hosting.Azure.AzureProvisioningResource.AzureProvisioningResource(string! name, System.Action! configureInfrastructure) -> void Aspire.Hosting.Azure.AzureProvisioningResource.ConfigureInfrastructure.get -> System.Action! Aspire.Hosting.Azure.AzureProvisioningResource -Aspire.Hosting.Azure.AzureProvisioningResource.ProvisioningContext.get -> Azure.Provisioning.ProvisioningContext? -Aspire.Hosting.Azure.AzureProvisioningResource.ProvisioningContext.set -> void +Aspire.Hosting.Azure.AzureProvisioningResource.ProvisioningBuildOptions.get -> Azure.Provisioning.ProvisioningBuildOptions? +Aspire.Hosting.Azure.AzureProvisioningResource.ProvisioningBuildOptions.set -> void Aspire.Hosting.Azure.AzureResourceInfrastructure Aspire.Hosting.Azure.AzureResourceInfrastructure.AspireResource.get -> Aspire.Hosting.Azure.AzureProvisioningResource! Aspire.Hosting.Azure.IResourceWithAzureFunctionsConfig Aspire.Hosting.Azure.IResourceWithAzureFunctionsConfig.ApplyAzureFunctionsConfiguration(System.Collections.Generic.IDictionary! target, string! connectionName) -> void Aspire.Hosting.AzureProvisioningResourceExtensions -override Aspire.Hosting.Azure.AspireV8ResourceNamePropertyResolver.ResolveName(Azure.Provisioning.ProvisioningContext! context, Azure.Provisioning.Primitives.Resource! resource, Azure.Provisioning.Primitives.ResourceNameRequirements requirements) -> Azure.Provisioning.BicepValue? +override Aspire.Hosting.Azure.AspireV8ResourceNamePropertyResolver.ResolveName(Azure.Provisioning.ProvisioningBuildOptions! options, Azure.Provisioning.Primitives.ProvisionableResource! resource, Azure.Provisioning.Primitives.ResourceNameRequirements requirements) -> Azure.Provisioning.BicepValue? override Aspire.Hosting.Azure.AzureProvisioningResource.GetBicepTemplateFile(string? directory = null, bool deleteTemporaryFileOnDispose = true) -> Aspire.Hosting.Azure.BicepTemplateFile override Aspire.Hosting.Azure.AzureProvisioningResource.GetBicepTemplateString() -> string! static Aspire.Hosting.AzureBicepResourceExtensions.WithParameter(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! name, Aspire.Hosting.ApplicationModel.EndpointReference! value) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! diff --git a/src/Aspire.Hosting.Azure/Utils/BicepIdentifierHelpers.cs b/src/Aspire.Hosting.Azure/Utils/BicepIdentifierHelpers.cs index af38d6a69b..20c83ee8b1 100644 --- a/src/Aspire.Hosting.Azure/Utils/BicepIdentifierHelpers.cs +++ b/src/Aspire.Hosting.Azure/Utils/BicepIdentifierHelpers.cs @@ -10,7 +10,7 @@ internal static class BicepIdentifierHelpers { internal static string ThrowIfInvalid(string name, [CallerArgumentExpression(nameof(name))] string? paramName = null) { - Infrastructure.ValidateIdentifierName(name, paramName); + Infrastructure.ValidateBicepIdentifier(name, paramName); return name; } } diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureBicepResourceTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureBicepResourceTests.cs index 27a42ae5fa..fef6e8b8a8 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureBicepResourceTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureBicepResourceTests.cs @@ -246,7 +246,7 @@ public async Task AddAzureCosmosDBViaRunMode() var cosmos = builder.AddAzureCosmosDB("cosmos") .ConfigureInfrastructure(infrastructure => { - callbackDatabases = infrastructure.GetResources().OfType(); + callbackDatabases = infrastructure.GetProvisionableResources().OfType(); }); cosmos.AddDatabase("mydatabase"); @@ -340,7 +340,7 @@ public async Task AddAzureCosmosDBViaPublishMode() var cosmos = builder.AddAzureCosmosDB("cosmos") .ConfigureInfrastructure(infrastructure => { - callbackDatabases = infrastructure.GetResources().OfType(); + callbackDatabases = infrastructure.GetProvisionableResources().OfType(); }); cosmos.AddDatabase("mydatabase"); @@ -774,8 +774,8 @@ public async Task AssignParameterPopulatesParametersEverywhere() var manifest = await ManifestUtils.GetManifest(infrastructure1.Resource); Assert.NotNull(moduleInfrastructure); - var infrastructureParameters = moduleInfrastructure.GetParameters().DistinctBy(x => x.IdentifierName); - var infrastructureParametersLookup = infrastructureParameters.ToDictionary(p => p.IdentifierName); + var infrastructureParameters = moduleInfrastructure.GetParameters().DistinctBy(x => x.BicepIdentifier); + var infrastructureParametersLookup = infrastructureParameters.ToDictionary(p => p.BicepIdentifier); Assert.True(infrastructureParametersLookup.ContainsKey("skuName")); var expectedManifest = """ @@ -813,8 +813,8 @@ public async Task AssignParameterWithSpecifiedNamePopulatesParametersEverywhere( var manifest = await ManifestUtils.GetManifest(infrastructure1.Resource); Assert.NotNull(moduleInfrastructure); - var infrastructureParameters = moduleInfrastructure.GetParameters().DistinctBy(x => x.IdentifierName); - var infrastructureParametersLookup = infrastructureParameters.ToDictionary(p => p.IdentifierName); + var infrastructureParameters = moduleInfrastructure.GetParameters().DistinctBy(x => x.BicepIdentifier); + var infrastructureParametersLookup = infrastructureParameters.ToDictionary(p => p.BicepIdentifier); Assert.True(infrastructureParametersLookup.ContainsKey("sku")); var expectedManifest = """ @@ -1879,7 +1879,7 @@ public async Task AddAzureStorageViaRunMode() var storage = builder.AddAzureStorage("storage") .ConfigureInfrastructure(infrastructure => { - var sa = infrastructure.GetResources().OfType().Single(); + var sa = infrastructure.GetProvisionableResources().OfType().Single(); sa.Sku = new StorageSku() { Name = storagesku.AsProvisioningParameter(infrastructure) @@ -2037,7 +2037,7 @@ public async Task AddAzureStorageViaRunModeAllowSharedKeyAccessOverridesDefaultF var storage = builder.AddAzureStorage("storage") .ConfigureInfrastructure(infrastructure => { - var sa = infrastructure.GetResources().OfType().Single(); + var sa = infrastructure.GetProvisionableResources().OfType().Single(); sa.Sku = new StorageSku() { Name = storagesku.AsProvisioningParameter(infrastructure) @@ -2196,7 +2196,7 @@ public async Task AddAzureStorageViaPublishMode() var storage = builder.AddAzureStorage("storage") .ConfigureInfrastructure(infrastructure => { - var sa = infrastructure.GetResources().OfType().Single(); + var sa = infrastructure.GetProvisionableResources().OfType().Single(); sa.Sku = new StorageSku() { Name = storagesku.AsProvisioningParameter(infrastructure) @@ -2354,7 +2354,7 @@ public async Task AddAzureStorageViaPublishModeEnableAllowSharedKeyAccessOverrid var storage = builder.AddAzureStorage("storage") .ConfigureInfrastructure(infrastructure => { - var sa = infrastructure.GetResources().OfType().Single(); + var sa = infrastructure.GetProvisionableResources().OfType().Single(); sa.Sku = new StorageSku() { Name = storagesku.AsProvisioningParameter(infrastructure) @@ -2514,7 +2514,7 @@ public async Task AddAzureSearch() var search = builder.AddAzureSearch("search") .ConfigureInfrastructure(infrastructure => { - var search = infrastructure.GetResources().OfType().Single(); + var search = infrastructure.GetProvisionableResources().OfType().Single(); search.SearchSkuName = sku.AsProvisioningParameter(infrastructure); }); @@ -2636,12 +2636,12 @@ public async Task AddAzureOpenAI(bool overrideLocalAuthDefault) var openai = builder.AddAzureOpenAI("openai") .ConfigureInfrastructure(infrastructure => { - aiDeployments = infrastructure.GetResources().OfType(); + aiDeployments = infrastructure.GetProvisionableResources().OfType(); if (overrideLocalAuthDefault) { - var account = infrastructure.GetResources().OfType().Single(); - account.Properties.Value!.DisableLocalAuth = false; + var account = infrastructure.GetProvisionableResources().OfType().Single(); + account.Properties.DisableLocalAuth = false; } }) .AddDeployment(new("mymodel", "gpt-35-turbo", "0613", "Basic", 4)) @@ -2781,24 +2781,17 @@ public async Task InfrastructureCanBeMutatedAfterCreation() }) .ConfigureInfrastructure(r => { - var vault = r.GetResources().OfType().Single(); + var vault = r.GetProvisionableResources().OfType().Single(); Assert.NotNull(vault); r.Add(new ProvisioningOutput("vaultUri", typeof(string)) { - Value = - new MemberExpression( - new MemberExpression( - new IdentifierExpression(vault.IdentifierName), - "properties"), - "vaultUri") - // TODO: this should be - //Value = keyVault.VaultUri + Value = vault.Properties.VaultUri }); }) .ConfigureInfrastructure(r => { - var vault = r.GetResources().OfType().Single(); + var vault = r.GetProvisionableResources().OfType().Single(); Assert.NotNull(vault); r.Add(new KeyVaultSecret("secret") diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureContainerAppsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureContainerAppsTests.cs index e149a94011..62d651204b 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureContainerAppsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureContainerAppsTests.cs @@ -6,7 +6,9 @@ using System.Runtime.CompilerServices; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Utils; +using Azure.Provisioning; using Azure.Provisioning.AppContainers; +using Azure.Provisioning.Primitives; using Microsoft.Extensions.DependencyInjection; using Xunit; using Xunit.Abstractions; @@ -365,8 +367,8 @@ public async Task AddContainerAppsInfrastructureWithParameterReference() Value = value.AsProvisioningParameter(module) }; - c.Template.Value!.Containers[0].Value!.Env.Add(val); - c.Template.Value!.Scale.Value!.MinReplicas = minReplicas.AsProvisioningParameter(module); + c.Template.Containers[0].Value!.Env.Add(val); + c.Template.Scale.MinReplicas = minReplicas.AsProvisioningParameter(module); }); using var app = builder.Build(); @@ -759,9 +761,9 @@ public async Task PublishAsContainerAppInfluencesContainerAppDefinition() builder.AddContainer("api", "myimage") .PublishAsAzureContainerApp((module, c) => { - Assert.Contains(c, module.GetResources()); + Assert.Contains(c, module.GetProvisionableResources()); - c.Template.Value!.Scale.Value!.MinReplicas = 0; + c.Template.Scale.MinReplicas = 0; }); using var app = builder.Build(); @@ -1273,6 +1275,94 @@ param outputs_azure_container_apps_environment_id string Assert.Equal(expectedBicep, bicep); } + [Fact] + public async Task CanCustomizeWithProvisioningBuildOptions() + { + var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + builder.Services.Configure(options => options.ProvisioningBuildOptions.InfrastructureResolvers.Insert(0, new MyResourceNamePropertyResolver())); + builder.AddAzureContainerAppsInfrastructure(); + + builder.AddContainer("api1", "myimage"); + + using var app = builder.Build(); + + await ExecuteBeforeStartHooksAsync(app, default); + + var model = app.Services.GetRequiredService(); + + var container = Assert.Single(model.GetContainerResources()); + + container.TryGetLastAnnotation(out var target); + + var resource = target?.DeploymentTarget as AzureProvisioningResource; + + Assert.NotNull(resource); + + var (_, bicep) = await ManifestUtils.GetManifestWithBicep(resource); + + var expectedBicep = + """ + @description('The location for the resource(s) to be deployed.') + param location string = resourceGroup().location + + param outputs_azure_container_registry_managed_identity_id string + + param outputs_managed_identity_client_id string + + param outputs_azure_container_apps_environment_id string + + resource api1 'Microsoft.App/containerApps@2024-03-01' = { + name: 'api1-my' + location: location + properties: { + configuration: { + activeRevisionsMode: 'Single' + } + environmentId: outputs_azure_container_apps_environment_id + template: { + containers: [ + { + image: 'myimage:latest' + name: 'api1' + env: [ + { + name: 'AZURE_CLIENT_ID' + value: outputs_managed_identity_client_id + } + ] + } + ] + scale: { + minReplicas: 1 + } + } + } + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${outputs_azure_container_registry_managed_identity_id}': { } + } + } + } + """; + output.WriteLine(bicep); + Assert.Equal(expectedBicep, bicep); + } + + private sealed class MyResourceNamePropertyResolver : DynamicResourceNamePropertyResolver + { + public override void ResolveProperties(ProvisionableConstruct construct, ProvisioningBuildOptions options) + { + if (construct is ContainerApp app) + { + app.Name = app.Name.Value + "-my"; + } + + base.ResolveProperties(construct, options); + } + } + [Fact] public async Task ExternalEndpointBecomesIngress() { diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureRedisExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureRedisExtensionsTests.cs index e195bcc160..9ec3483820 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureRedisExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureRedisExtensionsTests.cs @@ -50,11 +50,11 @@ param principalName string capacity: 1 } enableNonSslPort: false + disableAccessKeyAuthentication: true minimumTlsVersion: '1.2' redisConfiguration: { 'aad-enabled': 'true' } - disableAccessKeyAuthentication: 'true' } tags: { 'aspire-resource-name': 'redis-cache' @@ -116,7 +116,6 @@ param keyVaultName string } enableNonSslPort: false minimumTlsVersion: '1.2' - redisConfiguration: { } } tags: { 'aspire-resource-name': 'redis-cache' diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureResourceOptionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureResourceOptionsTests.cs index e5ec91abf7..f532f93081 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureResourceOptionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureResourceOptionsTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Utils; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -11,7 +12,7 @@ namespace Aspire.Hosting.Azure.Tests; public class AzureResourceOptionsTests(ITestOutputHelper output) { /// - /// Ensures that an AzureProvisioningOptions can be configured to modify the ProvisioningContext + /// Ensures that an AzureProvisioningOptions can be configured to modify the ProvisioningBuildOptions /// used when building the bicep for an Azure resource. /// /// This uses the .NET Aspire v8.x naming policy, which always calls toLower, appends a unique string with no separator, @@ -27,11 +28,16 @@ public async Task AzureResourceOptionsCanBeConfigured() { builder.Services.Configure(options => { - options.ProvisioningContext.PropertyResolvers.Insert(0, new AspireV8ResourceNamePropertyResolver()); + options.ProvisioningBuildOptions.InfrastructureResolvers.Insert(0, new AspireV8ResourceNamePropertyResolver()); }); var serviceBus = builder.AddAzureServiceBus("sb"); + // ensure that resources with a hyphen still have a hyphen in the bicep name + var sqlDatabase = builder.AddAzureSqlServer("sql-server") + .RunAsContainer(x => x.WithLifetime(ContainerLifetime.Persistent)) + .AddDatabase("evadexdb"); + using var app = builder.Build(); await app.StartAsync(); @@ -76,6 +82,56 @@ param principalType string output.WriteLine(actualBicep); Assert.Equal(expectedBicep, actualBicep); + actualBicep = await File.ReadAllTextAsync(Path.Combine(tempDir.FullName, "sql-server.module.bicep")); + + expectedBicep = """ + @description('The location for the resource(s) to be deployed.') + param location string = resourceGroup().location + + param principalId string + + param principalName string + + resource sql_server 'Microsoft.Sql/servers@2021-11-01' = { + name: toLower(take('sql-server${uniqueString(resourceGroup().id)}', 24)) + location: location + properties: { + administrators: { + administratorType: 'ActiveDirectory' + login: principalName + sid: principalId + tenantId: subscription().tenantId + azureADOnlyAuthentication: true + } + minimalTlsVersion: '1.2' + publicNetworkAccess: 'Enabled' + version: '12.0' + } + tags: { + 'aspire-resource-name': 'sql-server' + } + } + + resource sqlFirewallRule_AllowAllAzureIps 'Microsoft.Sql/servers/firewallRules@2021-11-01' = { + name: 'AllowAllAzureIps' + properties: { + endIpAddress: '0.0.0.0' + startIpAddress: '0.0.0.0' + } + parent: sql_server + } + + resource evadexdb 'Microsoft.Sql/servers/databases@2021-11-01' = { + name: 'evadexdb' + location: location + parent: sql_server + } + + output sqlServerFqdn string = sql_server.properties.fullyQualifiedDomainName + """; + output.WriteLine(actualBicep); + Assert.Equal(expectedBicep, actualBicep); + await app.StopAsync(); } diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureWebPubSubExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureWebPubSubExtensionsTests.cs index 2b43b5c002..d975977834 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureWebPubSubExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureWebPubSubExtensionsTests.cs @@ -29,7 +29,7 @@ public async Task AddWebPubSubHubNameWithSpecialChars() WebPubSubHub? realHub = null; var wps = builder.AddAzureWebPubSub("wps1").ConfigureInfrastructure(infrastructure => { - realHub = infrastructure.GetResources().OfType().Single(); + realHub = infrastructure.GetProvisionableResources().OfType().Single(); }); var hubName = "a-b-c"; var hub = wps.AddHub(hubName); @@ -38,7 +38,7 @@ public async Task AddWebPubSubHubNameWithSpecialChars() var manifest = await ManifestUtils.GetManifestWithBicep(wps.Resource); Assert.NotNull(realHub); Assert.Equal(hubName, realHub.Name.Value); - Assert.Equal("a_b_c", realHub.IdentifierName); + Assert.Equal("a_b_c", realHub.BicepIdentifier); } [Fact] @@ -74,9 +74,9 @@ public async Task AddAzureWebPubSubHubWorks() param sku string = 'Free_F1' param capacity int = 1 - + param principalId string - + param principalType string resource wps1 'Microsoft.SignalRService/webPubSub@2024-03-01' = { @@ -103,7 +103,6 @@ param principalType string resource abc 'Microsoft.SignalRService/webPubSub/hubs@2024-03-01' = { name: 'abc' - properties: { } parent: wps1 } @@ -119,8 +118,8 @@ public async Task AddWebPubSubWithHubConfigure() var hubName = "abc"; var wps = builder.AddAzureWebPubSub("wps1").ConfigureInfrastructure(infrastructure => { - var hub = infrastructure.GetResources().OfType().First(i => i.IdentifierName == hubName); - hub.Properties.Value!.AnonymousConnectPolicy = "allow"; + var hub = infrastructure.GetProvisionableResources().OfType().First(i => i.BicepIdentifier == hubName); + hub.Properties.AnonymousConnectPolicy = "allow"; }); wps.AddHub(hubName); @@ -150,9 +149,9 @@ public async Task AddWebPubSubWithHubConfigure() param sku string = 'Free_F1' param capacity int = 1 - + param principalId string - + param principalType string resource wps1 'Microsoft.SignalRService/webPubSub@2024-03-01' = { @@ -280,8 +279,8 @@ public async Task ConfigureConstructOverridesAddEventHandler() var serviceA = builder.AddProject("serviceA", o => o.ExcludeLaunchProfile = true).WithHttpsEndpoint(); var wps = builder.AddAzureWebPubSub("wps1").ConfigureInfrastructure(infrastructure => { - var hub = infrastructure.GetResources().OfType().First(i => string.Equals(i.IdentifierName, "abc", StringComparison.OrdinalIgnoreCase)); - hub.Properties.Value!.EventHandlers.Add(new WebPubSubEventHandler() { UrlTemplate = "http://fake.com" }); + var hub = infrastructure.GetProvisionableResources().OfType().First(i => string.Equals(i.BicepIdentifier, "abc", StringComparison.OrdinalIgnoreCase)); + hub.Properties.EventHandlers.Add(new WebPubSubEventHandler() { UrlTemplate = "http://fake.com" }); }); wps.AddHub("ABC").AddEventHandler($"http://fake1.com"); // Hub name is case insensitive @@ -358,10 +357,10 @@ public async Task AddAzureWebPubSubHubSettings() var url1 = "fake3.com"; var wps = builder.AddAzureWebPubSub("wps1").ConfigureInfrastructure(infrastructure => { - var hub = infrastructure.GetResources().OfType().First(i => i.IdentifierName == "hub1"); - hub.Properties.Value!.AnonymousConnectPolicy = "allow"; + var hub = infrastructure.GetProvisionableResources().OfType().First(i => i.BicepIdentifier == "hub1"); + hub.Properties.AnonymousConnectPolicy = "allow"; // allow directly event handler set - hub.Properties.Value!.EventHandlers.Add(new WebPubSubEventHandler() { UrlTemplate = "http://fake1.com" }); + hub.Properties.EventHandlers.Add(new WebPubSubEventHandler() { UrlTemplate = "http://fake1.com" }); }); // allow event handler set using a separate call // allow mulitple calls, and order matters @@ -448,7 +447,6 @@ param principalType string { urlTemplate: 'http://fake2.com' userEventPattern: 'event1' - auth: { } } { urlTemplate: 'http://fake3.com' diff --git a/tests/Aspire.Hosting.Tests/Schema/SchemaTests.cs b/tests/Aspire.Hosting.Tests/Schema/SchemaTests.cs index 12ed03647b..01d3d7fb29 100644 --- a/tests/Aspire.Hosting.Tests/Schema/SchemaTests.cs +++ b/tests/Aspire.Hosting.Tests/Schema/SchemaTests.cs @@ -146,7 +146,7 @@ public static TheoryData> Applica builder.AddProject("project") .PublishAsAzureContainerApp((infrastructure, app) => { - app.Template.Value!.Scale.Value!.MinReplicas = minReplicas.AsProvisioningParameter(infrastructure); + app.Template.Scale.MinReplicas = minReplicas.AsProvisioningParameter(infrastructure); }); } @@ -167,7 +167,7 @@ public static TheoryData> Applica builder.AddContainer("mycontainer", "myimage") .PublishAsAzureContainerApp((infrastructure, app) => { - app.Template.Value!.Scale.Value!.MinReplicas = minReplicas.AsProvisioningParameter(infrastructure); + app.Template.Scale.MinReplicas = minReplicas.AsProvisioningParameter(infrastructure); }); } From af83cc5d9309efdbf6e64f3be51b6f9071342273 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 29 Oct 2024 14:12:25 +0800 Subject: [PATCH 03/44] Add manual repo for duplicate span ids (#6475) --- playground/Stress/Stress.ApiService/Program.cs | 12 ++++++++++++ .../Stress/Stress.ApiService/TraceCreator.cs | 17 +++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/playground/Stress/Stress.ApiService/Program.cs b/playground/Stress/Stress.ApiService/Program.cs index 086e6fafb9..bcf9429ebb 100644 --- a/playground/Stress/Stress.ApiService/Program.cs +++ b/playground/Stress/Stress.ApiService/Program.cs @@ -225,4 +225,16 @@ async IAsyncEnumerable WriteOutput() return "Log with formatted data"; }); +app.MapGet("/duplicate-spanid", async () => +{ + var traceCreator = new TraceCreator(); + var span1 = traceCreator.CreateActivity("Test 1", "0485b1947fe788bb"); + await Task.Delay(1000); + span1?.Stop(); + var span2 = traceCreator.CreateActivity("Test 2", "0485b1947fe788bb"); + await Task.Delay(1000); + span2?.Stop(); + return $"Created duplicate span IDs."; +}); + app.Run(); diff --git a/playground/Stress/Stress.ApiService/TraceCreator.cs b/playground/Stress/Stress.ApiService/TraceCreator.cs index 5a7accc699..c8b08c60a8 100644 --- a/playground/Stress/Stress.ApiService/TraceCreator.cs +++ b/playground/Stress/Stress.ApiService/TraceCreator.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Reflection; namespace Stress.ApiService; @@ -13,6 +14,22 @@ public class TraceCreator private readonly List _allActivities = new List(); + public Activity? CreateActivity(string name, string? spandId) + { + var activity = s_activitySource.StartActivity(name, ActivityKind.Client); + if (activity != null) + { + if (spandId != null) + { + // Gross but it's the only way. + typeof(Activity).GetField("_spanId", BindingFlags.Instance | BindingFlags.NonPublic)!.SetValue(activity, spandId); + typeof(Activity).GetField("_traceId", BindingFlags.Instance | BindingFlags.NonPublic)!.SetValue(activity, activity.TraceId.ToString()); + } + } + + return activity; + } + public async Task CreateTraceAsync(int count, bool createChildren) { var activityStack = new Stack(); From 93364363e352299e4ece1ac307f5772ee5eea97f Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Tue, 29 Oct 2024 17:22:49 +1100 Subject: [PATCH 04/44] Enable devcontainers in repo. (#6491) Enable the use of DevContainers in the dotnet/aspire repository. This change includes the .devcontainer file optimized for use with our repo including some initial port forwards for demonstration purposes. From where we can enable pre-builds so we can more easily start using Codespaces to help trash out issues as are improving support for some of the various remote dev scenarios. --- .devcontainer/devcontainer.json | 94 +++++++++++++++++++ playground/Redis/Redis.AppHost/Program.cs | 4 +- .../waitfor/WaitForSandbox.AppHost/Program.cs | 5 +- .../PostgresBuilderExtensions.cs | 11 +++ .../Codespaces/CodespacesOptions.cs | 66 +++++++++++++ .../Codespaces/CodespacesUrlRewriter.cs | 21 +---- .../DistributedApplicationBuilder.cs | 1 + .../Codespaces/CodespacesUrlRewriterTests.cs | 4 + 8 files changed, 186 insertions(+), 20 deletions(-) create mode 100644 .devcontainer/devcontainer.json create mode 100644 src/Aspire.Hosting/Codespaces/CodespacesOptions.cs diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..f4d1001aba --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,94 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet +{ + "name": "C# (.NET)", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/dotnet:1-9.0", + "features": { + "ghcr.io/devcontainers/features/azure-cli:1": {}, + "ghcr.io/azure/azure-dev/azd:0": {}, + "ghcr.io/devcontainers/features/docker-in-docker": {}, + "ghcr.io/devcontainers/features/dotnet": { + "additionalVersions": [ + "8.0.403" + ] + } + }, + + "hostRequirements": { + "cpus": 8, + "memory": "32gb", + "storage": "64gb" + }, + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [ + 15887, + 5180, + 7024, + 15551, + 33803, + 5350, + 41567, + 15306 + ], + "portsAttributes": { + "5180": { + "label": "WaitFor Playground: ApiService", + "protocol": "http" + }, + "5350": { + "label": "Redis Playground: Api Service" + }, + "7024": { + "label": "WaitFor Playground: Frontend", + "protocol": "https" + }, + "15306": { + "label": "Redis Playground: App Host" + }, + "15551": { + "label": "WaitFor Playground: PGAdmin", + "protocol": "http" + }, + "15887": { + "label": "WaitFor Playground: AppHost", + "protocol": "https" + }, + "33803": { + "label": "Redis Playground: Redis Commander" + }, + "41567": { + "label": "Redis Playground: Redis Insight" + } + }, + "otherPortsAttributes": { + "onAutoForward": "ignore" + }, + + // Use 'postCreateCommand' to run commands after the container is created. + "customizations": { + "vscode": { + "extensions": [ + "ms-dotnettools.csdevkit", + "ms-azuretools.vscode-bicep", + "ms-azuretools.azure-dev" + ], + "settings": { + "remote.autoForwardPorts": false, + "dotnet.defaultSolution": "Aspire.sln" + } + } + }, + "onCreateCommand": "dotnet restore", + "postStartCommand": "dotnet dev-certs https --trust" + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/playground/Redis/Redis.AppHost/Program.cs b/playground/Redis/Redis.AppHost/Program.cs index b2cbe95d6a..93669bc6be 100644 --- a/playground/Redis/Redis.AppHost/Program.cs +++ b/playground/Redis/Redis.AppHost/Program.cs @@ -2,8 +2,8 @@ var redis = builder.AddRedis("redis") .WithDataVolume() - .WithRedisCommander() - .WithRedisInsight(); + .WithRedisCommander(c => c.WithHostPort(33803)) + .WithRedisInsight(c => c.WithHostPort(41567)); var garnet = builder.AddGarnet("garnet") .WithDataVolume(); diff --git a/playground/waitfor/WaitForSandbox.AppHost/Program.cs b/playground/waitfor/WaitForSandbox.AppHost/Program.cs index 6ee1f5ac79..e7ad817dd6 100644 --- a/playground/waitfor/WaitForSandbox.AppHost/Program.cs +++ b/playground/waitfor/WaitForSandbox.AppHost/Program.cs @@ -7,7 +7,10 @@ .WithPasswordAuthentication() .RunAsContainer(c => { - c.WithPgAdmin(); + c.WithPgAdmin(c => + { + c.WithHostPort(15551); + }); }) .AddDatabase("db"); diff --git a/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs b/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs index 7ba2558646..e588c37c51 100644 --- a/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs +++ b/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs @@ -6,6 +6,7 @@ using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Postgres; using Aspire.Hosting.Utils; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace Aspire.Hosting; @@ -333,6 +334,16 @@ private static void SetPgAdminEnvironmentVariables(EnvironmentCallbackContext co // You need to define the PGADMIN_DEFAULT_EMAIL and PGADMIN_DEFAULT_PASSWORD or PGADMIN_DEFAULT_PASSWORD_FILE environment variables. context.EnvironmentVariables.Add("PGADMIN_DEFAULT_EMAIL", "admin@domain.com"); context.EnvironmentVariables.Add("PGADMIN_DEFAULT_PASSWORD", "admin"); + + // When running in the context of Codespaces we need to set some additional environment + // varialbes so that PGAdmin will trust the forwarded headers that Codespaces port + // forwarding will send. + var config = context.ExecutionContext.ServiceProvider.GetRequiredService(); + if (context.ExecutionContext.IsRunMode && config.GetValue("CODESPACES", false)) + { + context.EnvironmentVariables["PGADMIN_CONFIG_PROXY_X_HOST_COUNT"] = "1"; + context.EnvironmentVariables["PGADMIN_CONFIG_PROXY_X_PREFIX_COUNT"] = "1"; + } } /// diff --git a/src/Aspire.Hosting/Codespaces/CodespacesOptions.cs b/src/Aspire.Hosting/Codespaces/CodespacesOptions.cs new file mode 100644 index 0000000000..8e54dbf695 --- /dev/null +++ b/src/Aspire.Hosting/Codespaces/CodespacesOptions.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; + +namespace Aspire.Hosting.Codespaces; + +/// +/// GitHub Codespaces configuration values. +/// +internal class CodespacesOptions +{ + /// + /// When set to true, the app host is running in a GitHub Codespace. + /// + /// + /// Maps to the CODESPACE environment variable. + /// + public bool IsCodespace { get; set; } + + /// + /// When set it is the domain suffix used when port forwarding services hosted on the Codespace. + /// + /// + /// Maps to the GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN environment variable. + /// + [MemberNotNullWhen(true, nameof(IsCodespace))] + public string? PortForwardingDomain { get; set; } + + /// + /// When set it is the name of the GitHub Codespace in which the app host is running. + /// + /// + /// Maps to the CODESPACE_NAME environment variable. + /// + [MemberNotNullWhen(true, nameof(IsCodespace))] + public string? CodespaceName { get; set; } +} + +internal class ConfigureCodespacesOptions(IConfiguration configuration) : IConfigureOptions +{ + private const string CodespacesEnvironmentVariable = "CODESPACES"; + private const string CodespaceNameEnvironmentVariable = "CODESPACE_NAME"; + private const string GitHubCodespacesPortForwardingDomain = "GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN"; + + private string GetRequiredCodespacesConfigurationValue(string key) + { + ArgumentNullException.ThrowIfNullOrEmpty(key); + return configuration.GetValue(key) ?? throw new DistributedApplicationException($"Codespaces was detected but {key} environment missing."); + } + + public void Configure(CodespacesOptions options) + { + if (!configuration.GetValue(CodespacesEnvironmentVariable, false)) + { + options.IsCodespace = false; + return; + } + + options.IsCodespace = true; + options.PortForwardingDomain = GetRequiredCodespacesConfigurationValue(GitHubCodespacesPortForwardingDomain); + options.CodespaceName = GetRequiredCodespacesConfigurationValue(CodespaceNameEnvironmentVariable); + } +} \ No newline at end of file diff --git a/src/Aspire.Hosting/Codespaces/CodespacesUrlRewriter.cs b/src/Aspire.Hosting/Codespaces/CodespacesUrlRewriter.cs index e72c3657c2..4d6a113c72 100644 --- a/src/Aspire.Hosting/Codespaces/CodespacesUrlRewriter.cs +++ b/src/Aspire.Hosting/Codespaces/CodespacesUrlRewriter.cs @@ -3,35 +3,22 @@ using System.Collections.Immutable; using Aspire.Hosting.ApplicationModel; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; namespace Aspire.Hosting.Codespaces; -internal sealed class CodespacesUrlRewriter(ILogger logger, IConfiguration configuration, ResourceNotificationService resourceNotificationService) : BackgroundService +internal sealed class CodespacesUrlRewriter(ILogger logger, IOptions options, ResourceNotificationService resourceNotificationService) : BackgroundService { - private const string CodespacesEnvironmentVariable = "CODESPACES"; - private const string CodespaceNameEnvironmentVariable = "CODESPACE_NAME"; - private const string GitHubCodespacesPortForwardingDomain = "GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN"; - - private string GetRequiredCodespacesConfigurationValue(string key) - { - ArgumentNullException.ThrowIfNullOrEmpty(key); - return configuration.GetValue(key) ?? throw new DistributedApplicationException($"Codespaces was detected but {key} environment missing."); - } - protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - if (!configuration.GetValue(CodespacesEnvironmentVariable, false)) + if (!options.Value.IsCodespace) { logger.LogTrace("Not running in Codespaces, skipping URL rewriting."); return; } - var gitHubCodespacesPortForwardingDomain = GetRequiredCodespacesConfigurationValue(GitHubCodespacesPortForwardingDomain); - var codespaceName = GetRequiredCodespacesConfigurationValue(CodespaceNameEnvironmentVariable); - do { try @@ -58,7 +45,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) // which is typically ".app.github.dev". The VSCode instance is typically // hosted at codespacename.github.dev whereas the forwarded ports // would be at codespacename-port.app.github.dev. - Url = $"{uri.Scheme}://{codespaceName}-{uri.Port}.{gitHubCodespacesPortForwardingDomain}{uri.AbsolutePath}" + Url = $"{uri.Scheme}://{options.Value.CodespaceName}-{uri.Port}.{options.Value.PortForwardingDomain}{uri.AbsolutePath}" }; remappedUrls.Add(originalUrlSnapshot, newUrlSnapshot); diff --git a/src/Aspire.Hosting/DistributedApplicationBuilder.cs b/src/Aspire.Hosting/DistributedApplicationBuilder.cs index cf2d5824f1..c1ced8265c 100644 --- a/src/Aspire.Hosting/DistributedApplicationBuilder.cs +++ b/src/Aspire.Hosting/DistributedApplicationBuilder.cs @@ -269,6 +269,7 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options) _innerBuilder.Services.AddSingleton(); // Codespaces + _innerBuilder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, ConfigureCodespacesOptions>()); _innerBuilder.Services.AddHostedService(); Eventing.Subscribe(BuiltInDistributedApplicationEventSubscriptionHandlers.InitializeDcpAnnotations); diff --git a/tests/Aspire.Hosting.Tests/Codespaces/CodespacesUrlRewriterTests.cs b/tests/Aspire.Hosting.Tests/Codespaces/CodespacesUrlRewriterTests.cs index b004b8bdf2..e8444a2496 100644 --- a/tests/Aspire.Hosting.Tests/Codespaces/CodespacesUrlRewriterTests.cs +++ b/tests/Aspire.Hosting.Tests/Codespaces/CodespacesUrlRewriterTests.cs @@ -15,6 +15,10 @@ public class CodespacesUrlRewriterTests(ITestOutputHelper testOutputHelper) public async Task VerifyUrlsRewriterStopsWhenNotInCodespaces() { using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper); + + // Explicitly disable codespace behavior for this test. + builder.Configuration["CODESPACES"] = "false"; + builder.Services.AddLogging(logging => { logging.AddFakeLogging(); From 96ede0303947c42fb31fb06987c319186a9e5a59 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 29 Oct 2024 14:57:35 +0800 Subject: [PATCH 05/44] Hosting test timeouts (#6387) --- ...Constants.cs => ComponentTestConstants.cs} | 2 +- .../ElasticsearchContainerFixture.cs | 2 +- .../RedisFunctionalTests.cs | 2 +- .../Aspire.Hosting.Tests/AddParameterTests.cs | 9 +- .../Aspire.Hosting.Tests.csproj | 1 - .../Dashboard/DashboardLifecycleHookTests.cs | 14 +- .../Dashboard/DashboardResourceTests.cs | 51 ++-- .../Dashboard/ResourcePublisherTests.cs | 51 ++-- .../Dcp/ApplicationExecutorTests.cs | 1 - .../DistributedApplicationTests.cs | 222 ++++++++---------- ...tributedApplicationBuilderEventingTests.cs | 34 ++- .../ExecutableResourceTests.cs | 5 +- .../ExpressionResolverTests.cs | 10 +- .../Health/ResourceHealthCheckServiceTests.cs | 110 ++++----- .../Aspire.Hosting.Tests/HealthCheckTests.cs | 22 +- .../Helpers/KubernetesHelper.cs | 4 +- .../KestrelConfigTests.cs | 15 +- .../ManifestGenerationTests.cs | 9 +- .../ProjectResourceTests.cs | 29 +-- .../PublishAsConnectionStringTests.cs | 3 +- .../PublishAsDockerfileTests.cs | 7 +- .../ResourceExtensionsTests.cs | 7 +- .../ResourceLoggerServiceTests.cs | 57 ++--- .../ResourceNotificationTests.cs | 79 +++---- .../SlimTestProgramTests.cs | 7 +- .../TestProgramFixture.cs | 3 +- .../TestDistributedApplicationBuilder.cs | 2 +- tests/Aspire.Hosting.Tests/WaitForTests.cs | 35 +-- .../Aspire.Hosting.Tests/WithEndpointTests.cs | 23 +- .../WithEnvironmentTests.cs | 29 +-- .../WithReferenceTests.cs | 29 +-- .../MilvusContainerFixture.cs | 2 +- .../MongoDbContainerFixture.cs | 2 +- .../MySqlContainerFixture.cs | 2 +- .../NatsContainerFixture.cs | 2 +- .../PostgreSQLContainerFixture.cs | 2 +- .../OracleContainerFixture.cs | 2 +- .../AspireRabbitMQLoggingTests.cs | 2 +- .../RabbitMQContainerFixture.cs | 2 +- .../RedisContainerFixture.cs | 2 +- tests/Shared/AsyncTestHelpers.cs | 156 ++++++++++++ tests/Shared/TaskExtensions.cs | 135 ----------- 42 files changed, 587 insertions(+), 596 deletions(-) rename tests/Aspire.Components.Common.Tests/{TestConstants.cs => ComponentTestConstants.cs} (86%) delete mode 100644 tests/Shared/TaskExtensions.cs diff --git a/tests/Aspire.Components.Common.Tests/TestConstants.cs b/tests/Aspire.Components.Common.Tests/ComponentTestConstants.cs similarity index 86% rename from tests/Aspire.Components.Common.Tests/TestConstants.cs rename to tests/Aspire.Components.Common.Tests/ComponentTestConstants.cs index 4b7971bf1f..f31dcce7d7 100644 --- a/tests/Aspire.Components.Common.Tests/TestConstants.cs +++ b/tests/Aspire.Components.Common.Tests/ComponentTestConstants.cs @@ -3,7 +3,7 @@ namespace Aspire.Components.Common.Tests; -public static class TestConstants +public static class ComponentTestConstants { public const string AspireTestContainerRegistry = "netaspireci.azurecr.io"; } diff --git a/tests/Aspire.Elastic.Clients.Elasticsearch.Tests/ElasticsearchContainerFixture.cs b/tests/Aspire.Elastic.Clients.Elasticsearch.Tests/ElasticsearchContainerFixture.cs index 037cca297c..881ba94431 100644 --- a/tests/Aspire.Elastic.Clients.Elasticsearch.Tests/ElasticsearchContainerFixture.cs +++ b/tests/Aspire.Elastic.Clients.Elasticsearch.Tests/ElasticsearchContainerFixture.cs @@ -20,7 +20,7 @@ public async Task InitializeAsync() if (RequiresDockerAttribute.IsSupported) { Container = new ElasticsearchBuilder() - .WithImage($"{TestConstants.AspireTestContainerRegistry}/{ElasticsearchContainerImageTags.Image}:{ElasticsearchContainerImageTags.Tag}") + .WithImage($"{ComponentTestConstants.AspireTestContainerRegistry}/{ElasticsearchContainerImageTags.Image}:{ElasticsearchContainerImageTags.Tag}") .Build(); await Container.StartAsync(); } diff --git a/tests/Aspire.Hosting.Redis.Tests/RedisFunctionalTests.cs b/tests/Aspire.Hosting.Redis.Tests/RedisFunctionalTests.cs index ff6e91d44c..0b01d42403 100644 --- a/tests/Aspire.Hosting.Redis.Tests/RedisFunctionalTests.cs +++ b/tests/Aspire.Hosting.Redis.Tests/RedisFunctionalTests.cs @@ -130,7 +130,7 @@ public async Task VerifyDatabasesAreNotDuplicatedForPersistentRedisInsightContai var configure = (DistributedApplicationOptions options) => { - options.ContainerRegistryOverride = TestConstants.AspireTestContainerRegistry; + options.ContainerRegistryOverride = ComponentTestConstants.AspireTestContainerRegistry; }; using var builder1 = TestDistributedApplicationBuilder.Create(configure, testOutputHelper); diff --git a/tests/Aspire.Hosting.Tests/AddParameterTests.cs b/tests/Aspire.Hosting.Tests/AddParameterTests.cs index 763dad147b..128858627d 100644 --- a/tests/Aspire.Hosting.Tests/AddParameterTests.cs +++ b/tests/Aspire.Hosting.Tests/AddParameterTests.cs @@ -4,6 +4,7 @@ using Aspire.Hosting.Lifecycle; using Aspire.Hosting.Publishing; using Aspire.Hosting.Utils; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -147,7 +148,7 @@ public async Task ParametersWithDefaultValueStringOverloadUsedRegardlessOfConfig Assert.Equal($"DefaultValue", parameterResource.Value); // The manifest should not include anything about the default value - var paramManifest = await ManifestUtils.GetManifest(appModel.Resources.OfType().Single(r => r.Name == "pass")); + var paramManifest = await ManifestUtils.GetManifest(appModel.Resources.OfType().Single(r => r.Name == "pass")).DefaultTimeout(); var expectedManifest = $$""" { "type": "parameter.v0", @@ -197,7 +198,7 @@ public async Task ParametersWithDefaultValueGetPublishedIfPublishFlagIsPassed(bo Assert.Equal($"DefaultValue", parameterResource.Value); // The manifest should include the default value, since we passed publishValueAsDefault: true - var paramManifest = await ManifestUtils.GetManifest(appModel.Resources.OfType().Single(r => r.Name == "pass")); + var paramManifest = await ManifestUtils.GetManifest(appModel.Resources.OfType().Single(r => r.Name == "pass")).DefaultTimeout(); var expectedManifest = $$""" { "type": "parameter.v0", @@ -255,7 +256,7 @@ public async Task ParametersWithDefaultValueObjectOverloadUsedRegardlessOfConfig Assert.Equal(10, parameterResource.Value.Length); // The manifest should include the fields for the generated default value - var paramManifest = await ManifestUtils.GetManifest(appModel.Resources.OfType().Single(r => r.Name == "pass")); + var paramManifest = await ManifestUtils.GetManifest(appModel.Resources.OfType().Single(r => r.Name == "pass")).DefaultTimeout(); var expectedManifest = $$""" { "type": "parameter.v0", @@ -310,7 +311,7 @@ public async Task ParametersCanGetValueFromNonDefaultConfigurationKeys() Assert.Equal($"MyAccessToken", parameterResource.Value); // The manifest is not affected by the custom configuration key - var paramManifest = await ManifestUtils.GetManifest(appModel.Resources.OfType().Single(r => r.Name == "val")); + var paramManifest = await ManifestUtils.GetManifest(appModel.Resources.OfType().Single(r => r.Name == "val")).DefaultTimeout(); var expectedManifest = $$""" { "type": "parameter.v0", diff --git a/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj b/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj index 4abc103be5..484d9f3940 100644 --- a/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj +++ b/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj @@ -23,7 +23,6 @@ - diff --git a/tests/Aspire.Hosting.Tests/Dashboard/DashboardLifecycleHookTests.cs b/tests/Aspire.Hosting.Tests/Dashboard/DashboardLifecycleHookTests.cs index 0cd7644049..b66eedde69 100644 --- a/tests/Aspire.Hosting.Tests/Dashboard/DashboardLifecycleHookTests.cs +++ b/tests/Aspire.Hosting.Tests/Dashboard/DashboardLifecycleHookTests.cs @@ -8,6 +8,7 @@ using Aspire.Hosting.Dashboard; using Aspire.Hosting.Dcp; using Aspire.Hosting.Tests.Utils; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -42,12 +43,12 @@ public async Task WatchDashboardLogs_WrittenToHostLoggerFactory(DateTime? timest var hook = CreateHook(resourceLoggerService, resourceNotificationService, configuration, loggerFactory: factory); var model = new DistributedApplicationModel(new ResourceCollection()); - await hook.BeforeStartAsync(model, CancellationToken.None); + await hook.BeforeStartAsync(model, CancellationToken.None).DefaultTimeout(); - await resourceNotificationService.PublishUpdateAsync(model.Resources.Single(), s => s); + await resourceNotificationService.PublishUpdateAsync(model.Resources.Single(), s => s).DefaultTimeout(); string resourceId = default!; - await foreach (var item in resourceLoggerService.WatchAnySubscribersAsync()) + await foreach (var item in resourceLoggerService.WatchAnySubscribersAsync().DefaultTimeout()) { if (item.Name.StartsWith(KnownResourceNames.AspireDashboard) && item.AnySubscribers) { @@ -61,10 +62,9 @@ public async Task WatchDashboardLogs_WrittenToHostLoggerFactory(DateTime? timest dashboardLoggerState.AddLog(LogEntry.Create(timestamp, logMessage, isErrorMessage: false), inMemorySource: true); // Assert - var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - while (!cts.IsCancellationRequested) + while (true) { - var logContext = await logChannel.Reader.ReadAsync(cts.Token); + var logContext = await logChannel.Reader.ReadAsync().DefaultTimeout(); if (logContext.LoggerName == expectedCategory) { Assert.Equal(expectedMessage, logContext.Message); @@ -86,7 +86,7 @@ public async Task BeforeStartAsync_ExcludeLifecycleCommands_CommandsNotAddedToDa var model = new DistributedApplicationModel(new ResourceCollection()); // Act - await hook.BeforeStartAsync(model, CancellationToken.None); + await hook.BeforeStartAsync(model, CancellationToken.None).DefaultTimeout(); var dashboardResource = model.Resources.Single(r => string.Equals(r.Name, KnownResourceNames.AspireDashboard, StringComparisons.ResourceName)); dashboardResource.AddLifeCycleCommands(); diff --git a/tests/Aspire.Hosting.Tests/Dashboard/DashboardResourceTests.cs b/tests/Aspire.Hosting.Tests/Dashboard/DashboardResourceTests.cs index 7fa6c37ad3..05c9962306 100644 --- a/tests/Aspire.Hosting.Tests/Dashboard/DashboardResourceTests.cs +++ b/tests/Aspire.Hosting.Tests/Dashboard/DashboardResourceTests.cs @@ -7,6 +7,7 @@ using Aspire.Hosting.Dcp; using Aspire.Hosting.Tests.Utils; using Aspire.Hosting.Utils; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -40,7 +41,7 @@ public async Task DashboardIsAutomaticallyAddedAsHiddenResource() using var app = builder.Build(); - await app.ExecuteBeforeStartHooksAsync(default); + await app.ExecuteBeforeStartHooksAsync(default).DefaultTimeout(); var model = app.Services.GetRequiredService(); @@ -64,7 +65,7 @@ public async Task DashboardIsAddedFirst() using var app = builder.Build(); - await app.ExecuteBeforeStartHooksAsync(default); + await app.ExecuteBeforeStartHooksAsync(default).DefaultTimeout(); var model = app.Services.GetRequiredService(); @@ -95,7 +96,7 @@ public async Task DashboardDoesNotAddResource_ConfiguresExistingDashboard() using var app = builder.Build(); - await app.ExecuteBeforeStartHooksAsync(default); + await app.ExecuteBeforeStartHooksAsync(default).DefaultTimeout(); var model = app.Services.GetRequiredService(); @@ -103,7 +104,7 @@ public async Task DashboardDoesNotAddResource_ConfiguresExistingDashboard() Assert.Same(container.Resource, dashboard); - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(dashboard, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(dashboard, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); Assert.Collection(config, e => @@ -165,13 +166,13 @@ public async Task DashboardWithDllPathLaunchesDotnet() var app = builder.Build(); - await app.ExecuteBeforeStartHooksAsync(default); + await app.ExecuteBeforeStartHooksAsync(default).DefaultTimeout(); var model = app.Services.GetRequiredService(); var dashboard = Assert.Single(model.Resources.OfType()); - var args = await ArgumentEvaluator.GetArgumentListAsync(dashboard); + var args = await ArgumentEvaluator.GetArgumentListAsync(dashboard).DefaultTimeout(); Assert.NotNull(dashboard); Assert.Equal("aspire-dashboard", dashboard.Name); @@ -201,13 +202,13 @@ public async Task DashboardAuthConfigured_EnvVarsPresent() using var app = builder.Build(); - await app.ExecuteBeforeStartHooksAsync(default); + await app.ExecuteBeforeStartHooksAsync(default).DefaultTimeout(); var model = app.Services.GetRequiredService(); var dashboard = Assert.Single(model.Resources); - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(dashboard, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(dashboard, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); Assert.Equal("BrowserToken", config.Single(e => e.Key == DashboardConfigNames.DashboardFrontendAuthModeName.EnvVarName).Value); Assert.Equal("TestBrowserToken!", config.Single(e => e.Key == DashboardConfigNames.DashboardFrontendBrowserTokenName.EnvVarName).Value); @@ -236,13 +237,13 @@ public async Task DashboardAuthRemoved_EnvVarsUnsecured() using var app = builder.Build(); - await app.ExecuteBeforeStartHooksAsync(default); + await app.ExecuteBeforeStartHooksAsync(default).DefaultTimeout(); var model = app.Services.GetRequiredService(); var dashboard = Assert.Single(model.Resources); - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(dashboard, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(dashboard, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); Assert.Equal("Unsecured", config.Single(e => e.Key == DashboardConfigNames.DashboardFrontendAuthModeName.EnvVarName).Value); Assert.Equal("Unsecured", config.Single(e => e.Key == DashboardConfigNames.DashboardOtlpAuthModeName.EnvVarName).Value); @@ -268,13 +269,13 @@ public async Task DashboardResourceServiceUriIsSet() using var app = builder.Build(); - await app.ExecuteBeforeStartHooksAsync(default); + await app.ExecuteBeforeStartHooksAsync(default).DefaultTimeout(); var model = app.Services.GetRequiredService(); var dashboard = Assert.Single(model.Resources); - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(dashboard, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(dashboard, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); Assert.Equal("http://localhost:5000", config.Single(e => e.Key == DashboardConfigNames.ResourceServiceUrlName.EnvVarName).Value); } @@ -301,7 +302,7 @@ public async Task DashboardResource_OtlpHttpEndpoint_CorsEnvVarSet() using var app = builder.Build(); - await app.ExecuteBeforeStartHooksAsync(default); + await app.ExecuteBeforeStartHooksAsync(default).DefaultTimeout(); var model = app.Services.GetRequiredService(); @@ -312,7 +313,7 @@ public async Task DashboardResource_OtlpHttpEndpoint_CorsEnvVarSet() var dashboard = Assert.Single(model.Resources.Where(r => r.Name == "aspire-dashboard")); - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(dashboard, DistributedApplicationOperation.Run, app.Services); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(dashboard, DistributedApplicationOperation.Run, app.Services).DefaultTimeout(); Assert.Equal("http://localhost:8081,http://localhost:58080", config.Single(e => e.Key == DashboardConfigNames.DashboardOtlpCorsAllowedOriginsKeyName.EnvVarName).Value); Assert.Equal("*", config.Single(e => e.Key == DashboardConfigNames.DashboardOtlpCorsAllowedHeadersKeyName.EnvVarName).Value); @@ -340,13 +341,13 @@ public async Task DashboardResource_OtlpGrpcEndpoint_CorsEnvVarNotSet() using var app = builder.Build(); - await app.ExecuteBeforeStartHooksAsync(default); + await app.ExecuteBeforeStartHooksAsync(default).DefaultTimeout(); var model = app.Services.GetRequiredService(); var dashboard = Assert.Single(model.Resources.Where(r => r.Name == "aspire-dashboard")); - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(dashboard, DistributedApplicationOperation.Run, app.Services); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(dashboard, DistributedApplicationOperation.Run, app.Services).DefaultTimeout(); Assert.DoesNotContain(config, e => e.Key == DashboardConfigNames.DashboardOtlpCorsAllowedOriginsKeyName.EnvVarName); Assert.DoesNotContain(config, e => e.Key == DashboardConfigNames.DashboardOtlpCorsAllowedHeadersKeyName.EnvVarName); @@ -365,7 +366,7 @@ public async Task DashboardIsNotAddedInPublishMode() using var app = builder.Build(); - await app.ExecuteBeforeStartHooksAsync(default); + await app.ExecuteBeforeStartHooksAsync(default).DefaultTimeout(); var model = app.Services.GetRequiredService(); @@ -381,7 +382,7 @@ public async Task DashboardIsNotAddedIfDisabled() var app = builder.Build(); - await app.ExecuteBeforeStartHooksAsync(default); + await app.ExecuteBeforeStartHooksAsync(default).DefaultTimeout(); var model = app.Services.GetRequiredService(); @@ -446,7 +447,7 @@ public async Task DashboardLifecycleHookWatchesLogs(LogLevel logLevel) } }); - await app.ExecuteBeforeStartHooksAsync(default).WaitAsync(TimeSpan.FromSeconds(15)); + await app.ExecuteBeforeStartHooksAsync(default).DefaultTimeout(); var model = app.Services.GetRequiredService(); var resourceNotificationService = app.Services.GetRequiredService(); @@ -457,10 +458,10 @@ public async Task DashboardLifecycleHookWatchesLogs(LogLevel logLevel) Assert.Equal("aspire-dashboard", dashboard.Name); // Push a notification through to the dashboard resource. - await resourceNotificationService.PublishUpdateAsync(dashboard, "aspire-dashboard-0", s => s with { State = "Running" }); + await resourceNotificationService.PublishUpdateAsync(dashboard, "aspire-dashboard-0", s => s with { State = "Running" }).DefaultTimeout(); // Wait for logs to be subscribed to - await watchForLogSubs.WaitAsync(TimeSpan.FromSeconds(15)); + await watchForLogSubs.DefaultTimeout(); // Push some logs through to the dashboard resource. var logger = resourceLoggerService.GetLogger("aspire-dashboard-0"); @@ -481,12 +482,12 @@ public async Task DashboardLifecycleHookWatchesLogs(LogLevel logLevel) Assert.NotNull(testLogger); // Get the first log message that was logged - var log = await testLogger.FirstLogTask.WaitAsync(TimeSpan.FromSeconds(15)); + var log = await testLogger.FirstLogTask.DefaultTimeout(); Assert.Equal("Test dashboard message", log.Message); Assert.Equal(logLevel, log.LogLevel); - await app.DisposeAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(15)); + await app.DisposeAsync().AsTask().DefaultTimeout(); } [Fact] @@ -498,7 +499,7 @@ public async Task DashboardIsExcludedFromManifestInPublishModeEvenIfAddedExplici var app = builder.Build(); - await app.ExecuteBeforeStartHooksAsync(default); + await app.ExecuteBeforeStartHooksAsync(default).DefaultTimeout(); var model = app.Services.GetRequiredService(); @@ -507,7 +508,7 @@ public async Task DashboardIsExcludedFromManifestInPublishModeEvenIfAddedExplici Assert.NotNull(dashboard); var annotation = Assert.Single(dashboard.Annotations.OfType()); - var manifest = await ManifestUtils.GetManifestOrNull(dashboard); + var manifest = await ManifestUtils.GetManifestOrNull(dashboard).DefaultTimeout(); Assert.Equal("aspire-dashboard", dashboard.Name); Assert.Same(ManifestPublishingCallbackAnnotation.Ignore, annotation); diff --git a/tests/Aspire.Hosting.Tests/Dashboard/ResourcePublisherTests.cs b/tests/Aspire.Hosting.Tests/Dashboard/ResourcePublisherTests.cs index 9f4fb0e48b..bc39540e67 100644 --- a/tests/Aspire.Hosting.Tests/Dashboard/ResourcePublisherTests.cs +++ b/tests/Aspire.Hosting.Tests/Dashboard/ResourcePublisherTests.cs @@ -3,6 +3,7 @@ using Aspire.Dashboard.Model; using Aspire.Hosting.Dashboard; +using Microsoft.AspNetCore.InternalTesting; using Xunit; namespace Aspire.Hosting.Tests.Dashboard; @@ -19,8 +20,8 @@ public async Task ProducesExpectedSnapshotAndUpdates() var b = CreateResourceSnapshot("B"); var c = CreateResourceSnapshot("C"); - await publisher.IntegrateAsync(new TestResource("A"), a, ResourceSnapshotChangeType.Upsert); - await publisher.IntegrateAsync(new TestResource("B"), b, ResourceSnapshotChangeType.Upsert); + await publisher.IntegrateAsync(new TestResource("A"), a, ResourceSnapshotChangeType.Upsert).DefaultTimeout(); + await publisher.IntegrateAsync(new TestResource("B"), b, ResourceSnapshotChangeType.Upsert).DefaultTimeout(); Assert.Equal(0, publisher.OutgoingSubscriberCount); @@ -42,17 +43,17 @@ public async Task ProducesExpectedSnapshotAndUpdates() } }); - await publisher.IntegrateAsync(new TestResource("C"), c, ResourceSnapshotChangeType.Upsert); + await publisher.IntegrateAsync(new TestResource("C"), c, ResourceSnapshotChangeType.Upsert).DefaultTimeout(); - var change = Assert.Single(await tcs.Task); + var change = Assert.Single(await tcs.Task.DefaultTimeout()); Assert.Equal(ResourceSnapshotChangeType.Upsert, change.ChangeType); Assert.Equal("C", change.Resource.Name); - await cts.CancelAsync(); + await cts.CancelAsync().DefaultTimeout(); try { - await task; + await task.DefaultTimeout(); } catch (OperationCanceledException) { @@ -72,8 +73,8 @@ public async Task SupportsMultipleSubscribers() var b = CreateResourceSnapshot("B"); var c = CreateResourceSnapshot("C"); - await publisher.IntegrateAsync(new TestResource("A"), a, ResourceSnapshotChangeType.Upsert); - await publisher.IntegrateAsync(new TestResource("B"), b, ResourceSnapshotChangeType.Upsert); + await publisher.IntegrateAsync(new TestResource("A"), a, ResourceSnapshotChangeType.Upsert).DefaultTimeout(); + await publisher.IntegrateAsync(new TestResource("B"), b, ResourceSnapshotChangeType.Upsert).DefaultTimeout(); Assert.Equal(0, publisher.OutgoingSubscriberCount); @@ -85,13 +86,13 @@ public async Task SupportsMultipleSubscribers() Assert.Equal(2, snapshot1.Length); Assert.Equal(2, snapshot2.Length); - await publisher.IntegrateAsync(new TestResource("C"), c, ResourceSnapshotChangeType.Upsert); + await publisher.IntegrateAsync(new TestResource("C"), c, ResourceSnapshotChangeType.Upsert).DefaultTimeout(); var enumerator1 = subscription1.GetAsyncEnumerator(cts.Token); var enumerator2 = subscription2.GetAsyncEnumerator(cts.Token); - await enumerator1.MoveNextAsync(); - await enumerator2.MoveNextAsync(); + await enumerator1.MoveNextAsync().DefaultTimeout(); + await enumerator2.MoveNextAsync().DefaultTimeout(); var v1 = Assert.Single(enumerator1.Current); var v2 = Assert.Single(enumerator2.Current); @@ -101,10 +102,10 @@ public async Task SupportsMultipleSubscribers() Assert.Equal("C", v1.Resource.Name); Assert.Equal("C", v2.Resource.Name); - await cts.CancelAsync(); + await cts.CancelAsync().DefaultTimeout(); - Assert.False(await enumerator1.MoveNextAsync()); - Assert.False(await enumerator2.MoveNextAsync()); + Assert.False(await enumerator1.MoveNextAsync().DefaultTimeout()); + Assert.False(await enumerator2.MoveNextAsync().DefaultTimeout()); Assert.Equal(0, publisher.OutgoingSubscriberCount); } @@ -119,15 +120,15 @@ public async Task MergesResourcesInSnapshot() var a2 = CreateResourceSnapshot("A"); var a3 = CreateResourceSnapshot("A"); - await publisher.IntegrateAsync(new TestResource("A"), a1, ResourceSnapshotChangeType.Upsert); - await publisher.IntegrateAsync(new TestResource("A"), a2, ResourceSnapshotChangeType.Upsert); - await publisher.IntegrateAsync(new TestResource("A"), a3, ResourceSnapshotChangeType.Upsert); + await publisher.IntegrateAsync(new TestResource("A"), a1, ResourceSnapshotChangeType.Upsert).DefaultTimeout(); + await publisher.IntegrateAsync(new TestResource("A"), a2, ResourceSnapshotChangeType.Upsert).DefaultTimeout(); + await publisher.IntegrateAsync(new TestResource("A"), a3, ResourceSnapshotChangeType.Upsert).DefaultTimeout(); var (snapshot, _) = publisher.Subscribe(); Assert.Equal("A", Assert.Single(snapshot).Name); - await cts.CancelAsync(); + await cts.CancelAsync().DefaultTimeout(); } [Fact] @@ -139,15 +140,15 @@ public async Task DeletesRemoveFromSnapshot() var a = CreateResourceSnapshot("A"); var b = CreateResourceSnapshot("B"); - await publisher.IntegrateAsync(new TestResource("A"), a, ResourceSnapshotChangeType.Upsert); - await publisher.IntegrateAsync(new TestResource("B"), b, ResourceSnapshotChangeType.Upsert); - await publisher.IntegrateAsync(new TestResource("A"), a, ResourceSnapshotChangeType.Delete); + await publisher.IntegrateAsync(new TestResource("A"), a, ResourceSnapshotChangeType.Upsert).DefaultTimeout(); + await publisher.IntegrateAsync(new TestResource("B"), b, ResourceSnapshotChangeType.Upsert).DefaultTimeout(); + await publisher.IntegrateAsync(new TestResource("A"), a, ResourceSnapshotChangeType.Delete).DefaultTimeout(); var (snapshot, _) = publisher.Subscribe(); Assert.Equal("B", Assert.Single(snapshot).Name); - await cts.CancelAsync(); + await cts.CancelAsync().DefaultTimeout(); } [Fact] @@ -168,15 +169,15 @@ public async Task CancelledSubscriptionIsCleanedUp() called = true; // Now we've received something, cancel. - await cts.CancelAsync(); + await cts.CancelAsync().DefaultTimeout(); } }); // Push through an update. - await publisher.IntegrateAsync(new TestResource("A"), CreateResourceSnapshot("A"), ResourceSnapshotChangeType.Upsert); + await publisher.IntegrateAsync(new TestResource("A"), CreateResourceSnapshot("A"), ResourceSnapshotChangeType.Upsert).DefaultTimeout(); // Let the subscriber exit. - await task; + await task.DefaultTimeout(); } private static GenericResourceSnapshot CreateResourceSnapshot(string name) diff --git a/tests/Aspire.Hosting.Tests/Dcp/ApplicationExecutorTests.cs b/tests/Aspire.Hosting.Tests/Dcp/ApplicationExecutorTests.cs index 165901aee1..f47767da08 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/ApplicationExecutorTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/ApplicationExecutorTests.cs @@ -560,7 +560,6 @@ public async Task ResourceLogging_ReplayBacklog_SentInBatch() var exeResource = Assert.Single(kubernetesService.CreatedResources.OfType()); // Start watching logs for container. - var watchCts = new CancellationTokenSource(); var watchSubscribers = resourceLoggerService.WatchAnySubscribersAsync(); var watchSubscribersEnumerator = watchSubscribers.GetAsyncEnumerator(); var watchLogs1 = resourceLoggerService.WatchAsync(exeResource.Metadata.Name); diff --git a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs index 1fdf99710d..3621eb5a10 100644 --- a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs +++ b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics; using System.Globalization; using System.Text.RegularExpressions; using Aspire.Components.Common.Tests; @@ -14,6 +13,7 @@ using Aspire.Hosting.Tests.Utils; using Aspire.Hosting.Utils; using k8s.Models; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -21,6 +21,7 @@ using Xunit; using Xunit.Abstractions; using Xunit.Sdk; +using TestConstants = Microsoft.AspNetCore.InternalTesting.TestConstants; namespace Aspire.Hosting.Tests; @@ -56,7 +57,7 @@ public async Task RegisteredLifecycleHookIsExecutedWhenRunAsynchronously() var cts = new CancellationTokenSource(); cts.CancelAfter(TimeSpan.FromMinutes(1)); await testProgram.RunAsync(cts.Token); - }); + }).DefaultTimeout(); Assert.Equal(exceptionMessage, ex.Message); } @@ -97,7 +98,7 @@ public async Task MultipleRegisteredLifecycleHooksAreExecuted() var cts = new CancellationTokenSource(); cts.CancelAfter(TimeSpan.FromMinutes(1)); await testProgram.RunAsync(cts.Token); - }); + }).DefaultTimeout(); Assert.Equal(exceptionMessage, ex.Message); Assert.True(signal.FirstHookExecuted); @@ -152,9 +153,9 @@ public async Task AllocatedPortsAssignedAfterHookRuns() await using var app = testProgram.Build(); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); - var appModel = await tcs.Task.WaitAsync(TimeSpan.FromSeconds(10)); + var appModel = await tcs.Task.DefaultTimeout(); foreach (var item in appModel.Resources) { @@ -190,51 +191,45 @@ public async Task TestServicesWithMultipleReplicas() var logger = app.Services.GetRequiredService>(); - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - - await app.StartAsync(cts.Token); + await app.StartAsync().DefaultTimeout(); - // Make sure services A and C are running + logger.LogInformation("Make sure services A and C are running"); using var clientA = app.CreateHttpClient(testProgram.ServiceABuilder.Resource.Name, "http"); - await clientA.GetStringAsync("/pid", cts.Token); - using var clientC = app.CreateHttpClient(testProgram.ServiceCBuilder.Resource.Name, "http"); - await clientC.GetStringAsync("/pid", cts.Token); + + await Task.WhenAll(clientA.GetStringAsync("/pid"), clientC.GetStringAsync("/pid")).DefaultTimeout(TestConstants.LongTimeoutDuration); // We should get 3 distinct PIDs from service B Dictionary pids = []; - try + var uri = app.GetEndpoint(testProgram.ServiceBBuilder.Resource.Name, "http"); + + var cts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(); + while (!cts.IsCancellationRequested) { - var uri = app.GetEndpoint(testProgram.ServiceBBuilder.Resource.Name, "http"); - while (true) + using var clientB = new HttpClient(); + var url = $"{uri}pid"; + logger.LogInformation("Calling PID API at {Url}", url); + var pidText = await clientB.GetStringAsync(url).DefaultTimeout(); + if (!string.IsNullOrEmpty(pidText)) { - using var clientB = new HttpClient(); - var url = $"{uri}pid"; - logger.LogInformation("Calling PID API at {Url}", url); - var pidText = await clientB.GetStringAsync(url, cts.Token); - if (!string.IsNullOrEmpty(pidText)) + var pid = int.Parse(pidText, CultureInfo.InvariantCulture); + if (pids.TryAdd(pid, true)) { - var pid = int.Parse(pidText, CultureInfo.InvariantCulture); - if (pids.TryAdd(pid, true)) - { - logger.LogInformation("PID API returned new value: {PID}", pid); + logger.LogInformation("PID API returned new value: {PID}", pid); - if (pids.Count == replicaCount) - { - logger.LogInformation("Success! We heard from all {ReplicaCount} replicas.", replicaCount); - break; - } + if (pids.Count == replicaCount) + { + logger.LogInformation("Success! We heard from all {ReplicaCount} replicas.", replicaCount); + break; } } - - await Task.Delay(100, cts.Token); } + + await Task.Delay(100); } - catch (OperationCanceledException) - { - Assert.Equal(3, pids.Count); - } + + Assert.Equal(3, pids.Count); } [Fact] @@ -251,10 +246,10 @@ public async Task VerifyDockerAppWorks() await using var app = testProgram.Build(); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); var s = app.Services.GetRequiredService(); - var list = await s.ListAsync(); + var list = await s.ListAsync().DefaultTimeout(); Assert.Collection(list, item => @@ -264,7 +259,7 @@ public async Task VerifyDockerAppWorks() Assert.Equal(["--add-host", "testlocalhost:127.0.0.1"], item.Spec.RunArgs); }); - await app.StopAsync(); + await app.StopAsync().DefaultTimeout(); } [Fact] @@ -285,18 +280,18 @@ public async Task VerifyContainerStopStartWorks() var applicationExecutor = app.Services.GetRequiredService(); var suffix = app.Services.GetRequiredService>().Value.ResourceNameSuffix; - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); - using var cts = new CancellationTokenSource(Debugger.IsAttached ? Timeout.InfiniteTimeSpan : TimeSpan.FromMinutes(1)); + using var cts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration); var token = cts.Token; var containerPattern = $"redis0-{ReplicaIdRegex}-{suffix}"; - var redisContainer = await KubernetesHelper.GetResourceByNameMatchAsync(kubernetes, containerPattern, r => r.Status?.State == ContainerState.Running, token); + var redisContainer = await KubernetesHelper.GetResourceByNameMatchAsync(kubernetes, containerPattern, r => r.Status?.State == ContainerState.Running, token).DefaultTimeout(TestConstants.LongTimeoutDuration); Assert.NotNull(redisContainer); - await applicationExecutor.StopResourceAsync(redisContainer.Metadata.Name, token); + await applicationExecutor.StopResourceAsync(redisContainer.Metadata.Name, token).DefaultTimeout(); - redisContainer = await KubernetesHelper.GetResourceByNameMatchAsync(kubernetes, containerPattern, r => r.Status?.State == ContainerState.Exited, token); + redisContainer = await KubernetesHelper.GetResourceByNameMatchAsync(kubernetes, containerPattern, r => r.Status?.State == ContainerState.Exited, token).DefaultTimeout(TestConstants.LongTimeoutDuration); Assert.NotNull(redisContainer); // TODO: Container start has issues in DCP. Waiting for fix. @@ -305,7 +300,7 @@ public async Task VerifyContainerStopStartWorks() //redisContainer = await KubernetesHelper.GetResourceByNameMatchAsync(kubernetes, containerPattern, r => r.Status?.State == ContainerState.Running, token); //Assert.NotNull(redisContainer); - await app.StopAsync(); + await app.StopAsync().DefaultTimeout(); } [Fact] @@ -322,26 +317,23 @@ public async Task VerifyExecutableStopStartWorks() var applicationExecutor = app.Services.GetRequiredService(); var suffix = app.Services.GetRequiredService>().Value.ResourceNameSuffix; - await app.StartAsync(); - - using var cts = new CancellationTokenSource(Debugger.IsAttached ? Timeout.InfiniteTimeSpan : TimeSpan.FromMinutes(1)); - var token = cts.Token; + await app.StartAsync().DefaultTimeout(); var executablePattern = $"servicea-{ReplicaIdRegex}-{suffix}"; - var serviceA = await KubernetesHelper.GetResourceByNameMatchAsync(kubernetes, executablePattern, r => r.Status?.State == ExecutableState.Running, token); + var serviceA = await KubernetesHelper.GetResourceByNameMatchAsync(kubernetes, executablePattern, r => r.Status?.State == ExecutableState.Running).DefaultTimeout(TestConstants.LongTimeoutDuration); Assert.NotNull(serviceA); - await applicationExecutor.StopResourceAsync(serviceA.Metadata.Name, token); + await applicationExecutor.StopResourceAsync(serviceA.Metadata.Name, CancellationToken.None).DefaultTimeout(); - serviceA = await KubernetesHelper.GetResourceByNameMatchAsync(kubernetes, executablePattern, r => r.Status?.State == ExecutableState.Finished, token); + serviceA = await KubernetesHelper.GetResourceByNameMatchAsync(kubernetes, executablePattern, r => r.Status?.State == ExecutableState.Finished).DefaultTimeout(TestConstants.LongTimeoutDuration); Assert.NotNull(serviceA); - await applicationExecutor.StartResourceAsync(serviceA.Metadata.Name, token); + await applicationExecutor.StartResourceAsync(serviceA.Metadata.Name, CancellationToken.None).DefaultTimeout(); - serviceA = await KubernetesHelper.GetResourceByNameMatchAsync(kubernetes, executablePattern, r => r.Status?.State == ExecutableState.Running, token); + serviceA = await KubernetesHelper.GetResourceByNameMatchAsync(kubernetes, executablePattern, r => r.Status?.State == ExecutableState.Running).DefaultTimeout(TestConstants.LongTimeoutDuration); Assert.NotNull(serviceA); - await app.StopAsync(); + await app.StopAsync().DefaultTimeout(); } [Fact] @@ -366,19 +358,16 @@ public async Task SpecifyingEnvPortInEndpointFlowsToEnv() var kubernetes = app.Services.GetRequiredService(); - await app.StartAsync(); - - using var cts = new CancellationTokenSource(Debugger.IsAttached ? Timeout.InfiniteTimeSpan : TimeSpan.FromMinutes(1)); - var token = cts.Token; + await app.StartAsync().DefaultTimeout(); var suffix = app.Services.GetRequiredService>().Value.ResourceNameSuffix; - var redisContainer = await KubernetesHelper.GetResourceByNameMatchAsync(kubernetes, $"redis0-{ReplicaIdRegex}-{suffix}", r => r.Status?.EffectiveEnv is not null, token); + var redisContainer = await KubernetesHelper.GetResourceByNameMatchAsync(kubernetes, $"redis0-{ReplicaIdRegex}-{suffix}", r => r.Status?.EffectiveEnv is not null).DefaultTimeout(); Assert.NotNull(redisContainer); - var serviceA = await KubernetesHelper.GetResourceByNameAsync(kubernetes, "servicea", suffix!, r => r.Status?.EffectiveEnv is not null, token); + var serviceA = await KubernetesHelper.GetResourceByNameAsync(kubernetes, "servicea", suffix!, r => r.Status?.EffectiveEnv is not null).DefaultTimeout(); Assert.NotNull(serviceA); - var nodeApp = await KubernetesHelper.GetResourceByNameMatchAsync(kubernetes, $"nodeapp-{ReplicaIdRegex}-{suffix}", r => r.Status?.EffectiveEnv is not null, token); + var nodeApp = await KubernetesHelper.GetResourceByNameMatchAsync(kubernetes, $"nodeapp-{ReplicaIdRegex}-{suffix}", r => r.Status?.EffectiveEnv is not null).DefaultTimeout(); Assert.NotNull(nodeApp); Assert.Equal("redis:latest", redisContainer.Spec.Image); @@ -395,7 +384,7 @@ public async Task SpecifyingEnvPortInEndpointFlowsToEnv() Assert.False(string.IsNullOrEmpty(nodeAppPortValue)); Assert.NotEqual(0, int.Parse(nodeAppPortValue, CultureInfo.InvariantCulture)); - await app.StopAsync(); + await app.StopAsync().DefaultTimeout(); static string? GetEnv(IEnumerable? envVars, string name) { @@ -422,13 +411,10 @@ public async Task StartAsync_DashboardAuthConfig_PassedToDashboardProcess() var kubernetes = app.Services.GetRequiredService(); - await app.StartAsync(); - - using var cts = new CancellationTokenSource(Debugger.IsAttached ? Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(10)); - var token = cts.Token; + await app.StartAsync().DefaultTimeout(); var suffix = app.Services.GetRequiredService>().Value.ResourceNameSuffix; - var aspireDashboard = await KubernetesHelper.GetResourceByNameMatchAsync(kubernetes, $"aspire-dashboard-{ReplicaIdRegex}-{suffix}", r => r.Status?.EffectiveEnv is not null, token); + var aspireDashboard = await KubernetesHelper.GetResourceByNameMatchAsync(kubernetes, $"aspire-dashboard-{ReplicaIdRegex}-{suffix}", r => r.Status?.EffectiveEnv is not null).DefaultTimeout(); Assert.NotNull(aspireDashboard); Assert.Equal("BrowserToken", GetEnv(aspireDashboard.Spec.Env, "DASHBOARD__FRONTEND__AUTHMODE")); @@ -438,7 +424,7 @@ public async Task StartAsync_DashboardAuthConfig_PassedToDashboardProcess() var keyBytes = Convert.FromHexString(GetEnv(aspireDashboard.Spec.Env, "DASHBOARD__OTLP__PRIMARYAPIKEY")!); Assert.Equal(16, keyBytes.Length); - await app.StopAsync(); + await app.StopAsync().DefaultTimeout(); static string? GetEnv(IEnumerable? envVars, string name) { @@ -464,19 +450,16 @@ public async Task StartAsync_UnsecuredAllowAnonymous_PassedToDashboardProcess() var kubernetes = app.Services.GetRequiredService(); - await app.StartAsync(); - - using var cts = new CancellationTokenSource(Debugger.IsAttached ? Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(10)); - var token = cts.Token; + await app.StartAsync().DefaultTimeout(); var suffix = app.Services.GetRequiredService>().Value.ResourceNameSuffix; - var aspireDashboard = await KubernetesHelper.GetResourceByNameMatchAsync(kubernetes, $"aspire-dashboard-{ReplicaIdRegex}-{suffix}", r => r.Status?.EffectiveEnv is not null, token); + var aspireDashboard = await KubernetesHelper.GetResourceByNameMatchAsync(kubernetes, $"aspire-dashboard-{ReplicaIdRegex}-{suffix}", r => r.Status?.EffectiveEnv is not null).DefaultTimeout(); Assert.NotNull(aspireDashboard); Assert.Equal("Unsecured", GetEnv(aspireDashboard.Spec.Env, "DASHBOARD__FRONTEND__AUTHMODE")); Assert.Equal("Unsecured", GetEnv(aspireDashboard.Spec.Env, "DASHBOARD__OTLP__AUTHMODE")); - await app.StopAsync(); + await app.StopAsync().DefaultTimeout(); static string? GetEnv(IEnumerable? envVars, string name) { @@ -498,23 +481,19 @@ public async Task VerifyDockerWithEntrypointWorks() await using var app = testProgram.Build(); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); var s = app.Services.GetRequiredService(); - using var cts = new CancellationTokenSource(Debugger.IsAttached ? Timeout.InfiniteTimeSpan : TimeSpan.FromMinutes(1)); - var token = cts.Token; - var suffix = app.Services.GetRequiredService>().Value.ResourceNameSuffix; var redisContainer = await KubernetesHelper.GetResourceByNameMatchAsync(s, $"redis-cli-{ReplicaIdRegex}-{suffix}", - r => r.Status?.State == ContainerState.FailedToStart && (r.Status?.Message.Contains("bob") ?? false), - token); + r => r.Status?.State == ContainerState.FailedToStart && (r.Status?.Message.Contains("bob") ?? false)).DefaultTimeout(TestConstants.LongTimeoutDuration); Assert.NotNull(redisContainer); Assert.Equal("redis:latest", redisContainer.Spec.Image); Assert.Equal("bob", redisContainer.Spec.Command); - await app.StopAsync(); + await app.StopAsync().DefaultTimeout(); } [Fact] @@ -531,24 +510,20 @@ public async Task VerifyDockerWithBindMountWorksWithAbsolutePaths() await using var app = testProgram.Build(); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); var s = app.Services.GetRequiredService(); - using var cts = new CancellationTokenSource(Debugger.IsAttached ? Timeout.InfiniteTimeSpan : TimeSpan.FromMinutes(1)); - var token = cts.Token; - var suffix = app.Services.GetRequiredService>().Value.ResourceNameSuffix; var redisContainer = await KubernetesHelper.GetResourceByNameMatchAsync( s, - $"redis-cli-{ReplicaIdRegex}-{suffix}", r => r.Spec.VolumeMounts != null, - token); + $"redis-cli-{ReplicaIdRegex}-{suffix}", r => r.Spec.VolumeMounts != null).DefaultTimeout(); Assert.NotNull(redisContainer.Spec.VolumeMounts); Assert.NotEmpty(redisContainer.Spec.VolumeMounts); Assert.Equal(sourcePath, redisContainer.Spec.VolumeMounts[0].Source); - await app.StopAsync(); + await app.StopAsync().DefaultTimeout(); } [Fact] @@ -564,25 +539,21 @@ public async Task VerifyDockerWithBindMountWorksWithRelativePaths() await using var app = testProgram.Build(); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); var s = app.Services.GetRequiredService(); - using var cts = new CancellationTokenSource(Debugger.IsAttached ? Timeout.InfiniteTimeSpan : TimeSpan.FromMinutes(1)); - var token = cts.Token; - var suffix = app.Services.GetRequiredService>().Value.ResourceNameSuffix; var redisContainer = await KubernetesHelper.GetResourceByNameMatchAsync( s, - $"redis-cli-{ReplicaIdRegex}-{suffix}", r => r.Spec.VolumeMounts != null, - token); + $"redis-cli-{ReplicaIdRegex}-{suffix}", r => r.Spec.VolumeMounts != null).DefaultTimeout(); Assert.NotNull(redisContainer.Spec.VolumeMounts); Assert.NotEmpty(redisContainer.Spec.VolumeMounts); Assert.NotEqual("etc/path-here", redisContainer.Spec.VolumeMounts[0].Source); Assert.True(Path.IsPathRooted(redisContainer.Spec.VolumeMounts[0].Source)); - await app.StopAsync(); + await app.StopAsync().DefaultTimeout(); } [Fact] @@ -598,24 +569,20 @@ public async Task VerifyDockerWithVolumeWorksWithName() await using var app = testProgram.Build(); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); var s = app.Services.GetRequiredService(); - using var cts = new CancellationTokenSource(Debugger.IsAttached ? Timeout.InfiniteTimeSpan : TimeSpan.FromMinutes(1)); - var token = cts.Token; - var suffix = app.Services.GetRequiredService>().Value.ResourceNameSuffix; var redisContainer = await KubernetesHelper.GetResourceByNameMatchAsync( s, - $"redis-cli-{ReplicaIdRegex}-{suffix}", r => r.Spec.VolumeMounts != null, - token); + $"redis-cli-{ReplicaIdRegex}-{suffix}", r => r.Spec.VolumeMounts != null).DefaultTimeout(); Assert.NotNull(redisContainer.Spec.VolumeMounts); Assert.NotEmpty(redisContainer.Spec.VolumeMounts); Assert.Equal("test-volume-name", redisContainer.Spec.VolumeMounts[0].Source); - await app.StopAsync(); + await app.StopAsync().DefaultTimeout(); } [Fact] @@ -628,13 +595,10 @@ public async Task KubernetesHasResourceNameForContainersAndExes() await using var app = testProgram.Build(); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); var s = app.Services.GetRequiredService(); - using var cts = new CancellationTokenSource(Debugger.IsAttached ? Timeout.InfiniteTimeSpan : TimeSpan.FromMinutes(1)); - var token = cts.Token; - var expectedExeResources = new HashSet() { "servicea", @@ -652,7 +616,7 @@ public async Task KubernetesHasResourceNameForContainersAndExes() "eventhubns" }; - await foreach (var resource in s.WatchAsync(cancellationToken: token)) + await foreach (var resource in s.WatchAsync().DefaultTimeout()) { Assert.True(resource.Item2.Metadata.Annotations.TryGetValue(Container.ResourceNameAnnotation, out var value)); if (expectedContainerResources.Contains(value)) @@ -666,7 +630,7 @@ public async Task KubernetesHasResourceNameForContainersAndExes() } } - await foreach (var resource in s.WatchAsync(cancellationToken: token)) + await foreach (var resource in s.WatchAsync().DefaultTimeout()) { Assert.True(resource.Item2.Metadata.Annotations.TryGetValue(Executable.ResourceNameAnnotation, out var value)); if (expectedExeResources.Contains(value)) @@ -694,7 +658,7 @@ public async Task ReplicasAndProxylessEndpointThrows() await using var app = testProgram.Build(); - var ex = await Assert.ThrowsAsync(() => app.StartAsync()); + var ex = await Assert.ThrowsAsync(() => app.StartAsync()).DefaultTimeout(); var suffix = app.Services.GetRequiredService>().Value.ResourceNameSuffix; Assert.Equal($"Resource 'servicea-{suffix}' uses multiple replicas and a proxy-less endpoint 'http'. These features do not work together.", ex.Message); } @@ -713,7 +677,7 @@ public async Task ProxylessEndpointWithoutPortThrows() await using var app = testProgram.Build(); - var ex = await Assert.ThrowsAsync(() => app.StartAsync()); + var ex = await Assert.ThrowsAsync(() => app.StartAsync()).DefaultTimeout(); var suffix = app.Services.GetRequiredService>().Value.ResourceNameSuffix; Assert.Equal($"Service 'servicea-{suffix}' needs to specify a port for endpoint 'http' since it isn't using a proxy.", ex.Message); } @@ -734,20 +698,23 @@ public async Task ProxylessEndpointWorks() testProgram.AppBuilder.Services.AddLogging(b => b.AddXunit(_testOutputHelper)); await using var app = testProgram.Build(); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); var client = app.CreateHttpClientWithResilience("servicea", "http"); - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - var result = await client.GetStringAsync("pid", cts.Token); + var result = await client.GetStringAsync("pid").DefaultTimeout(TestConstants.LongTimeoutDuration); Assert.NotNull(result); // Check that endpoint from launchsettings doesn't work await Assert.ThrowsAnyAsync(async () => { - using var client2 = new HttpClient(); + using var client2 = new HttpClient(new SocketsHttpHandler + { + // Provide a timeout to avoid long timeout while trying to connect. + ConnectTimeout = TimeSpan.FromSeconds(2) + }); await client2.GetStringAsync("http://localhost:5156/pid"); - }); + }).DefaultTimeout(); } [Fact] @@ -773,9 +740,10 @@ public async Task ProxylessAndProxiedEndpointBothWorkOnSameResource() await using var app = testProgram.Build(); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); - var token = new CancellationTokenSource(TimeSpan.FromMinutes(1)).Token; + using var cts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration); + var token = cts.Token; var urls = string.Empty; var httpEndPoint = app.GetEndpoint(testProgram.ServiceABuilder.Resource.Name, endpointName: "http"); @@ -804,7 +772,7 @@ public async Task ProxylessAndProxiedEndpointBothWorkOnSameResource() try { using var client = new HttpClient(); - var value = await client.GetStringAsync($"{httpsEndpoint}urls", token); + var value = await client.GetStringAsync($"{httpsEndpoint}urls", token).DefaultTimeout(); Assert.Equal(urls, value); break; } @@ -837,17 +805,17 @@ public async Task ProxylessContainerCanBeReferenced() .WithReference(redisNoPort); using var app = builder.Build(); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); // Wait for the application to be ready - await app.WaitForTextAsync("Application started.").WaitAsync(TimeSpan.FromMinutes(1)); + await app.WaitForTextAsync("Application started.").DefaultTimeout(); // Wait until the service itself starts. using var clientA = app.CreateHttpClient(servicea.Resource.Name, "http"); - await clientA.GetStringAsync("/"); + await clientA.GetStringAsync("/").DefaultTimeout(); var s = app.Services.GetRequiredService(); - var exeList = await s.ListAsync(); + var exeList = await s.ListAsync().DefaultTimeout(); var suffix = app.Services.GetRequiredService>().Value.ResourceNameSuffix; Assert.NotNull(suffix); @@ -855,7 +823,7 @@ public async Task ProxylessContainerCanBeReferenced() var env = Assert.Single(service.Spec.Env!.Where(e => e.Name == "ConnectionStrings__redis")); Assert.Equal("localhost:1234", env.Value); - var list = await s.ListAsync(); + var list = await s.ListAsync().DefaultTimeout(); var redisContainer = Assert.Single(list.Where(c => Regex.IsMatch(c.Name(),$"redis-{ReplicaIdRegex}-{suffix}"))) ; Assert.Equal(1234, Assert.Single(redisContainer.Spec.Ports!).HostPort); @@ -865,7 +833,7 @@ public async Task ProxylessContainerCanBeReferenced() var otherRedisContainer = Assert.Single(list.Where(c => Regex.IsMatch(c.Name(), $"redisNoPort-{ReplicaIdRegex}-{suffix}"))); Assert.Equal(6379, Assert.Single(otherRedisContainer.Spec.Ports!).HostPort); - await app.StopAsync(); + await app.StopAsync().DefaultTimeout(); } [Fact] @@ -882,7 +850,7 @@ public async Task ProxylessContainerWithoutPortThrows() using var app = builder.Build(); - var ex = await Assert.ThrowsAsync(() => app.StartAsync()); + var ex = await Assert.ThrowsAsync(() => app.StartAsync()).DefaultTimeout(); var suffix = app.Services.GetRequiredService>().Value.ResourceNameSuffix; Assert.Equal($"The endpoint 'tcp' for container resource 'dummyRedis-{suffix}' must specify the TargetPort value", ex.Message); } @@ -904,9 +872,9 @@ public async Task AfterResourcesCreatedLifecycleHookWorks() var kubernetesLifecycle = (KubernetesTestLifecycleHook)lifecycles.Where(l => l.GetType() == typeof(KubernetesTestLifecycleHook)).First(); kubernetesLifecycle.KubernetesService = s; - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); - await kubernetesLifecycle.HooksCompleted; + await kubernetesLifecycle.HooksCompleted.DefaultTimeout(); } private sealed class KubernetesTestLifecycleHook : IDistributedApplicationLifecycleHook diff --git a/tests/Aspire.Hosting.Tests/Eventing/DistributedApplicationBuilderEventingTests.cs b/tests/Aspire.Hosting.Tests/Eventing/DistributedApplicationBuilderEventingTests.cs index 11b9c6fe97..591963386e 100644 --- a/tests/Aspire.Hosting.Tests/Eventing/DistributedApplicationBuilderEventingTests.cs +++ b/tests/Aspire.Hosting.Tests/Eventing/DistributedApplicationBuilderEventingTests.cs @@ -4,6 +4,7 @@ using Aspire.Components.Common.Tests; using Aspire.Hosting.Eventing; using Aspire.Hosting.Utils; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -35,10 +36,10 @@ public async Task EventsCanBePublishedBlockSequential() var pendingPublish = builder.Eventing.PublishAsync(new DummyEvent(), EventDispatchBehavior.BlockingSequential); - await blockAssertionTcs.Task; + await blockAssertionTcs.Task.DefaultTimeout(); Assert.Equal(1, hitCount); blockFirstSubscriptionTcs.SetResult(); - await pendingPublish; + await pendingPublish.DefaultTimeout(); Assert.Equal(2, hitCount); } @@ -68,10 +69,10 @@ public async Task EventsCanBePublishedBlockConcurrent() var pendingPublish = builder.Eventing.PublishAsync(new DummyEvent(), EventDispatchBehavior.BlockingConcurrent); - await Task.WhenAll(blockAssertionSub1.Task, blockAssertionSub2.Task); + await Task.WhenAll(blockAssertionSub1.Task, blockAssertionSub2.Task).DefaultTimeout(); Assert.Equal(2, hitCount); blockSubscriptionCompletion.SetResult(); - await pendingPublish; + await pendingPublish.DefaultTimeout(); } [Fact] @@ -98,11 +99,10 @@ public async Task EventsCanBePublishedNonBlockingConcurrent() blockAssertionSub2.SetResult(); }); - var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); // Should be way more than we need! - await builder.Eventing.PublishAsync(new DummyEvent(), EventDispatchBehavior.NonBlockingConcurrent, timeoutCts.Token); + await builder.Eventing.PublishAsync(new DummyEvent(), EventDispatchBehavior.NonBlockingConcurrent).DefaultTimeout(); blockSubscriptionExecution.SetResult(); - await Task.WhenAll(blockAssertionSub1.Task, blockAssertionSub2.Task); + await Task.WhenAll(blockAssertionSub1.Task, blockAssertionSub2.Task).DefaultTimeout(); Assert.Equal(2, hitCount); } @@ -134,12 +134,11 @@ public async Task EventsCanBePublishedNonBlockingSequential() return Task.CompletedTask; }); - var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); // Should be way more than we need! - await builder.Eventing.PublishAsync(new DummyEvent(), EventDispatchBehavior.NonBlockingSequential, timeoutCts.Token); + await builder.Eventing.PublishAsync(new DummyEvent(), EventDispatchBehavior.NonBlockingSequential).DefaultTimeout(); // Make sure that we are zero when we enter // the first handler. - await blockAssert1.Task; + await blockAssert1.Task.DefaultTimeout(); Assert.Equal(0, hitCount); // Give the second handler a chance to run, @@ -152,13 +151,13 @@ public async Task EventsCanBePublishedNonBlockingSequential() // we update the hit count and verify // that it has moved to 1. blockEventSub1.SetResult(); - await blockAssert2.Task; + await blockAssert2.Task.DefaultTimeout(); Assert.Equal(1, hitCount); blockEventSub2.SetResult(); // Now block until the second handler has // run and make sure it has incremented. - await blockAssert3.Task; + await blockAssert3.Task.DefaultTimeout(); Assert.Equal(2, hitCount); } @@ -175,7 +174,7 @@ public void CanResolveIDistributedApplicationEventingFromDI() [RequiresDocker] public async Task ResourceEventsForContainersFireForSpecificResources() { - var beforeResourceStartedEvent = new ManualResetEventSlim(); + var beforeResourceStartedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); using var builder = TestDistributedApplicationBuilder.Create(); var redis = builder.AddRedis("redis"); @@ -184,17 +183,16 @@ public async Task ResourceEventsForContainersFireForSpecificResources() { Assert.NotNull(e.Services); Assert.NotNull(e.Resource); - beforeResourceStartedEvent.Set(); + beforeResourceStartedTcs.TrySetResult(); return Task.CompletedTask; }); using var app = builder.Build(); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); - var fired = beforeResourceStartedEvent.Wait(TimeSpan.FromSeconds(10)); + await beforeResourceStartedTcs.Task.DefaultTimeout(); - Assert.True(fired); - await app.StopAsync(); + await app.StopAsync().DefaultTimeout(); } [Fact] diff --git a/tests/Aspire.Hosting.Tests/ExecutableResourceTests.cs b/tests/Aspire.Hosting.Tests/ExecutableResourceTests.cs index 794900eb99..c1462fbecc 100644 --- a/tests/Aspire.Hosting.Tests/ExecutableResourceTests.cs +++ b/tests/Aspire.Hosting.Tests/ExecutableResourceTests.cs @@ -3,6 +3,7 @@ using Aspire.Hosting.Tests.Utils; using Aspire.Hosting.Utils; +using Microsoft.AspNetCore.InternalTesting; using Xunit; namespace Aspire.Hosting.Tests; @@ -35,7 +36,7 @@ public async Task AddExecutableWithArgs() using var app = appBuilder.Build(); - var args = await ArgumentEvaluator.GetArgumentListAsync(exe2.Resource); + var args = await ArgumentEvaluator.GetArgumentListAsync(exe2.Resource).DefaultTimeout(); Assert.Collection(args, arg => Assert.Equal("app.py", arg), @@ -47,7 +48,7 @@ public async Task AddExecutableWithArgs() arg => Assert.Equal("anotherConnectionString", arg) ); - var manifest = await ManifestUtils.GetManifest(exe2.Resource); + var manifest = await ManifestUtils.GetManifest(exe2.Resource).DefaultTimeout(); var expectedManifest = """ diff --git a/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs b/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs index 03eefe5d5c..37e1c3566d 100644 --- a/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs +++ b/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs @@ -2,9 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Hosting.Tests.Utils; +using Microsoft.AspNetCore.InternalTesting; using Xunit; namespace Aspire.Hosting.Tests; + public class ExpressionResolverTests { [Theory] @@ -49,7 +51,7 @@ public async Task ExpressionResolverGeneratesCorrectStrings(string exprName, boo // First test ExpressionResolver directly var csRef = new ConnectionStringReference(target.Resource, false); - var connectionString = await ExpressionResolver.ResolveAsync(sourceIsContainer, csRef, "ContainerHostName", CancellationToken.None); + var connectionString = await ExpressionResolver.ResolveAsync(sourceIsContainer, csRef, "ContainerHostName", CancellationToken.None).DefaultTimeout(); Assert.Equal(expectedConnectionString, connectionString); // Then test it indirectly with a resource reference, which exercises a more complete code path @@ -60,7 +62,7 @@ public async Task ExpressionResolverGeneratesCorrectStrings(string exprName, boo source = source.WithImage("someimage"); } - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(source.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance, "ContainerHostName"); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(source.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance, "ContainerHostName").DefaultTimeout(); Assert.Equal(expectedConnectionString, config["ConnectionStrings__testresource"]); } @@ -92,7 +94,7 @@ public async Task HostUrlPropertyGetsResolved(bool container, string hostUrlVal, test = test.WithImage("someimage"); } - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(test.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance, "ContainerHostName"); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(test.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance, "ContainerHostName").DefaultTimeout(); Assert.Equal(expectedValue, config["envname"]); } @@ -111,7 +113,7 @@ public async Task HostUrlPropertyGetsResolvedInOtlpExporterEndpoint(bool contain test = test.WithImage("someimage"); } - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(test.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance, "ContainerHostName"); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(test.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance, "ContainerHostName").DefaultTimeout(); Assert.Equal(expectedValue, config["OTEL_EXPORTER_OTLP_ENDPOINT"]); } } diff --git a/tests/Aspire.Hosting.Tests/Health/ResourceHealthCheckServiceTests.cs b/tests/Aspire.Hosting.Tests/Health/ResourceHealthCheckServiceTests.cs index 507eef4cb3..70bff83252 100644 --- a/tests/Aspire.Hosting.Tests/Health/ResourceHealthCheckServiceTests.cs +++ b/tests/Aspire.Hosting.Tests/Health/ResourceHealthCheckServiceTests.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using Aspire.Hosting.Utils; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Xunit; @@ -18,17 +19,17 @@ public async Task ResourcesWithoutHealthCheck_HealthyWhenRunning() using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper); var resource = builder.AddResource(new ParentResource("resource")); - await using var app = await builder.BuildAsync(); + await using var app = await builder.BuildAsync().DefaultTimeout(); var rns = app.Services.GetRequiredService(); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); await rns.PublishUpdateAsync(resource.Resource, s => s with { State = new ResourceStateSnapshot(KnownResourceStates.Starting, null) - }); + }).DefaultTimeout(); - var startingEvent = await rns.WaitForResourceAsync("resource", e => e.Snapshot.State?.Text == KnownResourceStates.Starting); + var startingEvent = await rns.WaitForResourceAsync("resource", e => e.Snapshot.State?.Text == KnownResourceStates.Starting).DefaultTimeout(); Assert.Null(startingEvent.Snapshot.HealthStatus); await rns.PublishUpdateAsync(resource.Resource, s => s with @@ -36,10 +37,10 @@ await rns.PublishUpdateAsync(resource.Resource, s => s with State = new ResourceStateSnapshot(KnownResourceStates.Running, null) }); - var healthyEvent = await rns.WaitForResourceHealthyAsync("resource"); + var healthyEvent = await rns.WaitForResourceHealthyAsync("resource").DefaultTimeout(); Assert.Equal(HealthStatus.Healthy, healthyEvent.Snapshot.HealthStatus); - await app.StopAsync(); + await app.StopAsync().DefaultTimeout(); } [Fact] @@ -52,17 +53,17 @@ public async Task ResourcesWithHealthCheck_NotHealthyUntilCheckSucceeds() var resource = builder.AddResource(new ParentResource("resource")) .WithHealthCheck("healthcheck_a"); - await using var app = await builder.BuildAsync(); + await using var app = await builder.BuildAsync().DefaultTimeout(); var rns = app.Services.GetRequiredService(); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); await rns.PublishUpdateAsync(resource.Resource, s => s with { State = new ResourceStateSnapshot(KnownResourceStates.Starting, null) - }); + }).DefaultTimeout(); - var startingEvent = await rns.WaitForResourceAsync("resource", e => e.Snapshot.State?.Text == KnownResourceStates.Starting); + var startingEvent = await rns.WaitForResourceAsync("resource", e => e.Snapshot.State?.Text == KnownResourceStates.Starting).DefaultTimeout(); Assert.Null(startingEvent.Snapshot.HealthStatus); await rns.PublishUpdateAsync(resource.Resource, s => s with @@ -70,12 +71,12 @@ await rns.PublishUpdateAsync(resource.Resource, s => s with State = new ResourceStateSnapshot(KnownResourceStates.Running, null) }); - var runningEvent = await rns.WaitForResourceAsync("resource", e => e.Snapshot.State?.Text == KnownResourceStates.Running); + var runningEvent = await rns.WaitForResourceAsync("resource", e => e.Snapshot.State?.Text == KnownResourceStates.Running).DefaultTimeout(); Assert.Equal(HealthStatus.Unhealthy, runningEvent.Snapshot.HealthStatus); - await rns.WaitForResourceHealthyAsync("resource"); + await rns.WaitForResourceHealthyAsync("resource").DefaultTimeout(); - await app.StopAsync(); + await app.StopAsync().DefaultTimeout(); } [Fact] @@ -98,15 +99,15 @@ public async Task HealthCheckIntervalSlowsAfterSteadyHealthyState() using var app = builder.Build(); var rns = app.Services.GetRequiredService(); - var abortTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(120)); + var abortTokenSource = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration); - await app.StartAsync(abortTokenSource.Token); + await app.StartAsync(abortTokenSource.Token).DefaultTimeout(); await rns.PublishUpdateAsync(resource.Resource, s => s with { State = KnownResourceStates.Running - }); - await rns.WaitForResourceHealthyAsync(resource.Resource.Name, abortTokenSource.Token); + }).DefaultTimeout(); + await rns.WaitForResourceHealthyAsync(resource.Resource.Name, abortTokenSource.Token).DefaultTimeout(); are = new AutoResetEvent(false); @@ -120,7 +121,7 @@ await rns.PublishUpdateAsync(resource.Resource, s => s with // Delay is 30 seconds but we allow for a (ridiculous) 10 second margin of error. Assert.True(stopwatch.ElapsedMilliseconds > 20000); - await app.StopAsync(abortTokenSource.Token); + await app.StopAsync(abortTokenSource.Token).DefaultTimeout(); } [Fact] @@ -143,15 +144,15 @@ public async Task HealthCheckIntervalDoesNotSlowBeforeSteadyHealthyState() using var app = builder.Build(); var rns = app.Services.GetRequiredService(); - var abortTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(120)); + var abortTokenSource = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration); - await app.StartAsync(abortTokenSource.Token); + await app.StartAsync(abortTokenSource.Token).DefaultTimeout(); await rns.PublishUpdateAsync(resource.Resource, s => s with { State = KnownResourceStates.Running - }); - await rns.WaitForResourceAsync(resource.Resource.Name, KnownResourceStates.Running, abortTokenSource.Token); + }).DefaultTimeout(); + await rns.WaitForResourceAsync(resource.Resource.Name, KnownResourceStates.Running, abortTokenSource.Token).DefaultTimeout(); are = new AutoResetEvent(false); @@ -167,7 +168,7 @@ await rns.PublishUpdateAsync(resource.Resource, s => s with // the 30 second slow path. Assert.True(stopwatch.ElapsedMilliseconds < 10000); - await app.StopAsync(abortTokenSource.Token); + await app.StopAsync(abortTokenSource.Token).DefaultTimeout(); } [Fact] @@ -190,13 +191,13 @@ public async Task ResourcesWithoutHealthCheckAnnotationsGetReadyEventFired() await rns.PublishUpdateAsync(resource.Resource, s => s with { State = new ResourceStateSnapshot(KnownResourceStates.Running, null) - }); + }).DefaultTimeout(); - var @event = await blockAssert.Task; + var @event = await blockAssert.Task.DefaultTimeout(); Assert.Equal(resource.Resource, @event.Resource); - await pendingStart; - await app.StopAsync(); + await pendingStart.DefaultTimeout(); + await app.StopAsync().DefaultTimeout(); } [Fact] @@ -217,15 +218,14 @@ public async Task PoorlyImplementedHealthChecksDontCauseMonitoringLoopToCrashout using var app = builder.Build(); var rns = app.Services.GetRequiredService(); - var abortTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(120)); - var pendingStart = app.StartAsync(abortTokenSource.Token); + var pendingStart = app.StartAsync().DefaultTimeout(); await rns.PublishUpdateAsync(resource.Resource, s => s with { State = new ResourceStateSnapshot(KnownResourceStates.Running, null) - }); + }).DefaultTimeout(); - while (!abortTokenSource.Token.IsCancellationRequested) + while (!pendingStart.IsCanceled) { if (hitCount > 2) { @@ -234,8 +234,8 @@ await rns.PublishUpdateAsync(resource.Resource, s => s with await Task.Delay(100); } - await pendingStart; - await app.StopAsync(); + await pendingStart; // already has a timeout + await app.StopAsync().DefaultTimeout(); } [Fact] @@ -264,12 +264,8 @@ public async Task ResourceHealthCheckServiceDoesNotRunHealthChecksUnlessResource return Task.CompletedTask; }); - // Make sure that this test doesn't run longer than a minute (should finish in a second or two) - // but allow enough time to debug things without having to adjust timings. - var abortTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(120)); - using var app = builder.Build(); - var pendingStart = app.StartAsync(abortTokenSource.Token); + var pendingStart = app.StartAsync().DefaultTimeout(TestConstants.LongTimeoutDuration); var rns = app.Services.GetRequiredService(); // Verify that the health check does not get run before we move the resource into the @@ -277,7 +273,7 @@ public async Task ResourceHealthCheckServiceDoesNotRunHealthChecksUnlessResource // so I'm just going to spin for up to ten seconds to be sure that no local perf // issues lead to a false pass here. var giveUpAfter = DateTime.Now.AddSeconds(10); - while (!abortTokenSource.Token.IsCancellationRequested) + while (!pendingStart.IsCanceled) { Assert.Equal(0, hitCount); await Task.Delay(100); @@ -287,20 +283,19 @@ public async Task ResourceHealthCheckServiceDoesNotRunHealthChecksUnlessResource break; } } - Assert.False(abortTokenSource.IsCancellationRequested); await rns.PublishUpdateAsync(parent.Resource, s => s with { State = new ResourceStateSnapshot(KnownResourceStates.Running, null) - }); + }).DefaultTimeout(); // Wait for the ResourceReadyEvent checkStatus = HealthCheckResult.Healthy(); - await Task.WhenAll([resourceReadyEventFired.Task]); + await Task.WhenAll([resourceReadyEventFired.Task]).DefaultTimeout(); Assert.True(hitCount > 0); - await pendingStart; - await app.StopAsync(); + await pendingStart; // already has a timeout + await app.StopAsync().DefaultTimeout(); } [Fact] @@ -329,21 +324,17 @@ public async Task ResourceHealthCheckServiceOnlyRaisesResourceReadyOnce() return Task.CompletedTask; }); - // Make sure that this test doesn't run longer than a minute (should finish in a second or two) - // but allow enough time to debug things without having to adjust timings. - var abortTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(120)); - using var app = builder.Build(); - var pendingStart = app.StartAsync(abortTokenSource.Token); + var pendingStart = app.StartAsync().DefaultTimeout(); var rns = app.Services.GetRequiredService(); // Get the custom resource to a running state. await rns.PublishUpdateAsync(parent.Resource, s => s with { State = new ResourceStateSnapshot(KnownResourceStates.Running, null) - }); + }).DefaultTimeout(); - while (!abortTokenSource.Token.IsCancellationRequested) + while (!pendingStart.IsCanceled) { // We wait for this hit count to reach 3 // because it means that we've had a chance @@ -355,11 +346,10 @@ await rns.PublishUpdateAsync(parent.Resource, s => s with await Task.Delay(100); } - Assert.False(abortTokenSource.IsCancellationRequested); Assert.Equal(1, eventHits); - await pendingStart; - await app.StopAsync(); + await pendingStart; // already has a timeout + await app.StopAsync().DefaultTimeout(); } [Fact] @@ -395,7 +385,7 @@ public async Task VerifyThatChildResourceWillBecomeHealthyOnceParentBecomesHealt await rns.PublishUpdateAsync(parent.Resource, s => s with { State = new ResourceStateSnapshot(KnownResourceStates.Running, null) - }); + }).DefaultTimeout(); // ... only need to do this with custom resources, for containers this // is handled by app executor. When we get operators we won't need to do @@ -403,16 +393,16 @@ await rns.PublishUpdateAsync(parent.Resource, s => s with await rns.PublishUpdateAsync(child.Resource, s => s with { State = new ResourceStateSnapshot(KnownResourceStates.Running, null) - }); + }).DefaultTimeout(); - var parentReadyEvent = await parentReady.Task; + var parentReadyEvent = await parentReady.Task.DefaultTimeout(); Assert.Equal(parentReadyEvent.Resource, parent.Resource); - var childReadyEvent = await childReady.Task; + var childReadyEvent = await childReady.Task.DefaultTimeout(); Assert.Equal(childReadyEvent.Resource, child.Resource); - await pendingStart; - await app.StopAsync(); + await pendingStart; // already has a timeout + await app.StopAsync().DefaultTimeout(); } private sealed class ParentResource(string name) : Resource(name) diff --git a/tests/Aspire.Hosting.Tests/HealthCheckTests.cs b/tests/Aspire.Hosting.Tests/HealthCheckTests.cs index b3ecf72598..f36616e9ab 100644 --- a/tests/Aspire.Hosting.Tests/HealthCheckTests.cs +++ b/tests/Aspire.Hosting.Tests/HealthCheckTests.cs @@ -3,6 +3,7 @@ using Aspire.Components.Common.Tests; using Aspire.Hosting.Utils; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; @@ -28,7 +29,7 @@ public async Task WithHttpHealthCheckThrowsIfReferencingEndpointThatIsNotHttpSch var ex = await Assert.ThrowsAsync(async () => { await app.StartAsync(); - }); + }).DefaultTimeout(); Assert.Equal( "The endpoint 'nonhttp' on resource 'resource' was not using the 'http' scheme.", @@ -50,7 +51,7 @@ public async Task WithHttpsHealthCheckThrowsIfReferencingEndpointThatIsNotHttpsS var ex = await Assert.ThrowsAsync(async () => { await app.StartAsync(); - }); + }).DefaultTimeout(); Assert.Equal( "The endpoint 'nonhttp' on resource 'resource' was not using the 'https' scheme.", @@ -62,7 +63,6 @@ public async Task WithHttpsHealthCheckThrowsIfReferencingEndpointThatIsNotHttpsS [RequiresDocker] public async Task VerifyWithHttpHealthCheckBlocksDependentResources() { - var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10)); using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper); var healthCheckTcs = new TaskCompletionSource(); @@ -81,23 +81,23 @@ public async Task VerifyWithHttpHealthCheckBlocksDependentResources() using var app = builder.Build(); - var pendingStart = app.StartAsync(cts.Token); + var pendingStart = app.StartAsync(); var rns = app.Services.GetRequiredService(); - await rns.WaitForResourceAsync(resource.Resource.Name, KnownResourceStates.Running, cts.Token); + await rns.WaitForResourceAsync(resource.Resource.Name, KnownResourceStates.Running).DefaultTimeout(TestConstants.LongTimeoutDuration); - await rns.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Waiting, cts.Token); + await rns.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Waiting).DefaultTimeout(TestConstants.LongTimeoutDuration); healthCheckTcs.SetResult(HealthCheckResult.Healthy()); - await rns.WaitForResourceHealthyAsync(resource.Resource.Name, cts.Token); + await rns.WaitForResourceHealthyAsync(resource.Resource.Name).DefaultTimeout(TestConstants.LongTimeoutDuration); - await rns.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Running, cts.Token); + await rns.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Running).DefaultTimeout(TestConstants.LongTimeoutDuration); - await pendingStart; + await pendingStart.DefaultTimeout(); - await app.StopAsync(); + await app.StopAsync().DefaultTimeout(); } [Fact] @@ -117,7 +117,7 @@ public async Task BuildThrowsOnMissingHealthCheckRegistration() var ex = await Assert.ThrowsAsync(async () => { await app.StartAsync(); - }); + }).DefaultTimeout(); Assert.Equal("A health check registration is missing. Check logs for more details.", ex.Message); diff --git a/tests/Aspire.Hosting.Tests/Helpers/KubernetesHelper.cs b/tests/Aspire.Hosting.Tests/Helpers/KubernetesHelper.cs index 816f214781..645290e4a9 100644 --- a/tests/Aspire.Hosting.Tests/Helpers/KubernetesHelper.cs +++ b/tests/Aspire.Hosting.Tests/Helpers/KubernetesHelper.cs @@ -10,7 +10,7 @@ namespace Aspire.Hosting.Tests.Helpers; internal static class KubernetesHelper { - public static async Task GetResourceByNameAsync(IKubernetesService kubernetes, string resourceName, string resourceNameSuffix, Func ready, CancellationToken cancellationToken) where T : CustomResource + public static async Task GetResourceByNameAsync(IKubernetesService kubernetes, string resourceName, string resourceNameSuffix, Func ready, CancellationToken cancellationToken = default) where T : CustomResource { await foreach (var (_, r) in kubernetes.WatchAsync(cancellationToken: cancellationToken)) { @@ -25,7 +25,7 @@ public static async Task GetResourceByNameAsync(IKubernetesService kuberne throw new InvalidOperationException($"Resource {resourceName}, not ready"); } - public static async Task GetResourceByNameMatchAsync(IKubernetesService kubernetes, string resourceNamePattern, Func ready, CancellationToken cancellationToken) where T : CustomResource + public static async Task GetResourceByNameMatchAsync(IKubernetesService kubernetes, string resourceNamePattern, Func ready, CancellationToken cancellationToken = default) where T : CustomResource { await foreach (var (_, r) in kubernetes.WatchAsync(cancellationToken: cancellationToken)) { diff --git a/tests/Aspire.Hosting.Tests/KestrelConfigTests.cs b/tests/Aspire.Hosting.Tests/KestrelConfigTests.cs index 522502f183..5419dd6e0b 100644 --- a/tests/Aspire.Hosting.Tests/KestrelConfigTests.cs +++ b/tests/Aspire.Hosting.Tests/KestrelConfigTests.cs @@ -3,6 +3,7 @@ using Aspire.Hosting.Tests.Utils; using Aspire.Hosting.Utils; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -37,7 +38,7 @@ public async Task SingleKestrelHttpEndpointIsNamedHttpAndOverridesProfile() AllocateTestEndpoints(resource); - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); // When using Kestrel, we should not be setting ASPNETCORE_URLS at all Assert.False(config.ContainsKey("ASPNETCORE_URLS")); @@ -75,7 +76,7 @@ public async Task KestrelHttpEndpointsAreIgnoredWhenFlagIsSet() AllocateTestEndpoints(resource); - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); // We're ignoring Kestrel, so we should be setting ASPNETCORE_URLS Assert.Equal("http://localhost:port_http;http://localhost:port_ExplicitHttp", config["ASPNETCORE_URLS"]); @@ -140,7 +141,7 @@ public async Task ExplicitEndpointsResultInKestrelOverridesAtRuntime() AllocateTestEndpoints(resource); - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); Assert.Collection( config.Where(envVar => envVar.Key.StartsWith("Kestrel__")), @@ -173,7 +174,7 @@ public async Task VerifyKestrelEndpointManifestGeneration() { var resource = CreateTestProjectResource(); - var manifest = await ManifestUtils.GetManifest(resource); + var manifest = await ManifestUtils.GetManifest(resource).DefaultTimeout(); var expectedManifest = """ { @@ -217,7 +218,7 @@ public async Task VerifyMultipleKestrelEndpointsManifestGeneration() builder.WithHttpEndpoint(5018, name: "ExplicitNoProxyHttp", isProxied: false); }); - var manifest = await ManifestUtils.GetManifest(resource); + var manifest = await ManifestUtils.GetManifest(resource).DefaultTimeout(); // Note that unlike in Run mode, SecondHttpEndpoint is using host * instead of localhost var expectedManifest = """ @@ -288,7 +289,7 @@ public async Task VerifyKestrelEnvVariablesGetOmittedFromManifestIfExcluded() .WithEndpointsInEnvironment(e => e.Name != "ExplicitProxiedHttp"); }); - var manifest = await ManifestUtils.GetManifest(resource); + var manifest = await ManifestUtils.GetManifest(resource).DefaultTimeout(); var expectedEnv = """ { @@ -310,7 +311,7 @@ public async Task VerifyEndpointLevelKestrelProtocol() var resource = CreateTestProjectResource( operation: DistributedApplicationOperation.Publish); - var manifest = await ManifestUtils.GetManifest(resource); + var manifest = await ManifestUtils.GetManifest(resource).DefaultTimeout(); var expectedBindings = """ { diff --git a/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs b/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs index 43a0d1c7d2..0b47c442cd 100644 --- a/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs +++ b/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs @@ -8,6 +8,7 @@ using Aspire.Hosting.Redis; using Aspire.Hosting.Tests.Helpers; using Aspire.Hosting.Utils; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -96,7 +97,7 @@ public async Task WithContainerRegistryUpdatesContainerImageAnnotationsDuringPub var redis = builder.AddContainer("redis", "redis"); builder.Build().Run(); - var redisManifest = await ManifestUtils.GetManifest(redis.Resource); + var redisManifest = await ManifestUtils.GetManifest(redis.Resource).DefaultTimeout(); var expectedManifest = $$""" { "type": "container.v0", @@ -437,7 +438,7 @@ public void VerifyTestProgramFullManifest() "redis": { "type": "container.v0", "connectionString": "{redis.bindings.tcp.host}:{redis.bindings.tcp.port}", - "image": "{{TestConstants.AspireTestContainerRegistry}}/{{RedisContainerImageTags.Image}}:{{RedisContainerImageTags.Tag}}", + "image": "{{ComponentTestConstants.AspireTestContainerRegistry}}/{{RedisContainerImageTags.Image}}:{{RedisContainerImageTags.Tag}}", "bindings": { "tcp": { "scheme": "tcp", @@ -450,7 +451,7 @@ public void VerifyTestProgramFullManifest() "postgres": { "type": "container.v0", "connectionString": "Host={postgres.bindings.tcp.host};Port={postgres.bindings.tcp.port};Username=postgres;Password={postgres-password.value}", - "image": "{{TestConstants.AspireTestContainerRegistry}}/{{PostgresContainerImageTags.Image}}:{{PostgresContainerImageTags.Tag}}", + "image": "{{ComponentTestConstants.AspireTestContainerRegistry}}/{{PostgresContainerImageTags.Image}}:{{PostgresContainerImageTags.Tag}}", "env": { "POSTGRES_HOST_AUTH_METHOD": "scram-sha-256", "POSTGRES_INITDB_ARGS": "--auth-host=scram-sha-256 --auth-local=scram-sha-256", @@ -552,7 +553,7 @@ public async Task ParameterInputDefaultValuesGenerateCorrectly() } """; - var manifest = await ManifestUtils.GetManifest(param.Resource); + var manifest = await ManifestUtils.GetManifest(param.Resource).DefaultTimeout(); Assert.Equal(expectedManifest, manifest.ToString()); } diff --git a/tests/Aspire.Hosting.Tests/ProjectResourceTests.cs b/tests/Aspire.Hosting.Tests/ProjectResourceTests.cs index 746612ff81..17fb959465 100644 --- a/tests/Aspire.Hosting.Tests/ProjectResourceTests.cs +++ b/tests/Aspire.Hosting.Tests/ProjectResourceTests.cs @@ -6,6 +6,7 @@ using Aspire.Hosting.Tests.Helpers; using Aspire.Hosting.Tests.Utils; using Aspire.Hosting.Utils; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -17,7 +18,7 @@ public class ProjectResourceTests [Fact] public async Task AddProjectWithInvalidLaunchSettingsShouldThrowSpecificError() { - var projectDetails = await PrepareProjectWithMalformedLaunchSettingsAsync(); + var projectDetails = await PrepareProjectWithMalformedLaunchSettingsAsync().DefaultTimeout(); var ex = Assert.Throws(() => { @@ -46,10 +47,10 @@ public async Task AddProjectWithInvalidLaunchSettingsShouldThrowSpecificError() var launchSettingsFilePath = Path.Combine(propertiesDirectoryPath, "launchSettings.json"); Directory.CreateDirectory(projectDirectoryPath); - await File.WriteAllTextAsync(projectFilePath, csProjContent); + await File.WriteAllTextAsync(projectFilePath, csProjContent).DefaultTimeout(); Directory.CreateDirectory(propertiesDirectoryPath); - await File.WriteAllTextAsync(launchSettingsFilePath, launchSettingsContent); + await File.WriteAllTextAsync(launchSettingsFilePath, launchSettingsContent).DefaultTimeout(); return (projectFilePath, launchSettingsFilePath); } @@ -74,7 +75,7 @@ public async Task AddProjectAddsEnvironmentVariablesAndServiceMetadata() var serviceMetadata = Assert.Single(resource.Annotations.OfType()); Assert.IsType(serviceMetadata); - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); Assert.Collection(config, env => @@ -185,7 +186,7 @@ public async Task AddProjectAddsEnvironmentVariablesAndServiceMetadata_OtlpAuthD var resource = Assert.Single(projectResources); Assert.Equal("projectName", resource.Name); - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); if (hasHeader) { @@ -314,7 +315,7 @@ public async Task AspNetCoreUrlsNotInjectedInPublishMode() var resource = Assert.Single(projectResources); - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(resource, DistributedApplicationOperation.Publish); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(resource, DistributedApplicationOperation.Publish).DefaultTimeout(); Assert.False(config.ContainsKey("ASPNETCORE_URLS")); Assert.False(config.ContainsKey("ASPNETCORE_HTTPS_PORT")); @@ -357,7 +358,7 @@ public async Task ExcludeLaunchProfileAddsHttpOrHttpsEndpointAddsToEnv() var resource = Assert.Single(projectResources); - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); Assert.Equal("http://localhost:p0;https://localhost:p1", config["ASPNETCORE_URLS"]); Assert.Equal("5001", config["ASPNETCORE_HTTPS_PORT"]); @@ -379,7 +380,7 @@ public async Task NoEndpointsDoesNotAddAspNetCoreUrls() var resource = Assert.Single(projectResources); - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); Assert.False(config.ContainsKey("ASPNETCORE_URLS")); Assert.False(config.ContainsKey("ASPNETCORE_HTTPS_PORT")); @@ -404,7 +405,7 @@ public async Task ProjectWithLaunchProfileAddsHttpOrHttpsEndpointAddsToEnv() var resource = Assert.Single(projectResources); - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); Assert.Equal("http://localhost:p0", config["ASPNETCORE_URLS"]); Assert.False(config.ContainsKey("ASPNETCORE_HTTPS_PORT")); @@ -431,7 +432,7 @@ public async Task ProjectWithMultipleLaunchProfileAppUrlsGetsAllUrls() var appModel = app.Services.GetRequiredService(); var projectResources = appModel.GetProjectResources(); var resource = Assert.Single(projectResources); - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); Assert.Equal("https://localhost:p2;http://localhost:p0;http://localhost:p1;https://localhost:p3;https://localhost:p4", config["ASPNETCORE_URLS"]); @@ -477,7 +478,7 @@ public async Task VerifyManifest(bool disableForwardedHeaders) var resource = Assert.Single(projectResources); - var manifest = await ManifestUtils.GetManifest(resource); + var manifest = await ManifestUtils.GetManifest(resource).DefaultTimeout(); var fordwardedHeadersEnvVar = disableForwardedHeaders ? "" @@ -527,7 +528,7 @@ public async Task VerifyManifestWithArgs() var resource = Assert.Single(projectResources); - var manifest = await ManifestUtils.GetManifest(resource); + var manifest = await ManifestUtils.GetManifest(resource).DefaultTimeout(); var expectedManifest = $$""" { @@ -583,7 +584,7 @@ public async Task AddProjectWithArgs() using var app = appBuilder.Build(); - var args = await ArgumentEvaluator.GetArgumentListAsync(project.Resource); + var args = await ArgumentEvaluator.GetArgumentListAsync(project.Resource).DefaultTimeout(); Assert.Collection(args, arg => Assert.Equal("arg1", arg), @@ -618,7 +619,7 @@ public async Task AddProjectWithWildcardUrlInLaunchSettings(bool isProxied, stri var resource = Assert.Single(projectResources); - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); var http = resource.GetEndpoint("http"); var https = resource.GetEndpoint("https"); diff --git a/tests/Aspire.Hosting.Tests/PublishAsConnectionStringTests.cs b/tests/Aspire.Hosting.Tests/PublishAsConnectionStringTests.cs index 5f35839e66..98a37a608f 100644 --- a/tests/Aspire.Hosting.Tests/PublishAsConnectionStringTests.cs +++ b/tests/Aspire.Hosting.Tests/PublishAsConnectionStringTests.cs @@ -3,6 +3,7 @@ using Xunit; using Aspire.Hosting.Utils; +using Microsoft.AspNetCore.InternalTesting; namespace Aspire.Hosting.Tests; @@ -17,7 +18,7 @@ public async Task PublishAsConnectionStringConfiguresManifestAsParameter() Assert.True(redis.Resource.TryGetLastAnnotation(out _)); - var manifest = await ManifestUtils.GetManifest(redis.Resource); + var manifest = await ManifestUtils.GetManifest(redis.Resource).DefaultTimeout(); var expected = """ diff --git a/tests/Aspire.Hosting.Tests/PublishAsDockerfileTests.cs b/tests/Aspire.Hosting.Tests/PublishAsDockerfileTests.cs index 0c23d68969..309b14d973 100644 --- a/tests/Aspire.Hosting.Tests/PublishAsDockerfileTests.cs +++ b/tests/Aspire.Hosting.Tests/PublishAsDockerfileTests.cs @@ -3,6 +3,7 @@ using Xunit; using Aspire.Hosting.Utils; +using Microsoft.AspNetCore.InternalTesting; namespace Aspire.Hosting.Tests; @@ -18,7 +19,7 @@ public async Task PublishAsDockerFileConfiguresManifestWithoutBuildArgs() Assert.True(frontend.Resource.TryGetLastAnnotation(out _)); - var manifest = await ManifestUtils.GetManifest(frontend.Resource); + var manifest = await ManifestUtils.GetManifest(frontend.Resource).DefaultTimeout(); var expected = $$""" @@ -53,7 +54,7 @@ public async Task PublishAsDockerFileConfiguresManifestWithBuildArgs() Assert.True(frontend.Resource.TryGetLastAnnotation(out _)); - var manifest = await ManifestUtils.GetManifest(frontend.Resource); + var manifest = await ManifestUtils.GetManifest(frontend.Resource).DefaultTimeout(); var expected = $$""" @@ -91,7 +92,7 @@ public async Task PublishAsDockerFileConfiguresManifestWithBuildArgsThatHaveNoVa Assert.True(frontend.Resource.TryGetLastAnnotation(out _)); - var manifest = await ManifestUtils.GetManifest(frontend.Resource); + var manifest = await ManifestUtils.GetManifest(frontend.Resource).DefaultTimeout(); var expected = $$""" diff --git a/tests/Aspire.Hosting.Tests/ResourceExtensionsTests.cs b/tests/Aspire.Hosting.Tests/ResourceExtensionsTests.cs index 2be8721d3a..e65d79d9cc 100644 --- a/tests/Aspire.Hosting.Tests/ResourceExtensionsTests.cs +++ b/tests/Aspire.Hosting.Tests/ResourceExtensionsTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Hosting.Utils; +using Microsoft.AspNetCore.InternalTesting; using Xunit; namespace Aspire.Hosting.Tests; @@ -98,7 +99,7 @@ public async Task GetEnvironmentVariableValuesAsyncReturnCorrectVariablesInRunMo context.EnvironmentVariables["ELASTIC_PASSWORD"] = "p@ssw0rd1"; }); - var env = await container.Resource.GetEnvironmentVariableValuesAsync(); + var env = await container.Resource.GetEnvironmentVariableValuesAsync().DefaultTimeout(); Assert.Collection(env, env => @@ -131,7 +132,7 @@ public async Task GetEnvironmentVariableValuesAsyncReturnCorrectVariablesUsingVa .WithEnvironment("xpack.security.enabled", "true") .WithEnvironment("ELASTIC_PASSWORD", passwordParameter); - var env = await container.Resource.GetEnvironmentVariableValuesAsync(); + var env = await container.Resource.GetEnvironmentVariableValuesAsync().DefaultTimeout(); Assert.Collection(env, env => @@ -164,7 +165,7 @@ public async Task GetEnvironmentVariableValuesAsyncReturnCorrectVariablesUsingMa .WithEnvironment("xpack.security.enabled", "true") .WithEnvironment("ELASTIC_PASSWORD", passwordParameter); - var env = await container.Resource.GetEnvironmentVariableValuesAsync(DistributedApplicationOperation.Publish); + var env = await container.Resource.GetEnvironmentVariableValuesAsync(DistributedApplicationOperation.Publish).DefaultTimeout(); Assert.Collection(env, env => diff --git a/tests/Aspire.Hosting.Tests/ResourceLoggerServiceTests.cs b/tests/Aspire.Hosting.Tests/ResourceLoggerServiceTests.cs index 49bca79b0e..110de733e7 100644 --- a/tests/Aspire.Hosting.Tests/ResourceLoggerServiceTests.cs +++ b/tests/Aspire.Hosting.Tests/ResourceLoggerServiceTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Hosting.Tests.Utils; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.Logging; using Xunit; @@ -22,14 +23,14 @@ public async Task AddingResourceLoggerAnnotationAllowsLogging() var logsLoop = ConsoleLoggingTestHelpers.WatchForLogsAsync(logsEnumerator1, 2); // Wait for subscriber to be added - await subsLoop.WaitAsync(TimeSpan.FromSeconds(15)); + await subsLoop.DefaultTimeout(); // Log logger.LogInformation("Hello, world!"); logger.LogError("Hello, error!"); // Wait for logs to be read - var allLogs = await logsLoop.WaitAsync(TimeSpan.FromSeconds(15)); + var allLogs = await logsLoop.DefaultTimeout(); Assert.Equal("2000-12-29T20:59:59.0000000Z Hello, world!", allLogs[0].Content); Assert.False(allLogs[0].IsErrorMessage); @@ -41,15 +42,15 @@ public async Task AddingResourceLoggerAnnotationAllowsLogging() subsLoop = WatchForSubscribers(service); var logsEnumerator2 = service.WatchAsync(testResource).GetAsyncEnumerator(); logsLoop = ConsoleLoggingTestHelpers.WatchForLogsAsync(logsEnumerator2, 2); - await subsLoop.WaitAsync(TimeSpan.FromSeconds(150)); - allLogs = await logsLoop.WaitAsync(TimeSpan.FromSeconds(15)); + await subsLoop.DefaultTimeout(); + allLogs = await logsLoop.DefaultTimeout(); Assert.Equal(2, allLogs.Count); Assert.Equal("2000-12-29T20:59:59.0000000Z Hello, world!", allLogs[0].Content); Assert.Equal("2000-12-29T20:59:59.0000000Z Hello, error!", allLogs[1].Content); - await logsEnumerator1.DisposeAsync(); - await logsEnumerator2.DisposeAsync(); + await logsEnumerator1.DisposeAsync().DefaultTimeout(); + await logsEnumerator2.DisposeAsync().DefaultTimeout(); } [Fact] @@ -63,7 +64,7 @@ public async Task StreamingLogsCancelledAfterComplete() var logsLoop = ConsoleLoggingTestHelpers.WatchForLogsAsync(service, 2, testResource); // Wait for subscriber to be added - await subsLoop.WaitAsync(TimeSpan.FromSeconds(15)); + await subsLoop.DefaultTimeout(); logger.LogInformation("Hello, world!"); logger.LogError("Hello, error!"); @@ -73,7 +74,7 @@ public async Task StreamingLogsCancelledAfterComplete() logger.LogInformation("The third log"); // Wait for logs to be read - var allLogs = await logsLoop.WaitAsync(TimeSpan.FromSeconds(15)); + var allLogs = await logsLoop.DefaultTimeout(); Assert.Collection(allLogs, l => Assert.Equal("2000-12-29T20:59:59.0000000Z Hello, world!", l.Content), @@ -84,7 +85,7 @@ public async Task StreamingLogsCancelledAfterComplete() // New sub should replay logs again. logsLoop = ConsoleLoggingTestHelpers.WatchForLogsAsync(service, 100, testResource); - allLogs = await logsLoop.WaitAsync(TimeSpan.FromSeconds(15)); + allLogs = await logsLoop.DefaultTimeout(); Assert.Collection(allLogs, l => Assert.Equal("2000-12-29T20:59:59.0000000Z Hello, world!", l.Content), @@ -103,14 +104,14 @@ public async Task SecondSubscriberGetsBacklog() var logsLoop = ConsoleLoggingTestHelpers.WatchForLogsAsync(logsEnumerator1, 2); // Wait for subscriber to be added - await subsLoop.WaitAsync(TimeSpan.FromSeconds(15)); + await subsLoop.DefaultTimeout(); // Log logger.LogInformation("Hello, world!"); logger.LogError("Hello, error!"); // Wait for logs to be read - var allLogs = await logsLoop.WaitAsync(TimeSpan.FromSeconds(15)); + var allLogs = await logsLoop.DefaultTimeout(); Assert.Equal("2000-12-29T20:59:59.0000000Z Hello, world!", allLogs[0].Content); Assert.False(allLogs[0].IsErrorMessage); @@ -122,8 +123,8 @@ public async Task SecondSubscriberGetsBacklog() subsLoop = WatchForSubscribers(service); var logsEnumerator2 = service.WatchAsync(testResource).GetAsyncEnumerator(); logsLoop = ConsoleLoggingTestHelpers.WatchForLogsAsync(logsEnumerator2, 2); - await subsLoop.WaitAsync(TimeSpan.FromSeconds(15)); - allLogs = await logsLoop.WaitAsync(TimeSpan.FromSeconds(15)); + await subsLoop.DefaultTimeout(); + allLogs = await logsLoop.DefaultTimeout(); Assert.Equal(2, allLogs.Count); Assert.Equal("2000-12-29T20:59:59.0000000Z Hello, world!", allLogs[0].Content); @@ -135,9 +136,9 @@ public async Task SecondSubscriberGetsBacklog() subsLoop = WatchForSubscribers(service); var logsEnumerator3 = service.WatchAsync(testResource).GetAsyncEnumerator(); logsLoop = ConsoleLoggingTestHelpers.WatchForLogsAsync(logsEnumerator3, 1); - await subsLoop.WaitAsync(TimeSpan.FromSeconds(15)); + await subsLoop.DefaultTimeout(); logger.LogInformation("The third log"); - allLogs = await logsLoop.WaitAsync(TimeSpan.FromSeconds(15)); + allLogs = await logsLoop.DefaultTimeout(); // The backlog should be cleared so only new logs are received Assert.Single(allLogs); @@ -159,10 +160,10 @@ public async Task InMemoryLogsPreservedBetweenWatches() var logsLoop = ConsoleLoggingTestHelpers.WatchForLogsAsync(logsEnumerator1, 1); // Wait for subscriber to be added - await subsLoop.WaitAsync(TimeSpan.FromSeconds(15)); + await subsLoop.DefaultTimeout(); // Read before watching log - var allLogs = await logsLoop.WaitAsync(TimeSpan.FromSeconds(15)); + var allLogs = await logsLoop.DefaultTimeout(); Assert.Equal("2000-12-29T20:59:59.0000000Z Before watching!", allLogs[0].Content); Assert.False(allLogs[0].IsErrorMessage); @@ -171,7 +172,7 @@ public async Task InMemoryLogsPreservedBetweenWatches() logger.LogInformation("While watching!"); logsLoop = ConsoleLoggingTestHelpers.WatchForLogsAsync(logsEnumerator1, 1); - allLogs = await logsLoop.WaitAsync(TimeSpan.FromSeconds(15)); + allLogs = await logsLoop.DefaultTimeout(); Assert.Equal("2000-12-29T20:59:59.0000000Z While watching!", allLogs[0].Content); Assert.False(allLogs[0].IsErrorMessage); @@ -180,15 +181,15 @@ public async Task InMemoryLogsPreservedBetweenWatches() subsLoop = WatchForSubscribers(service); var logsEnumerator2 = service.WatchAsync(testResource).GetAsyncEnumerator(); logsLoop = ConsoleLoggingTestHelpers.WatchForLogsAsync(logsEnumerator2, 2); - await subsLoop.WaitAsync(TimeSpan.FromSeconds(15)); - allLogs = await logsLoop.WaitAsync(TimeSpan.FromSeconds(15)); + await subsLoop.DefaultTimeout(); + allLogs = await logsLoop.DefaultTimeout(); Assert.Equal(2, allLogs.Count); Assert.Equal("2000-12-29T20:59:59.0000000Z Before watching!", allLogs[0].Content); Assert.Equal("2000-12-29T20:59:59.0000000Z While watching!", allLogs[1].Content); - await logsEnumerator1.DisposeAsync(); - await logsEnumerator2.DisposeAsync(); + await logsEnumerator1.DisposeAsync().DefaultTimeout(); + await logsEnumerator2.DisposeAsync().DefaultTimeout(); logger.LogInformation("After watching!"); @@ -198,9 +199,9 @@ public async Task InMemoryLogsPreservedBetweenWatches() subsLoop = WatchForSubscribers(service); var logsEnumerator3 = service.WatchAsync(testResource).GetAsyncEnumerator(); logsLoop = ConsoleLoggingTestHelpers.WatchForLogsAsync(logsEnumerator3, 4); - await subsLoop.WaitAsync(TimeSpan.FromSeconds(15)); + await subsLoop.DefaultTimeout(); logger.LogInformation("While watching again!"); - allLogs = await logsLoop.WaitAsync(TimeSpan.FromSeconds(15)); + allLogs = await logsLoop.DefaultTimeout(); Assert.Equal(4, allLogs.Count); Assert.Equal("2000-12-29T20:59:59.0000000Z Before watching!", allLogs[0].Content); @@ -224,7 +225,7 @@ public async Task MultipleInstancesLogsToAll() var logsLoop = ConsoleLoggingTestHelpers.WatchForLogsAsync(logsEnumerator, 4); // Wait for subscriber to be added - await subsLoop.WaitAsync(TimeSpan.FromSeconds(15)); + await subsLoop.DefaultTimeout(); // Log logger.LogInformation("Hello, world!"); @@ -234,7 +235,7 @@ public async Task MultipleInstancesLogsToAll() Assert.True(service.Loggers.ContainsKey("instance1")); // Wait for logs to be read - var allLogs = await logsLoop.WaitAsync(TimeSpan.FromSeconds(15)); + var allLogs = await logsLoop.DefaultTimeout(); var sortedLogs = allLogs.OrderBy(l => l.LineNumber).ToList(); @@ -245,9 +246,9 @@ public async Task MultipleInstancesLogsToAll() service.Complete(testResource); - Assert.False(await logsEnumerator.MoveNextAsync()); + Assert.False(await logsEnumerator.MoveNextAsync().DefaultTimeout()); - await logsEnumerator.DisposeAsync(); + await logsEnumerator.DisposeAsync().DefaultTimeout(); } private sealed class TestResource(string name) : Resource(name) diff --git a/tests/Aspire.Hosting.Tests/ResourceNotificationTests.cs b/tests/Aspire.Hosting.Tests/ResourceNotificationTests.cs index 36919ad875..3974f9df60 100644 --- a/tests/Aspire.Hosting.Tests/ResourceNotificationTests.cs +++ b/tests/Aspire.Hosting.Tests/ResourceNotificationTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Hosting.Tests.Utils; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; @@ -64,14 +65,14 @@ async Task> GetValuesAsync(CancellationToken cancellationTok return values; } - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + using var cts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(); var enumerableTask = GetValuesAsync(cts.Token); - await notificationService.PublishUpdateAsync(resource, state => state with { Properties = state.Properties.Add(new("A", "value")) }); + await notificationService.PublishUpdateAsync(resource, state => state with { Properties = state.Properties.Add(new("A", "value")) }).DefaultTimeout(); - await notificationService.PublishUpdateAsync(resource, state => state with { Properties = state.Properties.Add(new("B", "value")) }); + await notificationService.PublishUpdateAsync(resource, state => state with { Properties = state.Properties.Add(new("B", "value")) }).DefaultTimeout(); - var values = await enumerableTask; + var values = await enumerableTask.DefaultTimeout(); Assert.Collection(values, c => @@ -117,16 +118,16 @@ async Task> GetValuesAsync(CancellationToken cancellation) return values; } - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + using var cts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(); var enumerableTask = GetValuesAsync(cts.Token); - await notificationService.PublishUpdateAsync(resource1, state => state with { Properties = state.Properties.Add(new("A", "value")) }); + await notificationService.PublishUpdateAsync(resource1, state => state with { Properties = state.Properties.Add(new("A", "value")) }).DefaultTimeout(); - await notificationService.PublishUpdateAsync(resource2, state => state with { Properties = state.Properties.Add(new("B", "value")) }); + await notificationService.PublishUpdateAsync(resource2, state => state with { Properties = state.Properties.Add(new("B", "value")) }).DefaultTimeout(); - await notificationService.PublishUpdateAsync(resource1, "replica1", state => state with { Properties = state.Properties.Add(new("C", "value")) }); + await notificationService.PublishUpdateAsync(resource1, "replica1", state => state with { Properties = state.Properties.Add(new("C", "value")) }).DefaultTimeout(); - var values = await enumerableTask; + var values = await enumerableTask.DefaultTimeout(); Assert.Collection(values, c => @@ -159,11 +160,10 @@ public async Task WaitingOnResourceReturnsWhenResourceReachesTargetState() var notificationService = ResourceNotificationServiceTestHelpers.Create(); - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - var waitTask = notificationService.WaitForResourceAsync("myResource1", "SomeState", cts.Token); + var waitTask = notificationService.WaitForResourceAsync("myResource1", "SomeState"); - await notificationService.PublishUpdateAsync(resource1, snapshot => snapshot with { State = "SomeState" }); - await waitTask; + await notificationService.PublishUpdateAsync(resource1, snapshot => snapshot with { State = "SomeState" }).DefaultTimeout(); + await waitTask.DefaultTimeout(); Assert.True(waitTask.IsCompletedSuccessfully); } @@ -175,11 +175,11 @@ public async Task WaitingOnResourceReturnsWhenResourceReachesTargetStateWithDiff var notificationService = ResourceNotificationServiceTestHelpers.Create(); - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + using var cts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(); var waitTask = notificationService.WaitForResourceAsync("MYreSouRCe1", "sOmeSTAtE", cts.Token); - await notificationService.PublishUpdateAsync(resource1, snapshot => snapshot with { State = "SomeState" }); - await waitTask; + await notificationService.PublishUpdateAsync(resource1, snapshot => snapshot with { State = "SomeState" }).DefaultTimeout(); + await waitTask.DefaultTimeout(); Assert.True(waitTask.IsCompletedSuccessfully); } @@ -192,10 +192,9 @@ public async Task WaitingOnResourceReturnsImmediatelyWhenResourceIsInTargetState var notificationService = ResourceNotificationServiceTestHelpers.Create(); // Publish the state update first - await notificationService.PublishUpdateAsync(resource1, snapshot => snapshot with { State = "SomeState" }); + await notificationService.PublishUpdateAsync(resource1, snapshot => snapshot with { State = "SomeState" }).DefaultTimeout(); - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - var waitTask = notificationService.WaitForResourceAsync("myResource1", "SomeState", cts.Token); + var waitTask = notificationService.WaitForResourceAsync("myResource1", "SomeState"); Assert.True(waitTask.IsCompletedSuccessfully); } @@ -207,11 +206,10 @@ public async Task WaitingOnResourceReturnsWhenResourceReachesRunningStateIfNoTar var notificationService = ResourceNotificationServiceTestHelpers.Create(); - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - var waitTask = notificationService.WaitForResourceAsync("myResource1", targetState: null, cancellationToken: cts.Token); + var waitTask = notificationService.WaitForResourceAsync("myResource1", targetState: null); - await notificationService.PublishUpdateAsync(resource1, snapshot => snapshot with { State = KnownResourceStates.Running }); - await waitTask; + await notificationService.PublishUpdateAsync(resource1, snapshot => snapshot with { State = KnownResourceStates.Running }).DefaultTimeout(); + await waitTask.DefaultTimeout(); Assert.True(waitTask.IsCompletedSuccessfully); } @@ -223,11 +221,10 @@ public async Task WaitingOnResourceReturnsCorrectStateWhenResourceReachesOneOfTa var notificationService = ResourceNotificationServiceTestHelpers.Create(); - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - var waitTask = notificationService.WaitForResourceAsync("myResource1", ["SomeState", "SomeOtherState"], cts.Token); + var waitTask = notificationService.WaitForResourceAsync("myResource1", ["SomeState", "SomeOtherState"]); - await notificationService.PublishUpdateAsync(resource1, snapshot => snapshot with { State = "SomeOtherState" }); - var reachedState = await waitTask; + await notificationService.PublishUpdateAsync(resource1, snapshot => snapshot with { State = "SomeOtherState" }).DefaultTimeout(); + var reachedState = await waitTask.DefaultTimeout(); Assert.Equal("SomeOtherState", reachedState); } @@ -241,8 +238,8 @@ public async Task WaitingOnResourceReturnsCorrectStateWhenResourceReachesOneOfTa var waitTask = notificationService.WaitForResourceAsync("myResource1", ["SomeState", "SomeOtherState"], default); - await notificationService.PublishUpdateAsync(resource1, snapshot => snapshot with { State = "SomeOtherState" }); - var reachedState = await waitTask; + await notificationService.PublishUpdateAsync(resource1, snapshot => snapshot with { State = "SomeOtherState" }).DefaultTimeout(); + var reachedState = await waitTask.DefaultTimeout(); Assert.Equal("SomeOtherState", reachedState); } @@ -260,7 +257,7 @@ public async Task WaitingOnResourceThrowsOperationCanceledExceptionIfResourceDoe await Assert.ThrowsAsync(async () => { await waitTask; - }); + }).DefaultTimeout(); } [Fact] @@ -275,7 +272,7 @@ public async Task WaitingOnResourceThrowsOperationCanceledExceptionIfResourceDoe await Assert.ThrowsAsync(async () => { await waitTask; - }); + }).DefaultTimeout(); } [Fact] @@ -292,7 +289,7 @@ public async Task WaitingOnResourceThrowsOperationCanceledExceptionIfResourceDoe await Assert.ThrowsAsync(async () => { await waitTask; - }); + }).DefaultTimeout(); } [Fact] @@ -302,7 +299,7 @@ public async Task PublishLogsStateTextChangesCorrectly() var logger = new FakeLogger(); var notificationService = ResourceNotificationServiceTestHelpers.Create(logger: logger); - await notificationService.PublishUpdateAsync(resource1, snapshot => snapshot with { State = "SomeState" }); + await notificationService.PublishUpdateAsync(resource1, snapshot => snapshot with { State = "SomeState" }).DefaultTimeout(); var logs = logger.Collector.GetSnapshot(); @@ -313,7 +310,7 @@ public async Task PublishLogsStateTextChangesCorrectly() logger.Collector.Clear(); // Same state text as previous state, no log - await notificationService.PublishUpdateAsync(resource1, snapshot => snapshot with { State = "SomeState" }); + await notificationService.PublishUpdateAsync(resource1, snapshot => snapshot with { State = "SomeState" }).DefaultTimeout(); logs = logger.Collector.GetSnapshot(); @@ -323,7 +320,7 @@ public async Task PublishLogsStateTextChangesCorrectly() logger.Collector.Clear(); // Different state text, log the transition from the previous state to the new state - await notificationService.PublishUpdateAsync(resource1, snapshot => snapshot with { State = "NewState" }); + await notificationService.PublishUpdateAsync(resource1, snapshot => snapshot with { State = "NewState" }).DefaultTimeout(); logs = logger.Collector.GetSnapshot(); @@ -333,7 +330,7 @@ public async Task PublishLogsStateTextChangesCorrectly() logger.Collector.Clear(); // Null state text, no log - await notificationService.PublishUpdateAsync(resource1, snapshot => snapshot with { State = null }); + await notificationService.PublishUpdateAsync(resource1, snapshot => snapshot with { State = null }).DefaultTimeout(); logs = logger.Collector.GetSnapshot(); @@ -343,7 +340,7 @@ public async Task PublishLogsStateTextChangesCorrectly() logger.Collector.Clear(); // Empty state text, no log - await notificationService.PublishUpdateAsync(resource1, snapshot => snapshot with { State = "" }); + await notificationService.PublishUpdateAsync(resource1, snapshot => snapshot with { State = "" }).DefaultTimeout(); logs = logger.Collector.GetSnapshot(); @@ -353,7 +350,7 @@ public async Task PublishLogsStateTextChangesCorrectly() logger.Collector.Clear(); // White space state text, no log - await notificationService.PublishUpdateAsync(resource1, snapshot => snapshot with { State = " " }); + await notificationService.PublishUpdateAsync(resource1, snapshot => snapshot with { State = " " }).DefaultTimeout(); logs = logger.Collector.GetSnapshot(); @@ -371,9 +368,9 @@ public async Task PublishLogsTraceStateDetailsCorrectly() var notificationService = ResourceNotificationServiceTestHelpers.Create(logger: logger); var createdDate = DateTime.Now; - await notificationService.PublishUpdateAsync(resource1, snapshot => snapshot with { CreationTimeStamp = createdDate }); - await notificationService.PublishUpdateAsync(resource1, snapshot => snapshot with { State = "SomeState" }); - await notificationService.PublishUpdateAsync(resource1, snapshot => snapshot with { ExitCode = 0 }); + await notificationService.PublishUpdateAsync(resource1, snapshot => snapshot with { CreationTimeStamp = createdDate }).DefaultTimeout(); + await notificationService.PublishUpdateAsync(resource1, snapshot => snapshot with { State = "SomeState" }).DefaultTimeout(); + await notificationService.PublishUpdateAsync(resource1, snapshot => snapshot with { ExitCode = 0 }).DefaultTimeout(); var logs = logger.Collector.GetSnapshot(); diff --git a/tests/Aspire.Hosting.Tests/SlimTestProgramTests.cs b/tests/Aspire.Hosting.Tests/SlimTestProgramTests.cs index b48ddf65d0..e71389eecf 100644 --- a/tests/Aspire.Hosting.Tests/SlimTestProgramTests.cs +++ b/tests/Aspire.Hosting.Tests/SlimTestProgramTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Hosting.Testing; +using Microsoft.AspNetCore.InternalTesting; using Xunit; namespace Aspire.Hosting.Tests; @@ -21,7 +22,7 @@ public async Task TestProjectStartsAndStopsCleanly() { var testProgram = _slimTestProgramFixture.TestProgram; - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + using var cts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration); // Make sure each service is running await EnsureServicesAreRunning(testProgram, cts.Token); @@ -45,7 +46,7 @@ public async Task TestPortOnEndpointAnnotationAndAllocatedEndpointAnnotationMatc { var testProgram = _slimTestProgramFixture.TestProgram; - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + using var cts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration); // Make sure each service is running await EnsureServicesAreRunning(testProgram, cts.Token); @@ -63,7 +64,7 @@ public async Task TestPortOnEndpointAnnotationAndAllocatedEndpointAnnotationMatc { var testProgram = _slimTestProgramFixture.TestProgram; - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + using var cts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration); // Make sure each service is running await EnsureServicesAreRunning(testProgram, cts.Token); diff --git a/tests/Aspire.Hosting.Tests/TestProgramFixture.cs b/tests/Aspire.Hosting.Tests/TestProgramFixture.cs index ecf7a4ef70..ddf13ad0af 100644 --- a/tests/Aspire.Hosting.Tests/TestProgramFixture.cs +++ b/tests/Aspire.Hosting.Tests/TestProgramFixture.cs @@ -3,6 +3,7 @@ using Aspire.Hosting.Testing; using Aspire.Hosting.Tests.Utils; +using Microsoft.AspNetCore.InternalTesting; using Xunit; namespace Aspire.Hosting.Tests; @@ -29,7 +30,7 @@ public async Task InitializeAsync() _app = _testProgram.Build(); - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5)); + using var cts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration); await _app.StartAsync(cts.Token); diff --git a/tests/Aspire.Hosting.Tests/Utils/TestDistributedApplicationBuilder.cs b/tests/Aspire.Hosting.Tests/Utils/TestDistributedApplicationBuilder.cs index aed5c6891f..cd341c48f1 100644 --- a/tests/Aspire.Hosting.Tests/Utils/TestDistributedApplicationBuilder.cs +++ b/tests/Aspire.Hosting.Tests/Utils/TestDistributedApplicationBuilder.cs @@ -62,7 +62,7 @@ public static TestDistributedApplicationBuilder Create(Action - Create(o => o.ContainerRegistryOverride = TestConstants.AspireTestContainerRegistry, testOutputHelper); + Create(o => o.ContainerRegistryOverride = ComponentTestConstants.AspireTestContainerRegistry, testOutputHelper); private TestDistributedApplicationBuilder(Action? configureOptions, ITestOutputHelper? testOutputHelper = null) { diff --git a/tests/Aspire.Hosting.Tests/WaitForTests.cs b/tests/Aspire.Hosting.Tests/WaitForTests.cs index 7d4342283a..b5319459dd 100644 --- a/tests/Aspire.Hosting.Tests/WaitForTests.cs +++ b/tests/Aspire.Hosting.Tests/WaitForTests.cs @@ -3,6 +3,7 @@ using Aspire.Components.Common.Tests; using Aspire.Hosting.Utils; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.DependencyInjection; using Xunit; using Xunit.Abstractions; @@ -23,7 +24,7 @@ public async Task ResourceThatFailsToStartDueToExceptionDoesNotCauseStartAsyncTo var dependingExecutableResource = builder.AddExecutable("dependingexecutableresource", "doesnotmatter", "alsodoesntmatter") .WaitFor(throwingResource); - var abortCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var abortCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration); using var app = builder.Build(); await app.StartAsync(abortCts.Token); @@ -95,13 +96,13 @@ public async Task EnsureDependentResourceMovesIntoWaitingState() // into a Running state, so rather than awaiting it we'll hold onto the // task so we can inspect the state of the Nginx resource which should // be in a waiting state if everything is working correctly. - var startupCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var startupCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration); var startTask = app.StartAsync(startupCts.Token); // We don't want to wait forever for Nginx to move into a waiting state, - // it should be super quick, but we'll allow 60 seconds just in case the + // it should be super quick, but we'll allow a long timeout just in case the // CI machine is chugging (also useful when collecting code coverage). - var waitingStateCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var waitingStateCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration); var rns = app.Services.GetRequiredService(); await rns.WaitForResourceAsync(nginx.Resource.Name, "Waiting", waitingStateCts.Token); @@ -136,13 +137,13 @@ public async Task WaitForCompletionWaitsForTerminalStateOfDependencyResource() // into a Finished state, so rather than awaiting it we'll hold onto the // task so we can inspect the state of the Nginx resource which should // be in a waiting state if everything is working correctly. - var startupCts = new CancellationTokenSource(TimeSpan.FromSeconds(120)); + var startupCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration); var startTask = app.StartAsync(startupCts.Token); // We don't want to wait forever for Nginx to move into a waiting state, // it should be super quick, but we'll allow 60 seconds just in case the // CI machine is chugging (also useful when collecting code coverage). - var waitingStateCts = new CancellationTokenSource(TimeSpan.FromSeconds(120)); + var waitingStateCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration); var rns = app.Services.GetRequiredService(); await rns.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.Waiting, waitingStateCts.Token); @@ -159,7 +160,7 @@ await rns.PublishUpdateAsync(dependency.Resource, s => s with // This time we want to wait for Nginx to move into a Running state to verify that // it successfully started after we moved the dependency resource into the Finished, but // we need to give it more time since we have to download the image in CI. - var runningStateCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var runningStateCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration); await rns.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.Running, runningStateCts.Token); await startTask; @@ -184,13 +185,13 @@ public async Task WaitForThrowsIfResourceMovesToTerminalStateBeforeRunning() // into a Finished state, so rather than awaiting it we'll hold onto the // task so we can inspect the state of the Nginx resource which should // be in a waiting state if everything is working correctly. - var startupCts = new CancellationTokenSource(TimeSpan.FromSeconds(120)); + var startupCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration); var startTask = app.StartAsync(startupCts.Token); // We don't want to wait forever for Nginx to move into a waiting state, // it should be super quick, but we'll allow 60 seconds just in case the // CI machine is chugging (also useful when collecting code coverage). - var waitingStateCts = new CancellationTokenSource(TimeSpan.FromSeconds(120)); + var waitingStateCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration); var rns = app.Services.GetRequiredService(); await rns.WaitForResourceAsync(nginx.Resource.Name, "Waiting", waitingStateCts.Token); @@ -207,7 +208,7 @@ await rns.PublishUpdateAsync(dependency.Resource, s => s with // This time we want to wait for Nginx to move into a Running state to verify that // it successfully started after we moved the dependency resource into the Finished, but // we need to give it more time since we have to download the image in CI. - var runningStateCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var runningStateCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration); await rns.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.FailedToStart, runningStateCts.Token); await startTask; @@ -232,13 +233,13 @@ public async Task EnsureDependencyResourceThatReturnsNonMatchingExitCodeResultsI // into a Finished state, so rather than awaiting it we'll hold onto the // task so we can inspect the state of the Nginx resource which should // be in a waiting state if everything is working correctly. - var startupCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var startupCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration); var startTask = app.StartAsync(startupCts.Token); // We don't want to wait forever for Nginx to move into a waiting state, // it should be super quick, but we'll allow 60 seconds just in case the // CI machine is chugging (also useful when collecting code coverage). - var waitingStateCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var waitingStateCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration); var rns = app.Services.GetRequiredService(); await rns.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.Waiting, waitingStateCts.Token); @@ -254,7 +255,7 @@ await rns.PublishUpdateAsync(dependency.Resource, s => s with // This time we want to wait for Nginx to move into a FailedToStart state to verify that // it didn't start if the dependency resource didn't finish with the correct exit code. - var runningStateCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var runningStateCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration); await rns.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.FailedToStart, runningStateCts.Token); await startTask; @@ -281,13 +282,13 @@ public async Task DependencyWithGreaterThan1ReplicaAnnotationCausesDependentReso // into a Finished state, so rather than awaiting it we'll hold onto the // task so we can inspect the state of the Nginx resource which should // be in a waiting state if everything is working correctly. - var startupCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var startupCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration); var startTask = app.StartAsync(startupCts.Token); // We don't want to wait forever for Nginx to move into a waiting state, // it should be super quick, but we'll allow 60 seconds just in case the // CI machine is chugging (also useful when collecting code coverage). - var waitingStateCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var waitingStateCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration); var rns = app.Services.GetRequiredService(); await rns.WaitForResourceAsync(nginx.Resource.Name, "FailedToStart", waitingStateCts.Token); @@ -315,13 +316,13 @@ public async Task WaitForCompletionSucceedsIfDependentResourceEntersTerminalStat // into a Finished state, so rather than awaiting it we'll hold onto the // task so we can inspect the state of the Nginx resource which should // be in a waiting state if everything is working correctly. - var startupCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var startupCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration); var startTask = app.StartAsync(startupCts.Token); // We don't want to wait forever for Nginx to move into a waiting state, // it should be super quick, but we'll allow 60 seconds just in case the // CI machine is chugging (also useful when collecting code coverage). - var waitingStateCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var waitingStateCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration); var rns = app.Services.GetRequiredService(); await rns.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.Waiting, waitingStateCts.Token); diff --git a/tests/Aspire.Hosting.Tests/WithEndpointTests.cs b/tests/Aspire.Hosting.Tests/WithEndpointTests.cs index bafddc5a01..a03cbce88c 100644 --- a/tests/Aspire.Hosting.Tests/WithEndpointTests.cs +++ b/tests/Aspire.Hosting.Tests/WithEndpointTests.cs @@ -3,6 +3,7 @@ using Aspire.Hosting.Tests.Utils; using Aspire.Hosting.Utils; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -211,7 +212,7 @@ public async Task CanAddEndpointsWithContainerPortAndEnv() var resource = Assert.Single(exeResources); - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); Assert.Equal("foo", resource.Name); var endpoints = resource.Annotations.OfType().ToArray(); @@ -268,7 +269,7 @@ public async Task VerifyManifestWithBothDifferentPortAndTargetPort() var container = builder.AddContainer("app", "image") .WithEndpoint(name: "ep0", port: 8080, targetPort: 3000); - var manifest = await ManifestUtils.GetManifest(container.Resource); + var manifest = await ManifestUtils.GetManifest(container.Resource).DefaultTimeout(); var expectedManifest = """ { @@ -296,7 +297,7 @@ public async Task VerifyManifestWithHttpPortWithTargetPort() var container = builder.AddContainer("app", "image") .WithHttpEndpoint(name: "h1", targetPort: 3001); - var manifest = await ManifestUtils.GetManifest(container.Resource); + var manifest = await ManifestUtils.GetManifest(container.Resource).DefaultTimeout(); var expectedManifest = """ { @@ -323,7 +324,7 @@ public async Task VerifyManifestWithHttpsAndTargetPort() var container = builder.AddContainer("app", "image") .WithHttpsEndpoint(name: "h2", targetPort: 3001); - var manifest = await ManifestUtils.GetManifest(container.Resource); + var manifest = await ManifestUtils.GetManifest(container.Resource).DefaultTimeout(); var expectedManifest = """ { @@ -350,7 +351,7 @@ public async Task VerifyManifestContainerWithHttpEndpointAndNoPortsAllocatesPort var container = builder.AddContainer("app", "image") .WithHttpEndpoint(name: "h3"); - var manifest = await ManifestUtils.GetManifest(container.Resource); + var manifest = await ManifestUtils.GetManifest(container.Resource).DefaultTimeout(); var expectedManifest = """ { @@ -377,7 +378,7 @@ public async Task VerifyManifestContainerWithHttpsEndpointAllocatesPort() var container = builder.AddContainer("app", "image") .WithHttpsEndpoint(name: "h4"); - var manifest = await ManifestUtils.GetManifest(container.Resource); + var manifest = await ManifestUtils.GetManifest(container.Resource).DefaultTimeout(); var expectedManifest = """ { @@ -404,7 +405,7 @@ public async Task VerifyManifestWithHttpEndpointAndPortOnlySetsTargetPort() var container = builder.AddContainer("app", "image") .WithHttpEndpoint(name: "otlp", port: 1004); - var manifest = await ManifestUtils.GetManifest(container.Resource); + var manifest = await ManifestUtils.GetManifest(container.Resource).DefaultTimeout(); var expectedManifest = """ { @@ -431,7 +432,7 @@ public async Task VerifyManifestWithTcpEndpointAndNoPortAllocatesPort() var container = builder.AddContainer("app", "image") .WithEndpoint(name: "custom"); - var manifest = await ManifestUtils.GetManifest(container.Resource); + var manifest = await ManifestUtils.GetManifest(container.Resource).DefaultTimeout(); var expectedManifest = """ { @@ -462,7 +463,7 @@ public async Task VerifyManifestProjectWithDefaultHttpEndpointsDoesNotAllocatePo .WithHttpsEndpoint(name: "hps2") // Will get a targetPort .WithEndpoint(scheme: "tcp", name: "tcp0"); // Will get a targetPort - var manifest = await ManifestUtils.GetManifest(project.Resource); + var manifest = await ManifestUtils.GetManifest(project.Resource).DefaultTimeout(); var expectedManifest = """ @@ -532,7 +533,7 @@ public async Task VerifyManifestProjectWithEndpointsSetsPortsEnvVariables() // Should not be included in HTTP_PORTS .WithEndpointsInEnvironment(e => e.Name != "dontinjectme"); - var manifest = await ManifestUtils.GetManifest(project.Resource); + var manifest = await ManifestUtils.GetManifest(project.Resource).DefaultTimeout(); var expectedEnv = """ @@ -559,7 +560,7 @@ public async Task VerifyManifestPortAllocationIsGlobal() var container1 = builder.AddContainer("app1", "image") .WithEndpoint(name: "custom"); - var manifests = await ManifestUtils.GetManifests([container0.Resource, container1.Resource]); + var manifests = await ManifestUtils.GetManifests([container0.Resource, container1.Resource]).DefaultTimeout(); var expectedManifest0 = """ { diff --git a/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs b/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs index ec382850e3..ffe75a1985 100644 --- a/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs +++ b/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs @@ -3,6 +3,7 @@ using Aspire.Hosting.Tests.Utils; using Aspire.Hosting.Utils; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -29,7 +30,7 @@ public async Task BuiltApplicationHasAccessToIServiceProviderViaEnvironmentCallb var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync( container.Resource, serviceProvider: serviceProvider - ); + ).DefaultTimeout(); Assert.Equal("true", config["SP_AVAILABLE"]); } @@ -49,7 +50,7 @@ public async Task EnvironmentReferencingEndpointPopulatesWithBindingUrl() var projectB = builder.AddProject("projectB") .WithEnvironment("myName", projectA.GetEndpoint("mybinding")); - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); Assert.Equal("https://localhost:2000", config["myName"]); } @@ -62,7 +63,7 @@ public async Task SimpleEnvironmentWithNameAndValue() var project = builder.AddProject("projectA") .WithEnvironment("myName", "value"); - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(project.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(project.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); Assert.Equal("value", config["myName"]); } @@ -78,7 +79,7 @@ public async Task SimpleEnvironmentWithNameAndReferenceExpressionValue() var project = builder.AddProject("projectA") .WithEnvironment("myName", parameterExpression); - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(project.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(project.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); Assert.Equal("value", config["myName"]); } @@ -94,7 +95,7 @@ public async Task EnvironmentCallbackPopulatesValueWhenCalled() environmentValue = "value2"; // Call environment variable callbacks. - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectA.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectA.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); Assert.Equal("value2", config["myName"]); } @@ -111,7 +112,7 @@ public async Task EnvironmentCallbackPopulatesValueWhenParameterResourceProvided var projectA = builder.AddProject("projectA") .WithEnvironment("MY_PARAMETER", parameter); - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectA.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectA.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); Assert.Equal("MY_PARAMETER_VALUE", config["MY_PARAMETER"]); } @@ -127,7 +128,7 @@ public async Task EnvironmentCallbackPopulatesWithExpressionPlaceholderWhenPubli .WithEnvironment("MY_PARAMETER", parameter); var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectA.Resource, - DistributedApplicationOperation.Publish); + DistributedApplicationOperation.Publish).DefaultTimeout(); Assert.Equal("{parameter.value}", config["MY_PARAMETER"]); } @@ -146,7 +147,7 @@ public async Task EnvironmentCallbackThrowsWhenParameterValueMissingInDcpMode() projectA.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance - )); + )).DefaultTimeout(); Assert.Equal("Parameter resource could not be used because configuration key 'Parameters:parameter' is missing and the Parameter has no default value.", exception.Message); } @@ -166,7 +167,7 @@ public async Task ComplexEnvironmentCallbackPopulatesValueWhenCalled() environmentValue = "value2"; // Call environment variable callbacks. - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectA.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectA.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); Assert.Equal("value2", config["myName"]); } @@ -193,8 +194,8 @@ public async Task EnvironmentVariableExpressions() .WithEnvironment("TARGET_PORT", $"{endpoint.Property(EndpointProperty.TargetPort)}") .WithEnvironment("HOST", $"{test.Resource};name=1"); - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(containerB.Resource); - var manifestConfig = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(containerB.Resource, DistributedApplicationOperation.Publish); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(containerB.Resource).DefaultTimeout(); + var manifestConfig = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(containerB.Resource, DistributedApplicationOperation.Publish).DefaultTimeout(); Assert.Equal(4, config.Count); Assert.Equal($"http://container1:10005/foo", config["URL"]); @@ -226,7 +227,7 @@ public async Task EnvironmentVariableWithDynamicTargetPort() var containerB = builder.AddContainer("container2", "imageB") .WithEnvironment("TARGET_PORT", $"{endpoint.Property(EndpointProperty.TargetPort)}"); - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(containerB.Resource); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(containerB.Resource).DefaultTimeout(); var pair = Assert.Single(config); Assert.Equal("TARGET_PORT", pair.Key); @@ -250,13 +251,13 @@ public async Task EnvironmentWithConnectionStringSetsProperEnvironmentVariable() targetBuilder.WithEnvironment(envVarName, sourceBuilder); // Call environment variable callbacks for the Run operation. - var runConfig = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(targetBuilder.Resource, DistributedApplicationOperation.Run); + var runConfig = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(targetBuilder.Resource, DistributedApplicationOperation.Run).DefaultTimeout(); // Assert Assert.Single(runConfig, kvp => kvp.Key == envVarName && kvp.Value == sourceCon); // Call environment variable callbacks for the Publish operation. - var publishConfig = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(targetBuilder.Resource, DistributedApplicationOperation.Publish); + var publishConfig = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(targetBuilder.Resource, DistributedApplicationOperation.Publish).DefaultTimeout(); // Assert Assert.Single(publishConfig, kvp => kvp.Key == envVarName && kvp.Value == "{sourceService.connectionString}"); diff --git a/tests/Aspire.Hosting.Tests/WithReferenceTests.cs b/tests/Aspire.Hosting.Tests/WithReferenceTests.cs index b29e0686b4..6344604cde 100644 --- a/tests/Aspire.Hosting.Tests/WithReferenceTests.cs +++ b/tests/Aspire.Hosting.Tests/WithReferenceTests.cs @@ -3,6 +3,7 @@ using Aspire.Hosting.Tests.Utils; using Aspire.Hosting.Utils; +using Microsoft.AspNetCore.InternalTesting; using Xunit; namespace Aspire.Hosting.Tests; @@ -25,7 +26,7 @@ public async Task ResourceWithSingleEndpointProducesSimplifiedEnvironmentVariabl var projectB = builder.AddProject("b").WithReference(projectA.GetEndpoint(endpointName)); // Call environment variable callbacks. - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); Assert.Equal("https://localhost:2000", config["services__projecta__mybinding__0"]); } @@ -48,7 +49,7 @@ public async Task ResourceWithConflictingEndpointsProducesFullyScopedEnvironment .WithReference(projectA.GetEndpoint("myconflictingbinding")); // Call environment variable callbacks. - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); Assert.Equal("https://localhost:2000", config["services__projecta__mybinding__0"]); Assert.Equal("https://localhost:3000", config["services__projecta__myconflictingbinding__0"]); @@ -73,7 +74,7 @@ public async Task ResourceWithNonConflictingEndpointsProducesAllVariantsOfEnviro .WithReference(projectA.GetEndpoint("mynonconflictingbinding")); // Call environment variable callbacks. - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); Assert.Equal("https://localhost:2000", config["services__projecta__mybinding__0"]); Assert.Equal("http://localhost:3000", config["services__projecta__mynonconflictingbinding__0"]); @@ -96,7 +97,7 @@ public async Task ResourceWithConflictingEndpointsProducesAllEnvironmentVariable .WithReference(projectA); // Call environment variable callbacks. - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); Assert.Equal("https://localhost:2000", config["services__projecta__mybinding__0"]); Assert.Equal("https://localhost:3000", config["services__projecta__mybinding2__0"]); @@ -117,7 +118,7 @@ public async Task ResourceWithEndpointsProducesAllEnvironmentVariables() var projectB = builder.AddProject("projectb") .WithReference(projectA); // Call environment variable callbacks. - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); Assert.Equal("https://localhost:2000", config["services__projecta__mybinding__0"]); Assert.Equal("http://localhost:3000", config["services__projecta__mybinding2__0"]); @@ -136,7 +137,7 @@ public async Task ConnectionStringResourceThrowsWhenMissingConnectionString() await Assert.ThrowsAsync(async () => { await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); - }); + }).DefaultTimeout(); } [Fact] @@ -149,7 +150,7 @@ public async Task ConnectionStringResourceOptionalWithMissingConnectionString() var projectB = builder.AddProject("projectB") .WithReference(resource, optional: true); - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); var servicesKeysCount = config.Keys.Count(k => k.StartsWith("ConnectionStrings__")); Assert.Equal(0, servicesKeysCount); @@ -169,7 +170,7 @@ public async Task ParameterAsConnectionStringResourceThrowsWhenConnectionStringS var exception = await Assert.ThrowsAsync(async () => { var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); - }); + }).DefaultTimeout(); Assert.Equal("Connection string parameter resource could not be used because connection string 'missingresource' is missing.", exception.Message); } @@ -187,7 +188,7 @@ public async Task ParameterAsConnectionStringResourceInjectsConnectionStringWhen .WithReference(resource); // Call environment variable callbacks. - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); Assert.Equal("test connection string", config["ConnectionStrings__resource"]); } @@ -203,7 +204,7 @@ public async Task ParameterAsConnectionStringResourceInjectsExpressionWhenPublis .WithReference(resource); // Call environment variable callbacks. - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Publish); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Publish).DefaultTimeout(); Assert.Equal("{resource.connectionString}", config["ConnectionStrings__resource"]); } @@ -219,7 +220,7 @@ public async Task ParameterAsConnectionStringResourceInjectsCorrectEnvWhenPublis .WithReference(resource); // Call environment variable callbacks. - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Publish); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Publish).DefaultTimeout(); Assert.Equal("{resource.connectionString}", config["MY_ENV"]); } @@ -238,7 +239,7 @@ public async Task ConnectionStringResourceWithConnectionString() .WithReference(resource); // Call environment variable callbacks. - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); var servicesKeysCount = config.Keys.Count(k => k.StartsWith("ConnectionStrings__")); Assert.Equal(1, servicesKeysCount); @@ -260,7 +261,7 @@ public async Task ConnectionStringResourceWithConnectionStringOverwriteName() .WithReference(resource, connectionName: "bob"); // Call environment variable callbacks. - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); var servicesKeysCount = config.Keys.Count(k => k.StartsWith("ConnectionStrings__")); Assert.Equal(1, servicesKeysCount); @@ -292,7 +293,7 @@ public async Task WithReferenceHttpProduceEnvironmentVariables() .WithReference("petstore", new Uri("https://petstore.swagger.io/")); // Call environment variable callbacks. - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectA.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectA.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); var servicesKeysCount = config.Keys.Count(k => k.StartsWith("services__")); Assert.Equal(1, servicesKeysCount); diff --git a/tests/Aspire.Milvus.Client.Tests/MilvusContainerFixture.cs b/tests/Aspire.Milvus.Client.Tests/MilvusContainerFixture.cs index eddb69e6da..a959b874be 100644 --- a/tests/Aspire.Milvus.Client.Tests/MilvusContainerFixture.cs +++ b/tests/Aspire.Milvus.Client.Tests/MilvusContainerFixture.cs @@ -20,7 +20,7 @@ public async Task InitializeAsync() if (RequiresDockerAttribute.IsSupported) { Container = new MilvusBuilder() - .WithImage($"{TestConstants.AspireTestContainerRegistry}/{MilvusContainerImageTags.Image}:{MilvusContainerImageTags.Tag}") + .WithImage($"{ComponentTestConstants.AspireTestContainerRegistry}/{MilvusContainerImageTags.Image}:{MilvusContainerImageTags.Tag}") .Build(); await Container.StartAsync(); } diff --git a/tests/Aspire.MongoDB.Driver.Tests/MongoDbContainerFixture.cs b/tests/Aspire.MongoDB.Driver.Tests/MongoDbContainerFixture.cs index 95814b5009..44f1ba68f1 100644 --- a/tests/Aspire.MongoDB.Driver.Tests/MongoDbContainerFixture.cs +++ b/tests/Aspire.MongoDB.Driver.Tests/MongoDbContainerFixture.cs @@ -22,7 +22,7 @@ public async Task InitializeAsync() // testcontainers uses mongo:mongo by default, // resetting that for tests Container = new MongoDbBuilder() - .WithImage($"{TestConstants.AspireTestContainerRegistry}/{MongoDBContainerImageTags.Image}:{MongoDBContainerImageTags.Tag}") + .WithImage($"{ComponentTestConstants.AspireTestContainerRegistry}/{MongoDBContainerImageTags.Image}:{MongoDBContainerImageTags.Tag}") .WithUsername(null) .WithPassword(null) .Build(); diff --git a/tests/Aspire.MySqlConnector.Tests/MySqlContainerFixture.cs b/tests/Aspire.MySqlConnector.Tests/MySqlContainerFixture.cs index b521c27a3a..2f2a983938 100644 --- a/tests/Aspire.MySqlConnector.Tests/MySqlContainerFixture.cs +++ b/tests/Aspire.MySqlConnector.Tests/MySqlContainerFixture.cs @@ -20,7 +20,7 @@ public async Task InitializeAsync() if (RequiresDockerAttribute.IsSupported) { Container = new MySqlBuilder() - .WithImage($"{TestConstants.AspireTestContainerRegistry}/{MySqlContainerImageTags.Image}:{MySqlContainerImageTags.Tag}") + .WithImage($"{ComponentTestConstants.AspireTestContainerRegistry}/{MySqlContainerImageTags.Image}:{MySqlContainerImageTags.Tag}") .Build(); await Container.StartAsync(); } diff --git a/tests/Aspire.NATS.Net.Tests/NatsContainerFixture.cs b/tests/Aspire.NATS.Net.Tests/NatsContainerFixture.cs index f982c31ff2..6f515e3a93 100644 --- a/tests/Aspire.NATS.Net.Tests/NatsContainerFixture.cs +++ b/tests/Aspire.NATS.Net.Tests/NatsContainerFixture.cs @@ -20,7 +20,7 @@ public async Task InitializeAsync() if (RequiresDockerAttribute.IsSupported) { Container = new NatsBuilder() - .WithImage($"{TestConstants.AspireTestContainerRegistry}/{NatsContainerImageTags.Image}:{NatsContainerImageTags.Tag}") + .WithImage($"{ComponentTestConstants.AspireTestContainerRegistry}/{NatsContainerImageTags.Image}:{NatsContainerImageTags.Tag}") .Build(); await Container.StartAsync(); } diff --git a/tests/Aspire.Npgsql.Tests/PostgreSQLContainerFixture.cs b/tests/Aspire.Npgsql.Tests/PostgreSQLContainerFixture.cs index 5f5f8abeef..f5ee028b4d 100644 --- a/tests/Aspire.Npgsql.Tests/PostgreSQLContainerFixture.cs +++ b/tests/Aspire.Npgsql.Tests/PostgreSQLContainerFixture.cs @@ -20,7 +20,7 @@ public async Task InitializeAsync() if (RequiresDockerAttribute.IsSupported) { Container = new PostgreSqlBuilder() - .WithImage($"{TestConstants.AspireTestContainerRegistry}/{PostgresContainerImageTags.Image}:{PostgresContainerImageTags.Tag}") + .WithImage($"{ComponentTestConstants.AspireTestContainerRegistry}/{PostgresContainerImageTags.Image}:{PostgresContainerImageTags.Tag}") .Build(); await Container.StartAsync(); } diff --git a/tests/Aspire.Oracle.EntityFrameworkCore.Tests/OracleContainerFixture.cs b/tests/Aspire.Oracle.EntityFrameworkCore.Tests/OracleContainerFixture.cs index ee0a328b47..7c119d63d6 100644 --- a/tests/Aspire.Oracle.EntityFrameworkCore.Tests/OracleContainerFixture.cs +++ b/tests/Aspire.Oracle.EntityFrameworkCore.Tests/OracleContainerFixture.cs @@ -30,7 +30,7 @@ public async Task InitializeAsync() Container = new OracleBuilder() .WithPortBinding(1521, true) .WithHostname("localhost") - .WithImage($"{TestConstants.AspireTestContainerRegistry}/gvenzl/oracle-xe:21.3.0-slim-faststart") + .WithImage($"{ComponentTestConstants.AspireTestContainerRegistry}/gvenzl/oracle-xe:21.3.0-slim-faststart") .WithWaitStrategy(Wait .ForUnixContainer() .UntilMessageIsLogged("Completed: ALTER DATABASE OPEN") diff --git a/tests/Aspire.RabbitMQ.Client.Tests/AspireRabbitMQLoggingTests.cs b/tests/Aspire.RabbitMQ.Client.Tests/AspireRabbitMQLoggingTests.cs index 1ca8d40c5e..ca6bfc0b34 100644 --- a/tests/Aspire.RabbitMQ.Client.Tests/AspireRabbitMQLoggingTests.cs +++ b/tests/Aspire.RabbitMQ.Client.Tests/AspireRabbitMQLoggingTests.cs @@ -29,7 +29,7 @@ public class AspireRabbitMQLoggingTests public async Task EndToEndLoggingTest() { await using var rabbitMqContainer = new RabbitMqBuilder() - .WithImage($"{TestConstants.AspireTestContainerRegistry}/{RabbitMQContainerImageTags.Image}:{RabbitMQContainerImageTags.Tag}") + .WithImage($"{ComponentTestConstants.AspireTestContainerRegistry}/{RabbitMQContainerImageTags.Image}:{RabbitMQContainerImageTags.Tag}") .Build(); await rabbitMqContainer.StartAsync(); diff --git a/tests/Aspire.RabbitMQ.Client.Tests/RabbitMQContainerFixture.cs b/tests/Aspire.RabbitMQ.Client.Tests/RabbitMQContainerFixture.cs index 334eeef9c5..54c76d5177 100644 --- a/tests/Aspire.RabbitMQ.Client.Tests/RabbitMQContainerFixture.cs +++ b/tests/Aspire.RabbitMQ.Client.Tests/RabbitMQContainerFixture.cs @@ -34,7 +34,7 @@ public async Task DisposeAsync() public static async Task CreateContainerAsync() { var container = new RabbitMqBuilder() - .WithImage($"{TestConstants.AspireTestContainerRegistry}/{RabbitMQContainerImageTags.Image}:{RabbitMQContainerImageTags.Tag}") + .WithImage($"{ComponentTestConstants.AspireTestContainerRegistry}/{RabbitMQContainerImageTags.Image}:{RabbitMQContainerImageTags.Tag}") .Build(); await container.StartAsync(); diff --git a/tests/Aspire.StackExchange.Redis.Tests/RedisContainerFixture.cs b/tests/Aspire.StackExchange.Redis.Tests/RedisContainerFixture.cs index b831af6cd7..9ef1f1f6a8 100644 --- a/tests/Aspire.StackExchange.Redis.Tests/RedisContainerFixture.cs +++ b/tests/Aspire.StackExchange.Redis.Tests/RedisContainerFixture.cs @@ -34,7 +34,7 @@ public async Task DisposeAsync() public static async Task CreateContainerAsync() { var container = new RedisBuilder() - .WithImage($"{TestConstants.AspireTestContainerRegistry}/{RedisContainerImageTags.Image}:{RedisContainerImageTags.Tag}") + .WithImage($"{ComponentTestConstants.AspireTestContainerRegistry}/{RedisContainerImageTags.Image}:{RedisContainerImageTags.Tag}") .Build(); await container.StartAsync(); diff --git a/tests/Shared/AsyncTestHelpers.cs b/tests/Shared/AsyncTestHelpers.cs index 48bf6c8f5a..2edff0c209 100644 --- a/tests/Shared/AsyncTestHelpers.cs +++ b/tests/Shared/AsyncTestHelpers.cs @@ -1,12 +1,168 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics; +using System.Runtime.CompilerServices; using Microsoft.Extensions.Logging; +using System.Threading.Tasks; namespace Microsoft.AspNetCore.InternalTesting; +internal static class TestConstants +{ + // IMPORTANT: If a test fails because these time out, consider adding a new field with a larger value. + // These values are as big as they need to be to test things complete in the expected time. +#if DEBUG + // Shorter duration when running tests with debug. + // Less time waiting for hang unit tests to fail in aspnetcore solution. + public const int DefaultTimeoutDuration = 5 * 1000; + public const int LongTimeoutDuration = 20 * 1000; +#else + public const int DefaultTimeoutDuration = 30 * 1000; + public const int LongTimeoutDuration = 120 * 1000; +#endif + + public static TimeSpan DefaultTimeoutTimeSpan { get; } = TimeSpan.FromMilliseconds(DefaultTimeoutDuration); + public static TimeSpan LongTimeoutTimeSpan { get; } = TimeSpan.FromMilliseconds(LongTimeoutDuration); +} + internal static class AsyncTestHelpers { + private static readonly string s_assemblyName = typeof(TimeoutException).Assembly.GetName().Name!; + + public static CancellationTokenSource CreateDefaultTimeoutTokenSource(int milliseconds = TestConstants.DefaultTimeoutDuration) + { + var cts = new CancellationTokenSource(); + if (!Debugger.IsAttached) + { + cts.CancelAfter(TimeSpan.FromMilliseconds(milliseconds)); + } + return cts; + } + + public static async IAsyncEnumerable DefaultTimeout(this IAsyncEnumerable asyncEnumerable, int milliseconds = TestConstants.DefaultTimeoutDuration, [CallerFilePath] string? filePath = null, [CallerLineNumber] int lineNumber = default) + { + // Wrap the enumerable with an enumerable that times out after exceeding time limit on each iteration. + await using var enumator = asyncEnumerable.GetAsyncEnumerator(); + while (await enumator.MoveNextAsync().DefaultTimeout(milliseconds, filePath, lineNumber)) + { + yield return enumator.Current; + } + } + + public static Task DefaultTimeout(this Task task, int milliseconds = TestConstants.DefaultTimeoutDuration, [CallerFilePath] string? filePath = null, [CallerLineNumber] int lineNumber = default) + { + return task.TimeoutAfter(TimeSpan.FromMilliseconds(milliseconds), filePath, lineNumber); + } + + public static Task DefaultTimeout(this Task task, TimeSpan timeout, [CallerFilePath] string? filePath = null, [CallerLineNumber] int lineNumber = default) + { + return task.TimeoutAfter(timeout, filePath, lineNumber); + } + + public static Task DefaultTimeout(this ValueTask task, int milliseconds = TestConstants.DefaultTimeoutDuration, [CallerFilePath] string? filePath = null, [CallerLineNumber] int lineNumber = default) + { + return task.AsTask().TimeoutAfter(TimeSpan.FromMilliseconds(milliseconds), filePath, lineNumber); + } + + public static Task DefaultTimeout(this ValueTask task, TimeSpan timeout, [CallerFilePath] string? filePath = null, [CallerLineNumber] int lineNumber = default) + { + return task.AsTask().TimeoutAfter(timeout, filePath, lineNumber); + } + + public static Task DefaultTimeout(this Task task, int milliseconds = TestConstants.DefaultTimeoutDuration, [CallerFilePath] string? filePath = null, [CallerLineNumber] int lineNumber = default) + { + return task.TimeoutAfter(TimeSpan.FromMilliseconds(milliseconds), filePath, lineNumber); + } + + public static Task DefaultTimeout(this Task task, TimeSpan timeout, [CallerFilePath] string? filePath = null, [CallerLineNumber] int lineNumber = default) + { + return task.TimeoutAfter(timeout, filePath, lineNumber); + } + + public static Task DefaultTimeout(this ValueTask task, int milliseconds = TestConstants.DefaultTimeoutDuration, [CallerFilePath] string? filePath = null, [CallerLineNumber] int lineNumber = default) + { + return task.AsTask().TimeoutAfter(TimeSpan.FromMilliseconds(milliseconds), filePath, lineNumber); + } + + public static Task DefaultTimeout(this ValueTask task, TimeSpan timeout, [CallerFilePath] string? filePath = null, [CallerLineNumber] int lineNumber = default) + { + return task.AsTask().TimeoutAfter(timeout, filePath, lineNumber); + } + + public static async Task TimeoutAfter(this Task task, TimeSpan timeout, + [CallerFilePath] string? filePath = null, + [CallerLineNumber] int lineNumber = default) + { + // Don't create a timer if the task is already completed + // or the debugger is attached + if (task.IsCompleted || Debugger.IsAttached) + { + return await task.ConfigureAwait(false); + } +#if NET6_0_OR_GREATER + try + { + return await task.WaitAsync(timeout).ConfigureAwait(false); + } + catch (TimeoutException ex) when (ex.Source == s_assemblyName) + { + throw new TimeoutException(CreateMessage(timeout, filePath!, lineNumber)); + } +#else + var cts = new CancellationTokenSource(); + if (task == await Task.WhenAny(task, Task.Delay(timeout, cts.Token)).ConfigureAwait(false)) + { + cts.Cancel(); + return await task.ConfigureAwait(false); + } + else + { + throw new TimeoutException(CreateMessage(timeout, filePath, lineNumber)); + } +#endif + } + + public static async Task TimeoutAfter(this Task task, TimeSpan timeout, + [CallerFilePath] string? filePath = null, + [CallerLineNumber] int lineNumber = default) + { + // Don't create a timer if the task is already completed + // or the debugger is attached + if (task.IsCompleted || Debugger.IsAttached) + { + await task.ConfigureAwait(false); + return; + } +#if NET6_0_OR_GREATER + try + { + await task.WaitAsync(timeout).ConfigureAwait(false); + } + catch (TimeoutException ex) when (ex.Source == s_assemblyName) + { + throw new TimeoutException(CreateMessage(timeout, filePath!, lineNumber)); + } +#else + var cts = new CancellationTokenSource(); + if (task == await Task.WhenAny(task, Task.Delay(timeout, cts.Token)).ConfigureAwait(false)) + { + cts.Cancel(); + await task.ConfigureAwait(false); + } + else + { + throw new TimeoutException(CreateMessage(timeout, filePath, lineNumber)); + } +#endif + } + + private static string CreateMessage(TimeSpan timeout, string filePath, int lineNumber) + => string.IsNullOrEmpty(filePath) + ? $"The operation timed out after reaching the limit of {timeout.TotalMilliseconds}ms." + : $"The operation at {filePath}:{lineNumber} timed out after reaching the limit of {timeout.TotalMilliseconds}ms."; + public static Task AssertIsTrueRetryAsync(Func assert, string message, ILogger? logger = null) { return AssertIsTrueRetryAsync(() => Task.FromResult(assert()), message, logger); diff --git a/tests/Shared/TaskExtensions.cs b/tests/Shared/TaskExtensions.cs deleted file mode 100644 index 53f2ce5cc2..0000000000 --- a/tests/Shared/TaskExtensions.cs +++ /dev/null @@ -1,135 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; - -namespace Microsoft.AspNetCore.InternalTesting; - -internal static class TaskExtensions -{ -#if DEBUG - // Shorter duration when running tests with debug. - // Less time waiting for hang unit tests to fail in aspnetcore solution. - public const int DefaultTimeoutDuration = 5 * 1000; -#else - public const int DefaultTimeoutDuration = 30 * 1000; -#endif - - public static TimeSpan DefaultTimeoutTimeSpan { get; } = TimeSpan.FromMilliseconds(DefaultTimeoutDuration); - - public static Task DefaultTimeout(this Task task, int milliseconds = DefaultTimeoutDuration, [CallerFilePath] string? filePath = null, [CallerLineNumber] int lineNumber = default) - { - return task.TimeoutAfter(TimeSpan.FromMilliseconds(milliseconds), filePath, lineNumber); - } - - public static Task DefaultTimeout(this Task task, TimeSpan timeout, [CallerFilePath] string? filePath = null, [CallerLineNumber] int lineNumber = default) - { - return task.TimeoutAfter(timeout, filePath, lineNumber); - } - - public static Task DefaultTimeout(this ValueTask task, int milliseconds = DefaultTimeoutDuration, [CallerFilePath] string? filePath = null, [CallerLineNumber] int lineNumber = default) - { - return task.AsTask().TimeoutAfter(TimeSpan.FromMilliseconds(milliseconds), filePath, lineNumber); - } - - public static Task DefaultTimeout(this ValueTask task, TimeSpan timeout, [CallerFilePath] string? filePath = null, [CallerLineNumber] int lineNumber = default) - { - return task.AsTask().TimeoutAfter(timeout, filePath, lineNumber); - } - - public static Task DefaultTimeout(this Task task, int milliseconds = DefaultTimeoutDuration, [CallerFilePath] string? filePath = null, [CallerLineNumber] int lineNumber = default) - { - return task.TimeoutAfter(TimeSpan.FromMilliseconds(milliseconds), filePath, lineNumber); - } - - public static Task DefaultTimeout(this Task task, TimeSpan timeout, [CallerFilePath] string? filePath = null, [CallerLineNumber] int lineNumber = default) - { - return task.TimeoutAfter(timeout, filePath, lineNumber); - } - - public static Task DefaultTimeout(this ValueTask task, int milliseconds = DefaultTimeoutDuration, [CallerFilePath] string? filePath = null, [CallerLineNumber] int lineNumber = default) - { - return task.AsTask().TimeoutAfter(TimeSpan.FromMilliseconds(milliseconds), filePath, lineNumber); - } - - public static Task DefaultTimeout(this ValueTask task, TimeSpan timeout, [CallerFilePath] string? filePath = null, [CallerLineNumber] int lineNumber = default) - { - return task.AsTask().TimeoutAfter(timeout, filePath, lineNumber); - } - - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public static async Task TimeoutAfter(this Task task, TimeSpan timeout, - [CallerFilePath] string? filePath = null, - [CallerLineNumber] int lineNumber = default) - { - // Don't create a timer if the task is already completed - // or the debugger is attached - if (task.IsCompleted || Debugger.IsAttached) - { - return await task.ConfigureAwait(false); - } -#if NET6_0_OR_GREATER - try - { - return await task.WaitAsync(timeout).ConfigureAwait(false); - } - catch (TimeoutException ex) when (ex.Source == typeof(TaskExtensions).Namespace) - { - throw new TimeoutException(CreateMessage(timeout, filePath!, lineNumber)); - } -#else - var cts = new CancellationTokenSource(); - if (task == await Task.WhenAny(task, Task.Delay(timeout, cts.Token)).ConfigureAwait(false)) - { - cts.Cancel(); - return await task.ConfigureAwait(false); - } - else - { - throw new TimeoutException(CreateMessage(timeout, filePath, lineNumber)); - } -#endif - } - - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public static async Task TimeoutAfter(this Task task, TimeSpan timeout, - [CallerFilePath] string? filePath = null, - [CallerLineNumber] int lineNumber = default) - { - // Don't create a timer if the task is already completed - // or the debugger is attached - if (task.IsCompleted || Debugger.IsAttached) - { - await task.ConfigureAwait(false); - return; - } -#if NET6_0_OR_GREATER - try - { - await task.WaitAsync(timeout).ConfigureAwait(false); - } - catch (TimeoutException ex) when (ex.Source == typeof(TaskExtensions).Namespace) - { - throw new TimeoutException(CreateMessage(timeout, filePath!, lineNumber)); - } -#else - var cts = new CancellationTokenSource(); - if (task == await Task.WhenAny(task, Task.Delay(timeout, cts.Token)).ConfigureAwait(false)) - { - cts.Cancel(); - await task.ConfigureAwait(false); - } - else - { - throw new TimeoutException(CreateMessage(timeout, filePath, lineNumber)); - } -#endif - } - - private static string CreateMessage(TimeSpan timeout, string filePath, int lineNumber) - => string.IsNullOrEmpty(filePath) - ? $"The operation timed out after reaching the limit of {timeout.TotalMilliseconds}ms." - : $"The operation at {filePath}:{lineNumber} timed out after reaching the limit of {timeout.TotalMilliseconds}ms."; -} From e516d8250ab4a507f03148b894e4ae0c5c398751 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 29 Oct 2024 15:57:04 +0800 Subject: [PATCH 06/44] Add CORS allowed origins config override to app host (#6250) --- .../BrowserTelemetry.AppHost/Program.cs | 3 +- .../Properties/launchSettings.json | 6 +- .../Dashboard/DashboardLifecycleHook.cs | 75 ++++++++++++------- src/Shared/KnownConfigNames.cs | 1 + .../Dashboard/DashboardResourceTests.cs | 21 ++++-- 5 files changed, 68 insertions(+), 38 deletions(-) diff --git a/playground/BrowserTelemetry/BrowserTelemetry.AppHost/Program.cs b/playground/BrowserTelemetry/BrowserTelemetry.AppHost/Program.cs index 1934185e92..b6623f591b 100644 --- a/playground/BrowserTelemetry/BrowserTelemetry.AppHost/Program.cs +++ b/playground/BrowserTelemetry/BrowserTelemetry.AppHost/Program.cs @@ -4,7 +4,8 @@ var builder = DistributedApplication.CreateBuilder(args); builder.AddProject("web") - .WithExternalHttpEndpoints(); + .WithExternalHttpEndpoints() + .WithReplicas(2); #if !SKIP_DASHBOARD_REFERENCE // This project is only added in playground projects to support development/debugging diff --git a/playground/BrowserTelemetry/BrowserTelemetry.AppHost/Properties/launchSettings.json b/playground/BrowserTelemetry/BrowserTelemetry.AppHost/Properties/launchSettings.json index 3b13d71489..6ee205e25a 100644 --- a/playground/BrowserTelemetry/BrowserTelemetry.AppHost/Properties/launchSettings.json +++ b/playground/BrowserTelemetry/BrowserTelemetry.AppHost/Properties/launchSettings.json @@ -11,7 +11,8 @@ "DOTNET_ENVIRONMENT": "Development", "DOTNET_DASHBOARD_OTLP_HTTP_ENDPOINT_URL": "https://localhost:16175", "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:17037", - "DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES": "true" + "DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES": "true", + "DOTNET_DASHBOARD_CORS_ALLOWED_ORIGINS": "*" } }, "http": { @@ -25,7 +26,8 @@ "DOTNET_DASHBOARD_OTLP_HTTP_ENDPOINT_URL": "http://localhost:16175", "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:17037", "DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES": "true", - "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true" + "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true", + "DOTNET_DASHBOARD_CORS_ALLOWED_ORIGINS": "*" } }, "generate-manifest": { diff --git a/src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs b/src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs index 8c6aba5c72..69fa9f2b7a 100644 --- a/src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs +++ b/src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs @@ -178,38 +178,19 @@ private void ConfigureAspireDashboardResource(IResource dashboardResource) { context.EnvironmentVariables[DashboardConfigNames.DashboardOtlpHttpUrlName.EnvVarName] = otlpHttpEndpointUrl; - var model = context.ExecutionContext.ServiceProvider.GetRequiredService(); - var allResourceEndpoints = model.Resources - .Where(r => !string.Equals(r.Name, KnownResourceNames.AspireDashboard, StringComparisons.ResourceName)) - .SelectMany(r => r.Annotations) - .OfType() - .ToList(); - - var corsOrigins = new HashSet(StringComparers.UrlHost); - foreach (var endpoint in allResourceEndpoints) - { - if (endpoint.UriScheme is "http" or "https") - { - // Prefer allocated endpoint over EndpointAnnotation.Port. - var origin = endpoint.AllocatedEndpoint?.UriString; - var targetOrigin = (endpoint.TargetPort != null) - ? $"{endpoint.UriScheme}://localhost:{endpoint.TargetPort}" - : null; + // Use explicitly defined allowed origins if configured. + var allowedOrigins = configuration[KnownConfigNames.DashboardCorsAllowedOrigins]; - if (origin != null) - { - corsOrigins.Add(origin); - } - if (targetOrigin != null) - { - corsOrigins.Add(targetOrigin); - } - } + // If allowed origins are not configured then calculate allowed origins from endpoints. + if (string.IsNullOrEmpty(allowedOrigins)) + { + var model = context.ExecutionContext.ServiceProvider.GetRequiredService(); + allowedOrigins = GetAllowedOriginsFromResourceEndpoints(model); } - if (corsOrigins.Count > 0) + if (!string.IsNullOrEmpty(allowedOrigins)) { - context.EnvironmentVariables[DashboardConfigNames.DashboardOtlpCorsAllowedOriginsKeyName.EnvVarName] = string.Join(',', corsOrigins); + context.EnvironmentVariables[DashboardConfigNames.DashboardOtlpCorsAllowedOriginsKeyName.EnvVarName] = allowedOrigins; context.EnvironmentVariables[DashboardConfigNames.DashboardOtlpCorsAllowedHeadersKeyName.EnvVarName] = "*"; } } @@ -266,6 +247,44 @@ private void ConfigureAspireDashboardResource(IResource dashboardResource) })); } + private static string? GetAllowedOriginsFromResourceEndpoints(DistributedApplicationModel model) + { + var allResourceEndpoints = model.Resources + .Where(r => !string.Equals(r.Name, KnownResourceNames.AspireDashboard, StringComparisons.ResourceName)) + .SelectMany(r => r.Annotations) + .OfType() + .ToList(); + + var corsOrigins = new HashSet(StringComparers.UrlHost); + foreach (var endpoint in allResourceEndpoints) + { + if (endpoint.UriScheme is "http" or "https") + { + // Prefer allocated endpoint over EndpointAnnotation.Port. + var origin = endpoint.AllocatedEndpoint?.UriString; + var targetOrigin = (endpoint.TargetPort != null) + ? $"{endpoint.UriScheme}://localhost:{endpoint.TargetPort}" + : null; + + if (origin != null) + { + corsOrigins.Add(origin); + } + if (targetOrigin != null) + { + corsOrigins.Add(targetOrigin); + } + } + } + + if (corsOrigins.Count > 0) + { + return string.Join(',', corsOrigins); + } + + return null; + } + private async Task WatchDashboardLogsAsync(CancellationToken cancellationToken) { var loggerCache = new ConcurrentDictionary(StringComparer.Ordinal); diff --git a/src/Shared/KnownConfigNames.cs b/src/Shared/KnownConfigNames.cs index 489f420343..231002cc47 100644 --- a/src/Shared/KnownConfigNames.cs +++ b/src/Shared/KnownConfigNames.cs @@ -12,5 +12,6 @@ internal static class KnownConfigNames public const string DashboardFrontendBrowserToken = "DOTNET_DASHBOARD_FRONTEND_BROWSERTOKEN"; public const string DashboardResourceServiceClientApiKey = "DOTNET_DASHBOARD_RESOURCESERVICE_APIKEY"; public const string DashboardUnsecuredAllowAnonymous = "DOTNET_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS"; + public const string DashboardCorsAllowedOrigins = "DOTNET_DASHBOARD_CORS_ALLOWED_ORIGINS"; public const string ResourceServiceEndpointUrl = "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL"; } diff --git a/tests/Aspire.Hosting.Tests/Dashboard/DashboardResourceTests.cs b/tests/Aspire.Hosting.Tests/Dashboard/DashboardResourceTests.cs index 05c9962306..51f099309a 100644 --- a/tests/Aspire.Hosting.Tests/Dashboard/DashboardResourceTests.cs +++ b/tests/Aspire.Hosting.Tests/Dashboard/DashboardResourceTests.cs @@ -280,8 +280,10 @@ public async Task DashboardResourceServiceUriIsSet() Assert.Equal("http://localhost:5000", config.Single(e => e.Key == DashboardConfigNames.ResourceServiceUrlName.EnvVarName).Value); } - [Fact] - public async Task DashboardResource_OtlpHttpEndpoint_CorsEnvVarSet() + [Theory] + [InlineData("*")] + [InlineData(null)] + public async Task DashboardResource_OtlpHttpEndpoint_CorsEnvVarSet(string? explicitCorsAllowedOrigins) { // Arrange using var builder = TestDistributedApplicationBuilder.Create( @@ -297,7 +299,8 @@ public async Task DashboardResource_OtlpHttpEndpoint_CorsEnvVarSet() builder.Configuration.AddInMemoryCollection(new Dictionary { ["ASPNETCORE_URLS"] = "http://localhost", - ["DOTNET_DASHBOARD_OTLP_HTTP_ENDPOINT_URL"] = "http://localhost" + ["DOTNET_DASHBOARD_OTLP_HTTP_ENDPOINT_URL"] = "http://localhost", + ["DOTNET_DASHBOARD_CORS_ALLOWED_ORIGINS"] = explicitCorsAllowedOrigins }); using var app = builder.Build(); @@ -315,12 +318,15 @@ public async Task DashboardResource_OtlpHttpEndpoint_CorsEnvVarSet() var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(dashboard, DistributedApplicationOperation.Run, app.Services).DefaultTimeout(); - Assert.Equal("http://localhost:8081,http://localhost:58080", config.Single(e => e.Key == DashboardConfigNames.DashboardOtlpCorsAllowedOriginsKeyName.EnvVarName).Value); + var expectedAllowedOrigins = !string.IsNullOrEmpty(explicitCorsAllowedOrigins) ? explicitCorsAllowedOrigins : "http://localhost:8081,http://localhost:58080"; + Assert.Equal(expectedAllowedOrigins, config.Single(e => e.Key == DashboardConfigNames.DashboardOtlpCorsAllowedOriginsKeyName.EnvVarName).Value); Assert.Equal("*", config.Single(e => e.Key == DashboardConfigNames.DashboardOtlpCorsAllowedHeadersKeyName.EnvVarName).Value); } - [Fact] - public async Task DashboardResource_OtlpGrpcEndpoint_CorsEnvVarNotSet() + [Theory] + [InlineData("*")] + [InlineData(null)] + public async Task DashboardResource_OtlpGrpcEndpoint_CorsEnvVarNotSet(string? explicitCorsAllowedOrigins) { // Arrange using var builder = TestDistributedApplicationBuilder.Create( @@ -336,7 +342,8 @@ public async Task DashboardResource_OtlpGrpcEndpoint_CorsEnvVarNotSet() builder.Configuration.AddInMemoryCollection(new Dictionary { ["ASPNETCORE_URLS"] = "http://localhost", - ["DOTNET_DASHBOARD_OTLP_ENDPOINT_URL"] = "http://localhost" + ["DOTNET_DASHBOARD_OTLP_ENDPOINT_URL"] = "http://localhost", + ["DOTNET_DASHBOARD_CORS_ALLOWED_ORIGINS"] = explicitCorsAllowedOrigins }); using var app = builder.Build(); From 53f4daa0654fee5fe288ecb987fd5e4d6c8caf48 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 29 Oct 2024 17:53:27 +0800 Subject: [PATCH 07/44] Minor improvements to dist app builder config (#6524) --- .../DistributedApplicationBuilder.cs | 24 ++++++++++++++++--- .../DistributedApplicationBuilderTests.cs | 23 ++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/Aspire.Hosting/DistributedApplicationBuilder.cs b/src/Aspire.Hosting/DistributedApplicationBuilder.cs index c1ced8265c..be5d170963 100644 --- a/src/Aspire.Hosting/DistributedApplicationBuilder.cs +++ b/src/Aspire.Hosting/DistributedApplicationBuilder.cs @@ -179,9 +179,17 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options) // account for the path the AppHost is running from to disambiguate between different projects // with the same name as seen in https://github.com/dotnet/aspire/issues/5413. For publish scenarios, // we want to use a stable hash based only on the project name. - var appHostShaBytes = SHA256.HashData(Encoding.UTF8.GetBytes(AppHostPath)); - var appHostNameShaBytes = SHA256.HashData(Encoding.UTF8.GetBytes(appHostName)); - var appHostSha = ExecutionContext.IsPublishMode ? Convert.ToHexString(appHostNameShaBytes) : Convert.ToHexString(appHostShaBytes); + string appHostSha; + if (ExecutionContext.IsPublishMode) + { + var appHostNameShaBytes = SHA256.HashData(Encoding.UTF8.GetBytes(appHostName)); + appHostSha = Convert.ToHexString(appHostNameShaBytes); + } + else + { + var appHostShaBytes = SHA256.HashData(Encoding.UTF8.GetBytes(AppHostPath)); + appHostSha = Convert.ToHexString(appHostShaBytes); + } _innerBuilder.Configuration.AddInMemoryCollection(new Dictionary { ["AppHost:Sha256"] = appHostSha @@ -244,6 +252,16 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options) } ); } + else + { + // The dashboard is enabled but is unsecured. Set auth mode config setting to reflect this state. + _innerBuilder.Configuration.AddInMemoryCollection( + new Dictionary + { + ["AppHost:ResourceService:AuthMode"] = nameof(ResourceServiceAuthMode.Unsecured) + } + ); + } _innerBuilder.Services.AddSingleton(); _innerBuilder.Services.AddOptions().ValidateOnStart().PostConfigure(MapTransportOptionsFromCustomKeys); diff --git a/tests/Aspire.Hosting.Tests/DistributedApplicationBuilderTests.cs b/tests/Aspire.Hosting.Tests/DistributedApplicationBuilderTests.cs index 6cd1f98448..76175fe6d0 100644 --- a/tests/Aspire.Hosting.Tests/DistributedApplicationBuilderTests.cs +++ b/tests/Aspire.Hosting.Tests/DistributedApplicationBuilderTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Aspire.Hosting.Dashboard; using Aspire.Hosting.Dcp; using Aspire.Hosting.Lifecycle; using Aspire.Hosting.Publishing; @@ -95,6 +96,28 @@ public void AppHostDirectoryAvailableViaConfig() Assert.Equal(appHostDirectory, config["AppHost:Directory"]); } + [Fact] + public void ResourceServiceConfig_Secured() + { + var appBuilder = DistributedApplication.CreateBuilder(); + using var app = appBuilder.Build(); + + var config = app.Services.GetRequiredService(); + Assert.Equal(nameof(ResourceServiceAuthMode.ApiKey), config["AppHost:ResourceService:AuthMode"]); + Assert.False(string.IsNullOrEmpty(config["AppHost:ResourceService:ApiKey"])); + } + + [Fact] + public void ResourceServiceConfig_Unsecured() + { + var appBuilder = DistributedApplication.CreateBuilder(args: [$"{KnownConfigNames.DashboardUnsecuredAllowAnonymous}=true"]); + using var app = appBuilder.Build(); + + var config = app.Services.GetRequiredService(); + Assert.Equal(nameof(ResourceServiceAuthMode.Unsecured), config["AppHost:ResourceService:AuthMode"]); + Assert.True(string.IsNullOrEmpty(config["AppHost:ResourceService:ApiKey"])); + } + [Fact] public void AddResource_DuplicateResourceNames_SameCasing_Error() { From f8338c5585585777857002cd06013d41712bac2a Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 29 Oct 2024 21:08:17 +0800 Subject: [PATCH 08/44] Add argument null validation to Aspire.Hosting (#6278) --- .../ContainerResourceBuilderExtensions.cs | 66 +++++++++++- .../ContainerResourceExtensions.cs | 4 + .../ExecutableResourceBuilderExtensions.cs | 12 +++ .../ExecutableResourceExtensions.cs | 2 + .../OtlpConfigurationExtensions.cs | 6 ++ .../ParameterResourceBuilderExtensions.cs | 32 ++++++ .../ProjectResourceBuilderExtensions.cs | 30 ++++++ src/Aspire.Hosting/PublicAPI.Shipped.txt | 2 - src/Aspire.Hosting/PublicAPI.Unshipped.txt | 2 + .../ResourceBuilderExtensions.cs | 102 ++++++++++++++++++ 10 files changed, 253 insertions(+), 5 deletions(-) diff --git a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs index 041dac2ba5..10e6d02632 100644 --- a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs @@ -20,6 +20,10 @@ public static class ContainerResourceBuilderExtensions /// The for chaining. public static IResourceBuilder AddContainer(this IDistributedApplicationBuilder builder, [ResourceName] string name, string image) { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(image); + return builder.AddContainer(name, image, "latest"); } @@ -33,6 +37,11 @@ public static IResourceBuilder AddContainer(this IDistributed /// The for chaining. public static IResourceBuilder AddContainer(this IDistributedApplicationBuilder builder, [ResourceName] string name, string image, string tag) { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(image); + ArgumentNullException.ThrowIfNull(tag); + var container = new ContainerResource(name); return builder.AddResource(container) .WithImage(image, tag); @@ -47,8 +56,11 @@ public static IResourceBuilder AddContainer(this IDistributed /// The target path where the volume is mounted in the container. /// A flag that indicates if the volume should be mounted as read-only. /// The . - public static IResourceBuilder WithVolume(this IResourceBuilder builder, string name, string target, bool isReadOnly = false) where T : ContainerResource + public static IResourceBuilder WithVolume(this IResourceBuilder builder, string? name, string target, bool isReadOnly = false) where T : ContainerResource { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(target); + var annotation = new ContainerMountAnnotation(name, target, ContainerMountType.Volume, isReadOnly); return builder.WithAnnotation(annotation); } @@ -62,6 +74,9 @@ public static IResourceBuilder WithVolume(this IResourceBuilder builder /// The . public static IResourceBuilder WithVolume(this IResourceBuilder builder, string target) where T : ContainerResource { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(target); + var annotation = new ContainerMountAnnotation(null, target, ContainerMountType.Volume, false); return builder.WithAnnotation(annotation); } @@ -77,6 +92,10 @@ public static IResourceBuilder WithVolume(this IResourceBuilder builder /// The . public static IResourceBuilder WithBindMount(this IResourceBuilder builder, string source, string target, bool isReadOnly = false) where T : ContainerResource { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(target); + var annotation = new ContainerMountAnnotation(Path.GetFullPath(source, builder.ApplicationBuilder.AppHostDirectory), target, ContainerMountType.BindMount, isReadOnly); return builder.WithAnnotation(annotation); } @@ -90,6 +109,9 @@ public static IResourceBuilder WithBindMount(this IResourceBuilder buil /// The . public static IResourceBuilder WithEntrypoint(this IResourceBuilder builder, string entrypoint) where T : ContainerResource { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(entrypoint); + builder.Resource.Entrypoint = entrypoint; return builder; } @@ -103,6 +125,9 @@ public static IResourceBuilder WithEntrypoint(this IResourceBuilder bui /// public static IResourceBuilder WithImageTag(this IResourceBuilder builder, string tag) where T : ContainerResource { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(tag); + if (builder.Resource.Annotations.OfType().LastOrDefault() is { } existingImageAnnotation) { existingImageAnnotation.Tag = tag; @@ -119,8 +144,10 @@ public static IResourceBuilder WithImageTag(this IResourceBuilder build /// Builder for the container resource. /// Registry value. /// - public static IResourceBuilder WithImageRegistry(this IResourceBuilder builder, string registry) where T : ContainerResource + public static IResourceBuilder WithImageRegistry(this IResourceBuilder builder, string? registry) where T : ContainerResource { + ArgumentNullException.ThrowIfNull(builder); + if (builder.Resource.Annotations.OfType().LastOrDefault() is { } existingImageAnnotation) { existingImageAnnotation.Registry = registry; @@ -140,6 +167,10 @@ public static IResourceBuilder WithImageRegistry(this IResourceBuilder /// public static IResourceBuilder WithImage(this IResourceBuilder builder, string image, string tag = "latest") where T : ContainerResource { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(image); + ArgumentNullException.ThrowIfNull(tag); + if (builder.Resource.Annotations.OfType().LastOrDefault() is { } existingImageAnnotation) { existingImageAnnotation.Image = image; @@ -162,6 +193,9 @@ public static IResourceBuilder WithImage(this IResourceBuilder builder, /// public static IResourceBuilder WithImageSHA256(this IResourceBuilder builder, string sha256) where T : ContainerResource { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(sha256); + if (builder.Resource.Annotations.OfType().LastOrDefault() is { } existingImageAnnotation) { existingImageAnnotation.SHA256 = sha256; @@ -183,6 +217,8 @@ public static IResourceBuilder WithImageSHA256(this IResourceBuilder bu /// The . public static IResourceBuilder WithContainerRuntimeArgs(this IResourceBuilder builder, params string[] args) where T : ContainerResource { + ArgumentNullException.ThrowIfNull(builder); + return builder.WithContainerRuntimeArgs(context => context.Args.AddRange(args)); } @@ -198,6 +234,9 @@ public static IResourceBuilder WithContainerRuntimeArgs(this IResourceBuil /// The . public static IResourceBuilder WithContainerRuntimeArgs(this IResourceBuilder builder, Action callback) where T : ContainerResource { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(callback); + return builder.WithContainerRuntimeArgs(context => { callback(context); @@ -217,6 +256,9 @@ public static IResourceBuilder WithContainerRuntimeArgs(this IResourceBuil /// The . public static IResourceBuilder WithContainerRuntimeArgs(this IResourceBuilder builder, Func callback) where T : ContainerResource { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(callback); + var annotation = new ContainerRuntimeArgsCallbackAnnotation(callback); return builder.WithAnnotation(annotation); } @@ -238,6 +280,8 @@ public static IResourceBuilder WithContainerRuntimeArgs(this IResourceBuil /// public static IResourceBuilder WithLifetime(this IResourceBuilder builder, ContainerLifetime lifetime) where T : ContainerResource { + ArgumentNullException.ThrowIfNull(builder); + return builder.WithAnnotation(new ContainerLifetimeAnnotation { Lifetime = lifetime }, ResourceAnnotationMutationBehavior.Replace); } @@ -253,6 +297,8 @@ private static IResourceBuilder ThrowResourceIsNotContainer(IResourceBuild /// A reference to the . public static IResourceBuilder PublishAsContainer(this IResourceBuilder builder) where T : ContainerResource { + ArgumentNullException.ThrowIfNull(builder); + return builder.WithManifestPublishingCallback(context => context.WriteContainerAsync(builder.Resource)); } @@ -293,6 +339,7 @@ public static IResourceBuilder PublishAsContainer(this IResourceBuilder /// public static IResourceBuilder WithDockerfile(this IResourceBuilder builder, string contextPath, string? dockerfilePath = null, string? stage = null) where T : ContainerResource { + ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(contextPath); var fullyQualifiedContextPath = Path.GetFullPath(contextPath, builder.ApplicationBuilder.AppHostDirectory); @@ -314,7 +361,7 @@ public static IResourceBuilder WithDockerfile(this IResourceBuilder bui var imageName = builder.GenerateImageName(); var annotation = new DockerfileBuildAnnotation(fullyQualifiedContextPath, fullyQualifiedDockerfilePath, stage); return builder.WithAnnotation(annotation, ResourceAnnotationMutationBehavior.Replace) - .WithImageRegistry(null!) + .WithImageRegistry(registry: null) .WithImage(imageName) .WithImageTag("latest"); } @@ -350,6 +397,10 @@ public static IResourceBuilder WithDockerfile(this IResourceBuilder bui /// public static IResourceBuilder AddDockerfile(this IDistributedApplicationBuilder builder, [ResourceName] string name, string contextPath, string? dockerfilePath = null, string? stage = null) { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(contextPath); + return builder.AddContainer(name, "placeholder") // Image name will be replaced by WithDockerfile. .WithDockerfile(contextPath, dockerfilePath, stage); } @@ -369,6 +420,9 @@ public static IResourceBuilder AddDockerfile(this IDistribute /// The resource bulder for the container resource. public static IResourceBuilder WithContainerName(this IResourceBuilder builder, string name) where T : ContainerResource { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + return builder.WithAnnotation(new ContainerNameAnnotation { Name = name }, ResourceAnnotationMutationBehavior.Replace); } @@ -402,6 +456,7 @@ public static IResourceBuilder WithContainerName(this IResourceBuilder /// public static IResourceBuilder WithBuildArg(this IResourceBuilder builder, string name, object value) where T : ContainerResource { + ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(name); ArgumentNullException.ThrowIfNull(value); @@ -448,6 +503,10 @@ public static IResourceBuilder WithBuildArg(this IResourceBuilder build /// public static IResourceBuilder WithBuildArg(this IResourceBuilder builder, string name, IResourceBuilder value) where T : ContainerResource { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(value); + if (value.Resource.Secret) { throw new InvalidOperationException("Cannot add secret parameter as a build argument. Use WithSecretBuildArg instead."); @@ -487,6 +546,7 @@ public static IResourceBuilder WithBuildArg(this IResourceBuilder build /// public static IResourceBuilder WithBuildSecret(this IResourceBuilder builder, string name, IResourceBuilder value) where T : ContainerResource { + ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(name); ArgumentNullException.ThrowIfNull(value); diff --git a/src/Aspire.Hosting/ContainerResourceExtensions.cs b/src/Aspire.Hosting/ContainerResourceExtensions.cs index 5c520aea8b..ea48e0b557 100644 --- a/src/Aspire.Hosting/ContainerResourceExtensions.cs +++ b/src/Aspire.Hosting/ContainerResourceExtensions.cs @@ -17,6 +17,8 @@ public static class ContainerResourceExtensions /// A collection of container resources in the specified distributed application model. public static IEnumerable GetContainerResources(this DistributedApplicationModel model) { + ArgumentNullException.ThrowIfNull(model); + foreach (var resource in model.Resources) { if (resource.Annotations.OfType().Any()) @@ -33,6 +35,8 @@ public static IEnumerable GetContainerResources(this DistributedAppli /// true if the specified resource is a container resource; otherwise, false. public static bool IsContainer(this IResource resource) { + ArgumentNullException.ThrowIfNull(resource); + return resource.Annotations.OfType().Any(); } } diff --git a/src/Aspire.Hosting/ExecutableResourceBuilderExtensions.cs b/src/Aspire.Hosting/ExecutableResourceBuilderExtensions.cs index dc402967b1..8c16df8404 100644 --- a/src/Aspire.Hosting/ExecutableResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ExecutableResourceBuilderExtensions.cs @@ -23,6 +23,11 @@ public static class ExecutableResourceBuilderExtensions /// The . public static IResourceBuilder AddExecutable(this IDistributedApplicationBuilder builder, [ResourceName] string name, string command, string workingDirectory, params string[]? args) { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(command); + ArgumentNullException.ThrowIfNull(workingDirectory); + return AddExecutable(builder, name, command, workingDirectory, (object[]?)args); } @@ -37,6 +42,11 @@ public static IResourceBuilder AddExecutable(this IDistribut /// The . public static IResourceBuilder AddExecutable(this IDistributedApplicationBuilder builder, [ResourceName] string name, string command, string workingDirectory, params object[]? args) { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(command); + ArgumentNullException.ThrowIfNull(workingDirectory); + workingDirectory = PathNormalizer.NormalizePathForCurrentPlatform(Path.Combine(builder.AppHostDirectory, workingDirectory)); var executable = new ExecutableResource(name, command, workingDirectory); @@ -61,6 +71,8 @@ public static IResourceBuilder AddExecutable(this IDistribut /// A reference to the . public static IResourceBuilder PublishAsDockerFile(this IResourceBuilder builder, IEnumerable? buildArgs = null) where T : ExecutableResource { + ArgumentNullException.ThrowIfNull(builder); + return builder.WithManifestPublishingCallback(context => WriteExecutableAsDockerfileResourceAsync(context, builder.Resource, buildArgs)); } diff --git a/src/Aspire.Hosting/ExecutableResourceExtensions.cs b/src/Aspire.Hosting/ExecutableResourceExtensions.cs index 5b7604a1ad..69fccd1f39 100644 --- a/src/Aspire.Hosting/ExecutableResourceExtensions.cs +++ b/src/Aspire.Hosting/ExecutableResourceExtensions.cs @@ -17,6 +17,8 @@ public static class ExecutableResourceExtensions /// An enumerable collection of executable resources. public static IEnumerable GetExecutableResources(this DistributedApplicationModel model) { + ArgumentNullException.ThrowIfNull(model); + return model.Resources.OfType(); } } diff --git a/src/Aspire.Hosting/OtlpConfigurationExtensions.cs b/src/Aspire.Hosting/OtlpConfigurationExtensions.cs index e08c74f94a..ffed6b77be 100644 --- a/src/Aspire.Hosting/OtlpConfigurationExtensions.cs +++ b/src/Aspire.Hosting/OtlpConfigurationExtensions.cs @@ -27,6 +27,10 @@ public static class OtlpConfigurationExtensions /// The host environment to check if the application is running in development mode. public static void AddOtlpEnvironment(IResource resource, IConfiguration configuration, IHostEnvironment environment) { + ArgumentNullException.ThrowIfNull(resource); + ArgumentNullException.ThrowIfNull(configuration); + ArgumentNullException.ThrowIfNull(environment); + // Configure OpenTelemetry in projects using environment variables. // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/configuration/sdk-environment-variables.md @@ -112,6 +116,8 @@ static void SetOtelEndpointAndProtocol(Dictionary environmentVar /// The . public static IResourceBuilder WithOtlpExporter(this IResourceBuilder builder) where T : IResourceWithEnvironment { + ArgumentNullException.ThrowIfNull(builder); + AddOtlpEnvironment(builder.Resource, builder.ApplicationBuilder.Configuration, builder.ApplicationBuilder.Environment); return builder; } diff --git a/src/Aspire.Hosting/ParameterResourceBuilderExtensions.cs b/src/Aspire.Hosting/ParameterResourceBuilderExtensions.cs index ebd8006409..04473465aa 100644 --- a/src/Aspire.Hosting/ParameterResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ParameterResourceBuilderExtensions.cs @@ -24,6 +24,9 @@ public static class ParameterResourceBuilderExtensions /// public static IResourceBuilder AddParameter(this IDistributedApplicationBuilder builder, [ResourceName] string name, bool secret = false) { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + return builder.AddParameter(name, parameterDefault => GetParameterValue(builder.Configuration, name, parameterDefault), secret: secret); } @@ -41,6 +44,10 @@ public static IResourceBuilder AddParameter(this IDistributed Justification = "third parameters are mutually exclusive.")] public static IResourceBuilder AddParameter(this IDistributedApplicationBuilder builder, [ResourceName] string name, string value, bool publishValueAsDefault = false, bool secret = false) { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(value); + return builder.AddParameter(name, () => value, publishValueAsDefault, secret); } @@ -58,6 +65,10 @@ public static IResourceBuilder AddParameter(this IDistributed Justification = "third parameters are mutually exclusive.")] public static IResourceBuilder AddParameter(this IDistributedApplicationBuilder builder, string name, Func valueGetter, bool publishValueAsDefault = false, bool secret = false) { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(valueGetter); + // We don't allow a parameter to be both secret and published, as that would write the secret to the manifest. if (publishValueAsDefault && secret) { @@ -83,6 +94,10 @@ public static IResourceBuilder AddParameter(this IDistributed /// Resource builder for the parameter. public static IResourceBuilder AddParameterFromConfiguration(this IDistributedApplicationBuilder builder, string name, string configurationKey, bool secret = false) { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(configurationKey); + return builder.AddParameter( name, parameterDefault => GetParameterValue(builder.Configuration, name, parameterDefault, configurationKey), @@ -106,6 +121,10 @@ public static IResourceBuilder AddParameterFromConfiguration( Justification = "third parameters are mutually exclusive.")] public static IResourceBuilder AddParameter(this IDistributedApplicationBuilder builder, [ResourceName] string name, ParameterDefault value, bool secret = false, bool persist = false) { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(value); + // If it needs persistence, wrap it in a UserSecretsParameterDefault if (persist && builder.ExecutionContext.IsRunMode && builder.AppHostAssembly is not null) { @@ -197,6 +216,9 @@ public Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationT /// public static IResourceBuilder AddConnectionString(this IDistributedApplicationBuilder builder, [ResourceName] string name, string? environmentVariableName = null) { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + var parameterBuilder = builder.AddParameter(name, _ => { return builder.Configuration.GetConnectionString(name) ?? throw new DistributedApplicationException($"Connection string parameter resource could not be used because connection string '{name}' is missing."); @@ -217,6 +239,8 @@ public static IResourceBuilder AddConnectionStrin public static IResourceBuilder PublishAsConnectionString(this IResourceBuilder builder) where T : ContainerResource, IResourceWithConnectionString { + ArgumentNullException.ThrowIfNull(builder); + ConfigureConnectionStringManifestPublisher(builder); return builder; } @@ -227,6 +251,8 @@ public static IResourceBuilder PublishAsConnectionString(this IResourceBui /// The . public static void ConfigureConnectionStringManifestPublisher(IResourceBuilder builder) { + ArgumentNullException.ThrowIfNull(builder); + // Create a parameter resource that we use to write to the manifest var parameter = new ParameterResource(builder.Resource.Name, _ => "", secret: true); parameter.IsConnectionString = true; @@ -257,6 +283,9 @@ public static ParameterResource CreateDefaultPasswordParameter( bool lower = true, bool upper = true, bool numeric = true, bool special = true, int minLower = 0, int minUpper = 0, int minNumeric = 0, int minSpecial = 0) { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + var generatedPassword = new GenerateParameterDefault { MinLength = 22, // enough to give 128 bits of entropy when using the default 67 possible characters. See remarks in GenerateParameterDefault @@ -287,6 +316,9 @@ public static ParameterResource CreateDefaultPasswordParameter( public static ParameterResource CreateGeneratedParameter( IDistributedApplicationBuilder builder, string name, bool secret, GenerateParameterDefault parameterDefault) { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + var parameterResource = new ParameterResource(name, defaultValue => GetParameterValue(builder.Configuration, name, defaultValue), secret) { Default = parameterDefault diff --git a/src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs b/src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs index be0b4aeb4e..e63b78081a 100644 --- a/src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs @@ -60,6 +60,9 @@ public static class ProjectResourceBuilderExtensions /// public static IResourceBuilder AddProject(this IDistributedApplicationBuilder builder, [ResourceName] string name) where TProject : IProjectMetadata, new() { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + return builder.AddProject(name, _ => { }); } @@ -90,6 +93,10 @@ public static class ProjectResourceBuilderExtensions /// public static IResourceBuilder AddProject(this IDistributedApplicationBuilder builder, [ResourceName] string name, string projectPath) { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(projectPath); + return builder.AddProject(name, projectPath, _ => { }); } @@ -133,6 +140,9 @@ public static IResourceBuilder AddProject(this IDistributedAppl /// public static IResourceBuilder AddProject(this IDistributedApplicationBuilder builder, [ResourceName] string name, string? launchProfileName) where TProject : IProjectMetadata, new() { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + return builder.AddProject(name, options => { options.ExcludeLaunchProfile = launchProfileName is null; @@ -168,6 +178,10 @@ public static IResourceBuilder AddProject(this IDistributedAppl /// public static IResourceBuilder AddProject(this IDistributedApplicationBuilder builder, [ResourceName] string name, string projectPath, string? launchProfileName) { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(projectPath); + return builder.AddProject(name, projectPath, options => { options.ExcludeLaunchProfile = launchProfileName is null; @@ -213,6 +227,10 @@ public static IResourceBuilder AddProject(this IDistributedAppl /// public static IResourceBuilder AddProject(this IDistributedApplicationBuilder builder, [ResourceName] string name, Action configure) where TProject : IProjectMetadata, new() { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(configure); + var options = new ProjectResourceOptions(); configure(options); @@ -249,6 +267,11 @@ public static IResourceBuilder AddProject(this IDistributedAppl /// public static IResourceBuilder AddProject(this IDistributedApplicationBuilder builder, [ResourceName] string name, string projectPath, Action configure) { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(projectPath); + ArgumentNullException.ThrowIfNull(configure); + var options = new ProjectResourceOptions(); configure(options); @@ -539,6 +562,8 @@ EndpointAnnotation GetOrCreateEndpointForScheme(string scheme) /// public static IResourceBuilder WithReplicas(this IResourceBuilder builder, int replicas) { + ArgumentNullException.ThrowIfNull(builder); + builder.WithAnnotation(new ReplicaAnnotation(replicas)); return builder; } @@ -571,6 +596,8 @@ public static IResourceBuilder WithReplicas(this IResourceBuild /// public static IResourceBuilder DisableForwardedHeaders(this IResourceBuilder builder) { + ArgumentNullException.ThrowIfNull(builder); + builder.WithAnnotation(ResourceAnnotationMutationBehavior.Replace); return builder; } @@ -585,6 +612,9 @@ public static IResourceBuilder DisableForwardedHeaders(this IRe public static IResourceBuilder WithEndpointsInEnvironment( this IResourceBuilder builder, Func filter) { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(filter); + builder.Resource.Annotations.Add(new EndpointEnvironmentInjectionFilterAnnotation(filter)); return builder; diff --git a/src/Aspire.Hosting/PublicAPI.Shipped.txt b/src/Aspire.Hosting/PublicAPI.Shipped.txt index dcd312f1fa..993f54cb56 100644 --- a/src/Aspire.Hosting/PublicAPI.Shipped.txt +++ b/src/Aspire.Hosting/PublicAPI.Shipped.txt @@ -438,10 +438,8 @@ static Aspire.Hosting.ContainerResourceBuilderExtensions.WithContainerRuntimeArg static Aspire.Hosting.ContainerResourceBuilderExtensions.WithContainerRuntimeArgs(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, System.Func! callback) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.ContainerResourceBuilderExtensions.WithEntrypoint(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! entrypoint) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.ContainerResourceBuilderExtensions.WithImage(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! image, string! tag = "latest") -> Aspire.Hosting.ApplicationModel.IResourceBuilder! -static Aspire.Hosting.ContainerResourceBuilderExtensions.WithImageRegistry(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! registry) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.ContainerResourceBuilderExtensions.WithImageSHA256(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! sha256) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.ContainerResourceBuilderExtensions.WithImageTag(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! tag) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! -static Aspire.Hosting.ContainerResourceBuilderExtensions.WithVolume(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! name, string! target, bool isReadOnly = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.ContainerResourceBuilderExtensions.WithVolume(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! target) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.ContainerResourceExtensions.GetContainerResources(this Aspire.Hosting.ApplicationModel.DistributedApplicationModel! model) -> System.Collections.Generic.IEnumerable! static Aspire.Hosting.ContainerResourceExtensions.IsContainer(this Aspire.Hosting.ApplicationModel.IResource! resource) -> bool diff --git a/src/Aspire.Hosting/PublicAPI.Unshipped.txt b/src/Aspire.Hosting/PublicAPI.Unshipped.txt index 0c67fb7aaf..1207d8eeae 100644 --- a/src/Aspire.Hosting/PublicAPI.Unshipped.txt +++ b/src/Aspire.Hosting/PublicAPI.Unshipped.txt @@ -217,7 +217,9 @@ Aspire.Hosting.ApplicationModel.ResourceNotificationService.WaitForResourceAsync Aspire.Hosting.ApplicationModel.ResourceNotificationService.WaitForResourceAsync(string! resourceName, System.Collections.Generic.IEnumerable! targetStates, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! static Aspire.Hosting.ApplicationModel.ResourceExtensions.TryGetAnnotationsIncludingAncestorsOfType(this Aspire.Hosting.ApplicationModel.IResource! resource, out System.Collections.Generic.IEnumerable? result) -> bool static Aspire.Hosting.ContainerResourceBuilderExtensions.WithContainerName(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! name) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.ContainerResourceBuilderExtensions.WithImageRegistry(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string? registry) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.ContainerResourceBuilderExtensions.WithLifetime(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, Aspire.Hosting.ApplicationModel.ContainerLifetime lifetime) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.ContainerResourceBuilderExtensions.WithVolume(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string? name, string! target, bool isReadOnly = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.ParameterResourceBuilderExtensions.AddParameter(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, Aspire.Hosting.ApplicationModel.ParameterDefault! value, bool secret = false, bool persist = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.ParameterResourceBuilderExtensions.AddParameter(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, string! value, bool publishValueAsDefault = false, bool secret = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.ParameterResourceBuilderExtensions.AddParameter(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, System.Func! valueGetter, bool publishValueAsDefault = false, bool secret = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index c2457cbae1..4c6e44572f 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -28,6 +28,9 @@ public static class ResourceBuilderExtensions /// A resource configured with the specified environment variable. public static IResourceBuilder WithEnvironment(this IResourceBuilder builder, string name, string? value) where T : IResourceWithEnvironment { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + return builder.WithAnnotation(new EnvironmentAnnotation(name, value ?? string.Empty)); } @@ -42,6 +45,9 @@ public static IResourceBuilder WithEnvironment(this IResourceBuilder bu public static IResourceBuilder WithEnvironment(this IResourceBuilder builder, string name, in ReferenceExpression.ExpressionInterpolatedStringHandler value) where T : IResourceWithEnvironment { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + var expression = value.GetExpression(); return builder.WithEnvironment(context => @@ -61,6 +67,10 @@ public static IResourceBuilder WithEnvironment(this IResourceBuilder bu public static IResourceBuilder WithEnvironment(this IResourceBuilder builder, string name, ReferenceExpression value) where T : IResourceWithEnvironment { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(value); + return builder.WithEnvironment(context => { context.EnvironmentVariables[name] = value; @@ -77,6 +87,10 @@ public static IResourceBuilder WithEnvironment(this IResourceBuilder bu /// A resource configured with the specified environment variable. public static IResourceBuilder WithEnvironment(this IResourceBuilder builder, string name, Func callback) where T : IResourceWithEnvironment { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(callback); + return builder.WithAnnotation(new EnvironmentCallbackAnnotation(name, callback)); } @@ -89,6 +103,9 @@ public static IResourceBuilder WithEnvironment(this IResourceBuilder bu /// A resource configured with the environment variable callback. public static IResourceBuilder WithEnvironment(this IResourceBuilder builder, Action callback) where T : IResourceWithEnvironment { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(callback); + return builder.WithAnnotation(new EnvironmentCallbackAnnotation(callback)); } @@ -101,6 +118,9 @@ public static IResourceBuilder WithEnvironment(this IResourceBuilder bu /// A resource configured with the environment variable callback. public static IResourceBuilder WithEnvironment(this IResourceBuilder builder, Func callback) where T : IResourceWithEnvironment { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(callback); + return builder.WithAnnotation(new EnvironmentCallbackAnnotation(callback)); } @@ -114,6 +134,10 @@ public static IResourceBuilder WithEnvironment(this IResourceBuilder bu /// A resource configured with the environment variable callback. public static IResourceBuilder WithEnvironment(this IResourceBuilder builder, string name, EndpointReference endpointReference) where T : IResourceWithEnvironment { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(endpointReference); + return builder.WithEnvironment(context => { context.EnvironmentVariables[name] = endpointReference; @@ -130,6 +154,10 @@ public static IResourceBuilder WithEnvironment(this IResourceBuilder bu /// A resource configured with the environment variable callback. public static IResourceBuilder WithEnvironment(this IResourceBuilder builder, string name, IResourceBuilder parameter) where T : IResourceWithEnvironment { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(parameter); + return builder.WithEnvironment(context => { context.EnvironmentVariables[name] = parameter.Resource; @@ -150,6 +178,10 @@ public static IResourceBuilder WithEnvironment( IResourceBuilder resource) where T : IResourceWithEnvironment { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(envVarName); + ArgumentNullException.ThrowIfNull(resource); + return builder.WithEnvironment(context => { context.EnvironmentVariables[envVarName] = new ConnectionStringReference(resource.Resource, optional: false); @@ -165,6 +197,9 @@ public static IResourceBuilder WithEnvironment( /// The . public static IResourceBuilder WithArgs(this IResourceBuilder builder, params string[] args) where T : IResourceWithArgs { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(args); + return builder.WithArgs(context => context.Args.AddRange(args)); } @@ -177,6 +212,9 @@ public static IResourceBuilder WithArgs(this IResourceBuilder builder, /// The . public static IResourceBuilder WithArgs(this IResourceBuilder builder, params object[] args) where T : IResourceWithArgs { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(args); + return builder.WithArgs(context => context.Args.AddRange(args)); } @@ -189,6 +227,9 @@ public static IResourceBuilder WithArgs(this IResourceBuilder builder, /// The . public static IResourceBuilder WithArgs(this IResourceBuilder builder, Action callback) where T : IResourceWithArgs { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(callback); + return builder.WithArgs(context => { callback(context); @@ -205,6 +246,9 @@ public static IResourceBuilder WithArgs(this IResourceBuilder builder, /// The . public static IResourceBuilder WithArgs(this IResourceBuilder builder, Func callback) where T : IResourceWithArgs { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(callback); + return builder.WithAnnotation(new CommandLineArgsCallbackAnnotation(callback)); } @@ -217,6 +261,9 @@ public static IResourceBuilder WithArgs(this IResourceBuilder builder, /// A reference to the . public static IResourceBuilder WithManifestPublishingCallback(this IResourceBuilder builder, Action callback) where T : IResource { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(callback); + // You can only ever have one manifest publishing callback, so it must be a replace operation. return builder.WithAnnotation(new ManifestPublishingCallbackAnnotation(callback), ResourceAnnotationMutationBehavior.Replace); } @@ -230,6 +277,9 @@ public static IResourceBuilder WithManifestPublishingCallback(this IResour /// A reference to the . public static IResourceBuilder WithManifestPublishingCallback(this IResourceBuilder builder, Func callback) where T : IResource { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(callback); + // You can only ever have one manifest publishing callback, so it must be a replace operation. return builder.WithAnnotation(new ManifestPublishingCallbackAnnotation(callback), ResourceAnnotationMutationBehavior.Replace); } @@ -243,6 +293,9 @@ public static IResourceBuilder WithManifestPublishingCallback(this IResour /// A reference to the . public static IResourceBuilder WithConnectionStringRedirection(this IResourceBuilder builder, IResourceWithConnectionString resource) where T : IResourceWithConnectionString { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(resource); + // You can only ever have one manifest publishing callback, so it must be a replace operation. return builder.WithAnnotation(new ConnectionStringRedirectAnnotation(resource), ResourceAnnotationMutationBehavior.Replace); } @@ -290,6 +343,9 @@ private static Action CreateEndpointReferenceEnviron public static IResourceBuilder WithReference(this IResourceBuilder builder, IResourceBuilder source, string? connectionName = null, bool optional = false) where TDestination : IResourceWithEnvironment { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(source); + var resource = source.Resource; connectionName ??= resource.Name; @@ -312,6 +368,9 @@ public static IResourceBuilder WithReference(this IR public static IResourceBuilder WithReference(this IResourceBuilder builder, IResourceBuilder source) where TDestination : IResourceWithEnvironment { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(source); + ApplyEndpoints(builder, source.Resource); return builder; } @@ -328,6 +387,10 @@ public static IResourceBuilder WithReference(this IR public static IResourceBuilder WithReference(this IResourceBuilder builder, string name, Uri uri) where TDestination : IResourceWithEnvironment { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(uri); + if (!uri.IsAbsoluteUri) { throw new InvalidOperationException("The uri for service reference must be absolute."); @@ -352,6 +415,9 @@ public static IResourceBuilder WithReference(this IR public static IResourceBuilder WithReference(this IResourceBuilder builder, EndpointReference endpointReference) where TDestination : IResourceWithEnvironment { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(endpointReference); + ApplyEndpoints(builder, endpointReference.Resource, endpointReference.EndpointName); return builder; } @@ -399,6 +465,10 @@ private static void ApplyEndpoints(this IResourceBuilder builder, IResourc [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "")] public static IResourceBuilder WithEndpoint(this IResourceBuilder builder, [EndpointName] string endpointName, Action callback, bool createIfNotExists = true) where T : IResourceWithEndpoints { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(endpointName); + ArgumentNullException.ThrowIfNull(callback); + var endpoint = builder.Resource.Annotations .OfType() .Where(ea => StringComparers.EndpointAnnotationName.Equals(ea.Name, endpointName)) @@ -441,6 +511,8 @@ public static IResourceBuilder WithEndpoint(this IResourceBuilder build [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "")] public static IResourceBuilder WithEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, string? scheme = null, [EndpointName] string? name = null, string? env = null, bool isProxied = true, bool? isExternal = null) where T : IResourceWithEndpoints { + ArgumentNullException.ThrowIfNull(builder); + var annotation = new EndpointAnnotation( protocol: ProtocolType.Tcp, uriScheme: scheme, @@ -486,6 +558,8 @@ public static IResourceBuilder WithEndpoint(this IResourceBuilder build /// Throws an exception if an endpoint with the same name already exists on the specified resource. public static IResourceBuilder WithHttpEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, [EndpointName] string? name = null, string? env = null, bool isProxied = true) where T : IResourceWithEndpoints { + ArgumentNullException.ThrowIfNull(builder); + return builder.WithEndpoint(targetPort: targetPort, port: port, scheme: "http", name: name, env: env, isProxied: isProxied); } @@ -504,6 +578,8 @@ public static IResourceBuilder WithHttpEndpoint(this IResourceBuilder b /// Throws an exception if an endpoint with the same name already exists on the specified resource. public static IResourceBuilder WithHttpsEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, [EndpointName] string? name = null, string? env = null, bool isProxied = true) where T : IResourceWithEndpoints { + ArgumentNullException.ThrowIfNull(builder); + return builder.WithEndpoint(targetPort: targetPort, port: port, scheme: "https", name: name, env: env, isProxied: isProxied); } @@ -515,6 +591,8 @@ public static IResourceBuilder WithHttpsEndpoint(this IResourceBuilder /// public static IResourceBuilder WithExternalHttpEndpoints(this IResourceBuilder builder) where T : IResourceWithEndpoints { + ArgumentNullException.ThrowIfNull(builder); + if (!builder.Resource.TryGetAnnotationsOfType(out var endpoints)) { return builder; @@ -541,6 +619,8 @@ public static IResourceBuilder WithExternalHttpEndpoints(this IResourceBui /// An that can be used to resolve the address of the endpoint after resource allocation has occurred. public static EndpointReference GetEndpoint(this IResourceBuilder builder, [EndpointName] string name) where T : IResourceWithEndpoints { + ArgumentNullException.ThrowIfNull(builder); + return builder.Resource.GetEndpoint(name); } @@ -552,6 +632,8 @@ public static EndpointReference GetEndpoint(this IResourceBuilder builder, /// The . public static IResourceBuilder AsHttp2Service(this IResourceBuilder builder) where T : IResourceWithEndpoints { + ArgumentNullException.ThrowIfNull(builder); + return builder.WithAnnotation(new Http2ServiceAnnotation()); } @@ -563,6 +645,8 @@ public static IResourceBuilder AsHttp2Service(this IResourceBuilder bui /// The . public static IResourceBuilder ExcludeFromManifest(this IResourceBuilder builder) where T : IResource { + ArgumentNullException.ThrowIfNull(builder); + return builder.WithAnnotation(ManifestPublishingCallbackAnnotation.Ignore); } @@ -595,6 +679,9 @@ public static IResourceBuilder ExcludeFromManifest(this IResourceBuilder public static IResourceBuilder WaitFor(this IResourceBuilder builder, IResourceBuilder dependency) where T : IResourceWithWaitSupport { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(dependency); + if (builder.Resource as IResource == dependency.Resource) { throw new DistributedApplicationException($"The '{builder.Resource.Name}' resource cannot wait for itself."); @@ -645,6 +732,9 @@ public static IResourceBuilder WaitFor(this IResourceBuilder builder, I /// public static IResourceBuilder WaitForCompletion(this IResourceBuilder builder, IResourceBuilder dependency, int exitCode = 0) where T : IResourceWithWaitSupport { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(dependency); + if (builder.Resource as IResource == dependency.Resource) { throw new DistributedApplicationException($"The '{builder.Resource.Name}' resource cannot wait for itself."); @@ -696,6 +786,9 @@ public static IResourceBuilder WaitForCompletion(this IResourceBuilder /// public static IResourceBuilder WithHealthCheck(this IResourceBuilder builder, string key) where T : IResource { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(key); + if (builder.Resource.TryGetAnnotationsOfType(out var annotations) && annotations.Any(a => a.Key == key)) { throw new DistributedApplicationException($"Resource '{builder.Resource.Name}' already has a health check with key '{key}'."); @@ -737,6 +830,8 @@ public static IResourceBuilder WithHealthCheck(this IResourceBuilder bu /// public static IResourceBuilder WithHttpHealthCheck(this IResourceBuilder builder, string? path = null, int? statusCode = null, string? endpointName = null) where T : IResourceWithEndpoints { + ArgumentNullException.ThrowIfNull(builder); + endpointName = endpointName ?? "http"; return builder.WithHttpHealthCheckInternal( path: path, @@ -830,6 +925,8 @@ internal static IResourceBuilder WithHttpHealthCheckInternal(this IResourc /// public static IResourceBuilder WithHttpsHealthCheck(this IResourceBuilder builder, string? path = null, int? statusCode = null, string? endpointName = null) where T : IResourceWithEndpoints { + ArgumentNullException.ThrowIfNull(builder); + endpointName = endpointName ?? "https"; return builder.WithHttpHealthCheckInternal( path: path, @@ -887,6 +984,11 @@ public static IResourceBuilder WithCommand( IconVariant? iconVariant = null, bool isHighlighted = false) where T : IResource { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(displayName); + ArgumentNullException.ThrowIfNull(executeCommand); + // Replace existing annotation with the same name. var existingAnnotation = builder.Resource.Annotations.OfType().SingleOrDefault(a => a.Name == name); if (existingAnnotation != null) From 6758a8d5763eb7bf04ba6f674891b41964688bd8 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 29 Oct 2024 21:56:40 +0800 Subject: [PATCH 09/44] Add commands to stress playground (#6487) --- .../Stress/Stress.ApiService/Program.cs | 14 +++- .../Stress/Stress.ApiService/TraceCreator.cs | 4 +- playground/Stress/Stress.AppHost/Program.cs | 13 ++- .../ResourceBuilderExtensions.cs | 80 +++++++++++++++++++ 4 files changed, 103 insertions(+), 8 deletions(-) create mode 100644 playground/Stress/Stress.AppHost/ResourceBuilderExtensions.cs diff --git a/playground/Stress/Stress.ApiService/Program.cs b/playground/Stress/Stress.ApiService/Program.cs index bcf9429ebb..6d8d1cb0bf 100644 --- a/playground/Stress/Stress.ApiService/Program.cs +++ b/playground/Stress/Stress.ApiService/Program.cs @@ -95,13 +95,19 @@ return $"Sent requests to {string.Join(';', urls)}"; }); -app.MapGet("/log-message-limit", ([FromServices] ILogger logger) => +app.MapGet("/log-message-limit", async ([FromServices] ILogger logger) => { - const int LogCount = 20_000; + const int LogCount = 10_000; + const int BatchSize = 10; - for (var i = 0; i < LogCount; i++) + for (var i = 0; i < LogCount / BatchSize; i++) { - logger.LogInformation("Log entry {LogEntryIndex}", i); + for (var j = 0; j < BatchSize; j++) + { + logger.LogInformation("Log entry {BatchIndex}-{LogEntryIndex}", i, j); + } + + await Task.Delay(100); } return $"Created {LogCount} logs."; diff --git a/playground/Stress/Stress.ApiService/TraceCreator.cs b/playground/Stress/Stress.ApiService/TraceCreator.cs index c8b08c60a8..687b6f40fd 100644 --- a/playground/Stress/Stress.ApiService/TraceCreator.cs +++ b/playground/Stress/Stress.ApiService/TraceCreator.cs @@ -34,7 +34,7 @@ public async Task CreateTraceAsync(int count, bool createChildren) { var activityStack = new Stack(); - for (var i = 0; i < 10; i++) + for (var i = 0; i < count; i++) { if (i > 0) { @@ -54,8 +54,6 @@ public async Task CreateTraceAsync(int count, bool createChildren) { await CreateChildActivityAsync(name); } - - await Task.Delay(Random.Shared.Next(10, 50)); } while (activityStack.Count > 0) diff --git a/playground/Stress/Stress.AppHost/Program.cs b/playground/Stress/Stress.AppHost/Program.cs index 9c32f26064..987637bcb1 100644 --- a/playground/Stress/Stress.AppHost/Program.cs +++ b/playground/Stress/Stress.AppHost/Program.cs @@ -1,7 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Extensions.DependencyInjection; + var builder = DistributedApplication.CreateBuilder(args); +builder.Services.AddHttpClient(); for (var i = 0; i < 10; i++) { @@ -27,12 +30,20 @@ iconName: "CloudDatabase", isHighlighted: true); -for (var i = 0; i < 30; i++) +serviceBuilder.WithHttpEndpoint(5180, name: $"http"); +for (var i = 1; i <= 30; i++) { var port = 5180 + i; serviceBuilder.WithHttpEndpoint(port, name: $"http-{port}"); } +serviceBuilder.WithHttpCommand("/write-console", "Write to console", method: HttpMethod.Get, iconName: "ContentViewGalleryLightning"); +serviceBuilder.WithHttpCommand("/increment-counter", "Increment counter", method: HttpMethod.Get, iconName: "ContentViewGalleryLightning"); +serviceBuilder.WithHttpCommand("/big-trace", "Big trace", method: HttpMethod.Get, iconName: "ContentViewGalleryLightning"); +serviceBuilder.WithHttpCommand("/trace-limit", "Trace limit", method: HttpMethod.Get, iconName: "ContentViewGalleryLightning"); +serviceBuilder.WithHttpCommand("/log-message", "Log message", method: HttpMethod.Get, iconName: "ContentViewGalleryLightning"); +serviceBuilder.WithHttpCommand("/log-message-limit", "Log message limit", method: HttpMethod.Get, iconName: "ContentViewGalleryLightning"); + builder.AddProject("stress-telemetryservice"); #if !SKIP_DASHBOARD_REFERENCE diff --git a/playground/Stress/Stress.AppHost/ResourceBuilderExtensions.cs b/playground/Stress/Stress.AppHost/ResourceBuilderExtensions.cs new file mode 100644 index 0000000000..87b432b95c --- /dev/null +++ b/playground/Stress/Stress.AppHost/ResourceBuilderExtensions.cs @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; + +internal static class ResourceBuilderExtensions +{ + /// + /// Adds a command to the resource that sends an HTTP request to the specified path. + /// + public static IResourceBuilder WithHttpsCommand(this IResourceBuilder builder, + string path, + string displayName, + HttpMethod? method = default, + string? endpointName = default, + string? iconName = default) + where TResource : IResourceWithEndpoints + => WithHttpCommandImpl(builder, path, displayName, endpointName ?? "https", method, "https", iconName); + + /// + /// Adds a command to the resource that sends an HTTP request to the specified path. + /// + public static IResourceBuilder WithHttpCommand(this IResourceBuilder builder, + string path, + string displayName, + HttpMethod? method = default, + string? endpointName = default, + string? iconName = default) + where TResource : IResourceWithEndpoints + => WithHttpCommandImpl(builder, path, displayName, endpointName ?? "http", method, "http", iconName); + + private static IResourceBuilder WithHttpCommandImpl(this IResourceBuilder builder, + string path, + string displayName, + string endpointName, + HttpMethod? method, + string expectedScheme, + string? iconName = default) + where TResource : IResourceWithEndpoints + { + method ??= HttpMethod.Post; + + var endpoints = builder.Resource.GetEndpoints(); + var endpoint = endpoints.FirstOrDefault(e => string.Equals(e.EndpointName, endpointName, StringComparison.OrdinalIgnoreCase)) + ?? throw new DistributedApplicationException($"Could not create HTTP command for resource '{builder.Resource.Name}' as no endpoint named '{endpointName}' was found."); + + var commandType = $"http-{method.Method.ToLowerInvariant()}-{path.ToLowerInvariant()}-request"; + + builder.WithCommand(commandType, displayName, async context => + { + if (!endpoint.IsAllocated) + { + return new ExecuteCommandResult { Success = false, ErrorMessage = "Endpoints are not yet allocated." }; + } + + if (!string.Equals(endpoint.Scheme, expectedScheme, StringComparison.OrdinalIgnoreCase)) + { + return new ExecuteCommandResult { Success = false, ErrorMessage = $"The endpoint named '{endpointName}' on resource '{builder.Resource.Name}' does not have the expected scheme of '{expectedScheme}'." }; + } + + var uri = new UriBuilder(endpoint.Url) { Path = path }.Uri; + var httpClient = context.ServiceProvider.GetRequiredService().CreateClient(); + var request = new HttpRequestMessage(method, uri); + try + { + var response = await httpClient.SendAsync(request, context.CancellationToken); + response.EnsureSuccessStatusCode(); + } + catch (Exception ex) + { + return new ExecuteCommandResult { Success = false, ErrorMessage = ex.Message }; + } + return new ExecuteCommandResult { Success = true }; + }, + iconName: iconName, + iconVariant: IconVariant.Regular); + + return builder; + } +} From b76abf5a277ef8b51077f4916252fdd394bccd71 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Oct 2024 18:10:26 +0000 Subject: [PATCH 10/44] Update dependencies to latest versions (#6500) Update dependencies to latest versions - Microsoft.Azure.Cosmos to 3.45.0 - NATS.Net to 2.5.2 - Azure.Identity to 1.13.1 --- Directory.Packages.props | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 6e7fb51c15..c0a687d553 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -35,7 +35,7 @@ - + @@ -125,7 +125,7 @@ - + @@ -195,7 +195,7 @@ - + From 51e9a0d020eccf8e14fbc52cd4b0bc0d5cadddfe Mon Sep 17 00:00:00 2001 From: Adam Shortland Date: Tue, 29 Oct 2024 19:39:10 -0400 Subject: [PATCH 11/44] Makes VolumeNameGenerator public (#6531) * -Makes VolumeNameGenerator public * Update src/Aspire.Hosting/Utils/VolumeNameGenerator.cs Co-authored-by: Dan Moseley * Take @danmosely suggestion to change the exception message * Renames VolumeNameGenerator.CreateVolumeName to Generate * -Mark VolumeNameGenerator.Sanitize as internal * Pretty sure that should be all of it * Oops * Update src/Aspire.Hosting/Utils/VolumeNameGenerator.cs Co-authored-by: Damian Edwards * Drops code doc of Sanitize since it's not public --------- Co-authored-by: Dan Moseley Co-authored-by: Damian Edwards --- .../Aspire.Hosting.Azure.CosmosDB.csproj | 1 - .../AzureCosmosDBExtensions.cs | 2 +- .../Aspire.Hosting.Azure.EventHubs.csproj | 4 ---- .../AzureEventHubsExtensions.cs | 2 +- .../Aspire.Hosting.Azure.Storage.csproj | 6 +----- .../AzureStorageExtensions.cs | 2 +- .../Aspire.Hosting.Elasticsearch.csproj | 1 - .../ElasticsearchBuilderExtensions.cs | 2 +- .../Aspire.Hosting.Garnet.csproj | 4 ---- .../GarnetBuilderExtensions.cs | 2 +- .../Aspire.Hosting.Kafka.csproj | 6 ------ .../KafkaBuilderExtensions.cs | 2 +- .../Aspire.Hosting.Keycloak.csproj | 4 ---- .../KeycloakResourceBuilderExtensions.cs | 2 +- .../Aspire.Hosting.Milvus.csproj | 1 - .../MilvusBuilderExtensions.cs | 2 +- .../Aspire.Hosting.MongoDB.csproj | 1 - .../MongoDBBuilderExtensions.cs | 2 +- .../Aspire.Hosting.MySql.csproj | 1 - .../MySqlBuilderExtensions.cs | 2 +- .../Aspire.Hosting.Nats.csproj | 1 - .../NatsBuilderExtensions.cs | 2 +- .../Aspire.Hosting.Oracle.csproj | 1 - .../OracleDatabaseBuilderExtensions.cs | 2 +- .../Aspire.Hosting.PostgreSQL.csproj | 1 - .../PostgresBuilderExtensions.cs | 2 +- .../Aspire.Hosting.Qdrant.csproj | 1 - .../QdrantBuilderExtensions.cs | 2 +- .../Aspire.Hosting.RabbitMQ.csproj | 4 ---- .../RabbitMQBuilderExtensions.cs | 2 +- .../Aspire.Hosting.Redis.csproj | 4 ---- .../RedisBuilderExtensions.cs | 2 +- .../Aspire.Hosting.Seq.csproj | 4 ---- .../SeqBuilderExtensions.cs | 2 +- .../Aspire.Hosting.SqlServer.csproj | 1 - .../SqlServerBuilderExtensions.cs | 2 +- .../Aspire.Hosting.Valkey.csproj | 4 ---- .../ValkeyBuilderExtensions.cs | 2 +- src/Aspire.Hosting/Aspire.Hosting.csproj | 1 - src/Aspire.Hosting/PublicAPI.Unshipped.txt | 2 ++ .../Utils}/VolumeNameGenerator.cs | 20 ++++++++++++++----- .../AzureCosmosDBEmulatorFunctionalTests.cs | 2 +- .../ElasticsearchFunctionalTests.cs | 2 +- .../Aspire.Hosting.Garnet.Tests.csproj | 1 - .../GarnetFunctionalTests.cs | 2 +- .../Aspire.Hosting.Kafka.Tests.csproj | 1 - .../KafkaFunctionalTests.cs | 2 +- .../MilvusFunctionalTests.cs | 2 +- .../Aspire.Hosting.MongoDB.Tests.csproj | 1 - .../MongoDbFunctionalTests.cs | 2 +- .../MySqlFunctionalTests.cs | 2 +- .../Aspire.Hosting.Nats.Tests.csproj | 1 - .../NatsFunctionalTests.cs | 2 +- .../Aspire.Hosting.Oracle.Tests.csproj | 1 - .../OracleFunctionalTests.cs | 2 +- .../PostgresFunctionalTests.cs | 2 +- .../Aspire.Hosting.Qdrant.Tests.csproj | 1 - .../QdrantFunctionalTests.cs | 2 +- .../Aspire.Hosting.RabbitMQ.Tests.csproj | 1 - .../RabbitMQFunctionalTests.cs | 2 +- .../RedisFunctionalTests.cs | 2 +- .../Aspire.Hosting.SqlServer.Tests.csproj | 1 - .../SqlServerFunctionalTests.cs | 2 +- .../Utils/VolumeNameGeneratorTests.cs | 8 ++++---- .../Aspire.Hosting.Valkey.Tests.csproj | 1 - .../ValkeyFunctionalTests.cs | 2 +- 66 files changed, 56 insertions(+), 102 deletions(-) rename src/{Shared => Aspire.Hosting/Utils}/VolumeNameGenerator.cs (71%) diff --git a/src/Aspire.Hosting.Azure.CosmosDB/Aspire.Hosting.Azure.CosmosDB.csproj b/src/Aspire.Hosting.Azure.CosmosDB/Aspire.Hosting.Azure.CosmosDB.csproj index 3561f62d29..68eab3d8ea 100644 --- a/src/Aspire.Hosting.Azure.CosmosDB/Aspire.Hosting.Azure.CosmosDB.csproj +++ b/src/Aspire.Hosting.Azure.CosmosDB/Aspire.Hosting.Azure.CosmosDB.csproj @@ -15,7 +15,6 @@ - diff --git a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs index 6fbb1067b3..6a8b1c8298 100644 --- a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs +++ b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs @@ -184,7 +184,7 @@ static CosmosClient CreateCosmosClient(string connectionString) /// A builder for the . public static IResourceBuilder WithDataVolume(this IResourceBuilder builder, string? name = null) => builder.WithEnvironment("AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE", "true") - .WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "data"), "/tmp/cosmos/appdata", isReadOnly: false); + .WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), "/tmp/cosmos/appdata", isReadOnly: false); /// /// Configures the gateway port for the Azure Cosmos DB emulator. diff --git a/src/Aspire.Hosting.Azure.EventHubs/Aspire.Hosting.Azure.EventHubs.csproj b/src/Aspire.Hosting.Azure.EventHubs/Aspire.Hosting.Azure.EventHubs.csproj index c396206476..852f590fe3 100644 --- a/src/Aspire.Hosting.Azure.EventHubs/Aspire.Hosting.Azure.EventHubs.csproj +++ b/src/Aspire.Hosting.Azure.EventHubs/Aspire.Hosting.Azure.EventHubs.csproj @@ -12,10 +12,6 @@ 14 - - - - diff --git a/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs b/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs index 8cb4934eb1..2cf6549dd2 100644 --- a/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs +++ b/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs @@ -268,7 +268,7 @@ public static IResourceBuilder WithDataBindMount /// The name of the volume. Defaults to an auto-generated name based on the application and resource names. /// A builder for the . public static IResourceBuilder WithDataVolume(this IResourceBuilder builder, string? name = null) - => builder.WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "data"), "/data", isReadOnly: false); + => builder.WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), "/data", isReadOnly: false); /// /// Configures the gateway port for the Azure Event Hubs emulator. diff --git a/src/Aspire.Hosting.Azure.Storage/Aspire.Hosting.Azure.Storage.csproj b/src/Aspire.Hosting.Azure.Storage/Aspire.Hosting.Azure.Storage.csproj index b934c144be..2b1c529781 100644 --- a/src/Aspire.Hosting.Azure.Storage/Aspire.Hosting.Azure.Storage.csproj +++ b/src/Aspire.Hosting.Azure.Storage/Aspire.Hosting.Azure.Storage.csproj @@ -11,11 +11,7 @@ 96 - - - - - + diff --git a/src/Aspire.Hosting.Azure.Storage/AzureStorageExtensions.cs b/src/Aspire.Hosting.Azure.Storage/AzureStorageExtensions.cs index d34f2f466b..dfef9e7648 100644 --- a/src/Aspire.Hosting.Azure.Storage/AzureStorageExtensions.cs +++ b/src/Aspire.Hosting.Azure.Storage/AzureStorageExtensions.cs @@ -167,7 +167,7 @@ public static IResourceBuilder WithDataBindMount(t /// A flag that indicates if this is a read-only volume. /// A builder for the . public static IResourceBuilder WithDataVolume(this IResourceBuilder builder, string? name = null, bool isReadOnly = false) - => builder.WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "data"), "/data", isReadOnly); + => builder.WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), "/data", isReadOnly); /// /// Modifies the host port that the storage emulator listens on for blob requests. diff --git a/src/Aspire.Hosting.Elasticsearch/Aspire.Hosting.Elasticsearch.csproj b/src/Aspire.Hosting.Elasticsearch/Aspire.Hosting.Elasticsearch.csproj index 63fdf59765..7a8673380b 100644 --- a/src/Aspire.Hosting.Elasticsearch/Aspire.Hosting.Elasticsearch.csproj +++ b/src/Aspire.Hosting.Elasticsearch/Aspire.Hosting.Elasticsearch.csproj @@ -13,7 +13,6 @@ - diff --git a/src/Aspire.Hosting.Elasticsearch/ElasticsearchBuilderExtensions.cs b/src/Aspire.Hosting.Elasticsearch/ElasticsearchBuilderExtensions.cs index 27e3ee3cf7..02b884a500 100644 --- a/src/Aspire.Hosting.Elasticsearch/ElasticsearchBuilderExtensions.cs +++ b/src/Aspire.Hosting.Elasticsearch/ElasticsearchBuilderExtensions.cs @@ -117,7 +117,7 @@ public static IResourceBuilder WithDataVolume(this IResou { ArgumentNullException.ThrowIfNull(builder); - return builder.WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "data"), "/usr/share/elasticsearch/data"); + return builder.WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), "/usr/share/elasticsearch/data"); } /// diff --git a/src/Aspire.Hosting.Garnet/Aspire.Hosting.Garnet.csproj b/src/Aspire.Hosting.Garnet/Aspire.Hosting.Garnet.csproj index 2d519865be..c86cd3414e 100644 --- a/src/Aspire.Hosting.Garnet/Aspire.Hosting.Garnet.csproj +++ b/src/Aspire.Hosting.Garnet/Aspire.Hosting.Garnet.csproj @@ -14,10 +14,6 @@ 92 - - - - diff --git a/src/Aspire.Hosting.Garnet/GarnetBuilderExtensions.cs b/src/Aspire.Hosting.Garnet/GarnetBuilderExtensions.cs index 3e419819c7..89d1b5eb6e 100644 --- a/src/Aspire.Hosting.Garnet/GarnetBuilderExtensions.cs +++ b/src/Aspire.Hosting.Garnet/GarnetBuilderExtensions.cs @@ -105,7 +105,7 @@ public static IResourceBuilder WithDataVolume(this IResourceBuil { ArgumentNullException.ThrowIfNull(builder); - builder.WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "data"), GarnetContainerDataDirectory, + builder.WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), GarnetContainerDataDirectory, isReadOnly); if (!isReadOnly) { diff --git a/src/Aspire.Hosting.Kafka/Aspire.Hosting.Kafka.csproj b/src/Aspire.Hosting.Kafka/Aspire.Hosting.Kafka.csproj index fb7a53c1aa..2c758bb462 100644 --- a/src/Aspire.Hosting.Kafka/Aspire.Hosting.Kafka.csproj +++ b/src/Aspire.Hosting.Kafka/Aspire.Hosting.Kafka.csproj @@ -11,12 +11,6 @@ 25 - - - - - - diff --git a/src/Aspire.Hosting.Kafka/KafkaBuilderExtensions.cs b/src/Aspire.Hosting.Kafka/KafkaBuilderExtensions.cs index 5c4dff9aa4..256682cbf4 100644 --- a/src/Aspire.Hosting.Kafka/KafkaBuilderExtensions.cs +++ b/src/Aspire.Hosting.Kafka/KafkaBuilderExtensions.cs @@ -178,7 +178,7 @@ public static IResourceBuilder WithDataVolume(this IResourc return builder .WithEnvironment(ConfigureLogDirs) - .WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "data"), Target, isReadOnly); + .WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), Target, isReadOnly); } /// diff --git a/src/Aspire.Hosting.Keycloak/Aspire.Hosting.Keycloak.csproj b/src/Aspire.Hosting.Keycloak/Aspire.Hosting.Keycloak.csproj index 66241918be..7caeba6d97 100644 --- a/src/Aspire.Hosting.Keycloak/Aspire.Hosting.Keycloak.csproj +++ b/src/Aspire.Hosting.Keycloak/Aspire.Hosting.Keycloak.csproj @@ -11,10 +11,6 @@ 80 - - - - diff --git a/src/Aspire.Hosting.Keycloak/KeycloakResourceBuilderExtensions.cs b/src/Aspire.Hosting.Keycloak/KeycloakResourceBuilderExtensions.cs index 8696fe25cc..7363ee70c0 100644 --- a/src/Aspire.Hosting.Keycloak/KeycloakResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting.Keycloak/KeycloakResourceBuilderExtensions.cs @@ -99,7 +99,7 @@ public static IResourceBuilder WithDataVolume(this IResourceBu { ArgumentNullException.ThrowIfNull(builder); - return builder.WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "data"), "/opt/keycloak/data", + return builder.WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), "/opt/keycloak/data", false); } diff --git a/src/Aspire.Hosting.Milvus/Aspire.Hosting.Milvus.csproj b/src/Aspire.Hosting.Milvus/Aspire.Hosting.Milvus.csproj index 81e5c37030..51ad651575 100644 --- a/src/Aspire.Hosting.Milvus/Aspire.Hosting.Milvus.csproj +++ b/src/Aspire.Hosting.Milvus/Aspire.Hosting.Milvus.csproj @@ -18,7 +18,6 @@ - diff --git a/src/Aspire.Hosting.Milvus/MilvusBuilderExtensions.cs b/src/Aspire.Hosting.Milvus/MilvusBuilderExtensions.cs index 5a02375604..c88eeb8375 100644 --- a/src/Aspire.Hosting.Milvus/MilvusBuilderExtensions.cs +++ b/src/Aspire.Hosting.Milvus/MilvusBuilderExtensions.cs @@ -156,7 +156,7 @@ public static IResourceBuilder WithAttu(this IResourceBuilder builder, public static IResourceBuilder WithDataVolume(this IResourceBuilder builder, string? name = null, bool isReadOnly = false) { ArgumentNullException.ThrowIfNull(builder); - return builder.WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "data"), "/var/lib/milvus", isReadOnly); + return builder.WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), "/var/lib/milvus", isReadOnly); } /// diff --git a/src/Aspire.Hosting.MongoDB/Aspire.Hosting.MongoDB.csproj b/src/Aspire.Hosting.MongoDB/Aspire.Hosting.MongoDB.csproj index 2558c18177..b4bca67905 100644 --- a/src/Aspire.Hosting.MongoDB/Aspire.Hosting.MongoDB.csproj +++ b/src/Aspire.Hosting.MongoDB/Aspire.Hosting.MongoDB.csproj @@ -15,7 +15,6 @@ - diff --git a/src/Aspire.Hosting.MongoDB/MongoDBBuilderExtensions.cs b/src/Aspire.Hosting.MongoDB/MongoDBBuilderExtensions.cs index f4d7539909..e6170e672f 100644 --- a/src/Aspire.Hosting.MongoDB/MongoDBBuilderExtensions.cs +++ b/src/Aspire.Hosting.MongoDB/MongoDBBuilderExtensions.cs @@ -179,7 +179,7 @@ public static IResourceBuilder WithDataVolume(this IResou { ArgumentNullException.ThrowIfNull(builder); - return builder.WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "data"), "/data/db", isReadOnly); + return builder.WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), "/data/db", isReadOnly); } /// diff --git a/src/Aspire.Hosting.MySql/Aspire.Hosting.MySql.csproj b/src/Aspire.Hosting.MySql/Aspire.Hosting.MySql.csproj index f9ea708cb4..76cd66b686 100644 --- a/src/Aspire.Hosting.MySql/Aspire.Hosting.MySql.csproj +++ b/src/Aspire.Hosting.MySql/Aspire.Hosting.MySql.csproj @@ -14,7 +14,6 @@ - diff --git a/src/Aspire.Hosting.MySql/MySqlBuilderExtensions.cs b/src/Aspire.Hosting.MySql/MySqlBuilderExtensions.cs index b44c0dd6bc..fa1cfea5df 100644 --- a/src/Aspire.Hosting.MySql/MySqlBuilderExtensions.cs +++ b/src/Aspire.Hosting.MySql/MySqlBuilderExtensions.cs @@ -203,7 +203,7 @@ public static IResourceBuilder WithDataVolume(this IResourc { ArgumentNullException.ThrowIfNull(builder); - return builder.WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "data"), "/var/lib/mysql", isReadOnly); + return builder.WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), "/var/lib/mysql", isReadOnly); } /// diff --git a/src/Aspire.Hosting.Nats/Aspire.Hosting.Nats.csproj b/src/Aspire.Hosting.Nats/Aspire.Hosting.Nats.csproj index a2c0ab9640..a56566f9b3 100644 --- a/src/Aspire.Hosting.Nats/Aspire.Hosting.Nats.csproj +++ b/src/Aspire.Hosting.Nats/Aspire.Hosting.Nats.csproj @@ -13,7 +13,6 @@ - diff --git a/src/Aspire.Hosting.Nats/NatsBuilderExtensions.cs b/src/Aspire.Hosting.Nats/NatsBuilderExtensions.cs index 9bb1d07da6..145ba29202 100644 --- a/src/Aspire.Hosting.Nats/NatsBuilderExtensions.cs +++ b/src/Aspire.Hosting.Nats/NatsBuilderExtensions.cs @@ -110,7 +110,7 @@ public static IResourceBuilder WithDataVolume(this IResource { ArgumentNullException.ThrowIfNull(builder); - return builder.WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "data"), "/var/lib/nats", + return builder.WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), "/var/lib/nats", isReadOnly) .WithArgs("-sd", "/var/lib/nats"); } diff --git a/src/Aspire.Hosting.Oracle/Aspire.Hosting.Oracle.csproj b/src/Aspire.Hosting.Oracle/Aspire.Hosting.Oracle.csproj index 4a166988e7..be38c931b7 100644 --- a/src/Aspire.Hosting.Oracle/Aspire.Hosting.Oracle.csproj +++ b/src/Aspire.Hosting.Oracle/Aspire.Hosting.Oracle.csproj @@ -13,7 +13,6 @@ - diff --git a/src/Aspire.Hosting.Oracle/OracleDatabaseBuilderExtensions.cs b/src/Aspire.Hosting.Oracle/OracleDatabaseBuilderExtensions.cs index 4eb232e895..9fdf990488 100644 --- a/src/Aspire.Hosting.Oracle/OracleDatabaseBuilderExtensions.cs +++ b/src/Aspire.Hosting.Oracle/OracleDatabaseBuilderExtensions.cs @@ -82,7 +82,7 @@ public static IResourceBuilder AddDatabase(this IResourc /// The name of the volume. Defaults to an auto-generated name based on the application and resource names. /// The . public static IResourceBuilder WithDataVolume(this IResourceBuilder builder, string? name = null) - => builder.WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "data"), "/opt/oracle/oradata", false); + => builder.WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), "/opt/oracle/oradata", false); /// /// Adds a bind mount for the data folder to a Oracle Database server container resource. diff --git a/src/Aspire.Hosting.PostgreSQL/Aspire.Hosting.PostgreSQL.csproj b/src/Aspire.Hosting.PostgreSQL/Aspire.Hosting.PostgreSQL.csproj index 5aeaa9682c..ddfe2b6113 100644 --- a/src/Aspire.Hosting.PostgreSQL/Aspire.Hosting.PostgreSQL.csproj +++ b/src/Aspire.Hosting.PostgreSQL/Aspire.Hosting.PostgreSQL.csproj @@ -14,7 +14,6 @@ - diff --git a/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs b/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs index e588c37c51..1ebece067c 100644 --- a/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs +++ b/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs @@ -357,7 +357,7 @@ public static IResourceBuilder WithDataVolume(this IReso { ArgumentNullException.ThrowIfNull(builder); - return builder.WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "data"), + return builder.WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), "/var/lib/postgresql/data", isReadOnly); } diff --git a/src/Aspire.Hosting.Qdrant/Aspire.Hosting.Qdrant.csproj b/src/Aspire.Hosting.Qdrant/Aspire.Hosting.Qdrant.csproj index ce2929322e..27699a9b97 100644 --- a/src/Aspire.Hosting.Qdrant/Aspire.Hosting.Qdrant.csproj +++ b/src/Aspire.Hosting.Qdrant/Aspire.Hosting.Qdrant.csproj @@ -14,7 +14,6 @@ - diff --git a/src/Aspire.Hosting.Qdrant/QdrantBuilderExtensions.cs b/src/Aspire.Hosting.Qdrant/QdrantBuilderExtensions.cs index de9f5c584b..f92ca39021 100644 --- a/src/Aspire.Hosting.Qdrant/QdrantBuilderExtensions.cs +++ b/src/Aspire.Hosting.Qdrant/QdrantBuilderExtensions.cs @@ -99,7 +99,7 @@ public static IResourceBuilder WithDataVolume(this IResour { ArgumentNullException.ThrowIfNull(builder); - return builder.WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "data"), "/qdrant/storage", + return builder.WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), "/qdrant/storage", isReadOnly); } diff --git a/src/Aspire.Hosting.RabbitMQ/Aspire.Hosting.RabbitMQ.csproj b/src/Aspire.Hosting.RabbitMQ/Aspire.Hosting.RabbitMQ.csproj index 6840d6544f..e505538143 100644 --- a/src/Aspire.Hosting.RabbitMQ/Aspire.Hosting.RabbitMQ.csproj +++ b/src/Aspire.Hosting.RabbitMQ/Aspire.Hosting.RabbitMQ.csproj @@ -11,10 +11,6 @@ 59 - - - - diff --git a/src/Aspire.Hosting.RabbitMQ/RabbitMQBuilderExtensions.cs b/src/Aspire.Hosting.RabbitMQ/RabbitMQBuilderExtensions.cs index 96c6c37e59..b6f9c7bbe6 100644 --- a/src/Aspire.Hosting.RabbitMQ/RabbitMQBuilderExtensions.cs +++ b/src/Aspire.Hosting.RabbitMQ/RabbitMQBuilderExtensions.cs @@ -85,7 +85,7 @@ public static IResourceBuilder WithDataVolume(this IReso { ArgumentNullException.ThrowIfNull(builder); - return builder.WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "data"), "/var/lib/rabbitmq", isReadOnly) + return builder.WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), "/var/lib/rabbitmq", isReadOnly) .RunWithStableNodeName(); } diff --git a/src/Aspire.Hosting.Redis/Aspire.Hosting.Redis.csproj b/src/Aspire.Hosting.Redis/Aspire.Hosting.Redis.csproj index 0a67848cd7..0e640c2c0b 100644 --- a/src/Aspire.Hosting.Redis/Aspire.Hosting.Redis.csproj +++ b/src/Aspire.Hosting.Redis/Aspire.Hosting.Redis.csproj @@ -12,10 +12,6 @@ 90 - - - - diff --git a/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs b/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs index f08f348294..f3e5ff521d 100644 --- a/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs +++ b/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs @@ -370,7 +370,7 @@ public static IResourceBuilder WithDataVolume(this IResourceBuild { ArgumentNullException.ThrowIfNull(builder); - builder.WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "data"), "/data", isReadOnly); + builder.WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), "/data", isReadOnly); if (!isReadOnly) { builder.WithPersistence(); diff --git a/src/Aspire.Hosting.Seq/Aspire.Hosting.Seq.csproj b/src/Aspire.Hosting.Seq/Aspire.Hosting.Seq.csproj index 86226702b8..4f84660205 100644 --- a/src/Aspire.Hosting.Seq/Aspire.Hosting.Seq.csproj +++ b/src/Aspire.Hosting.Seq/Aspire.Hosting.Seq.csproj @@ -12,10 +12,6 @@ 100 - - - - diff --git a/src/Aspire.Hosting.Seq/SeqBuilderExtensions.cs b/src/Aspire.Hosting.Seq/SeqBuilderExtensions.cs index 90693a1247..a6c1039989 100644 --- a/src/Aspire.Hosting.Seq/SeqBuilderExtensions.cs +++ b/src/Aspire.Hosting.Seq/SeqBuilderExtensions.cs @@ -48,7 +48,7 @@ public static IResourceBuilder AddSeq( /// A flag that indicates if this is a read-only volume. /// The . public static IResourceBuilder WithDataVolume(this IResourceBuilder builder, string? name = null, bool isReadOnly = false) - => builder.WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "data"), SeqContainerDataDirectory, isReadOnly); + => builder.WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), SeqContainerDataDirectory, isReadOnly); /// /// Adds a bind mount for the data folder to a Seq container resource. diff --git a/src/Aspire.Hosting.SqlServer/Aspire.Hosting.SqlServer.csproj b/src/Aspire.Hosting.SqlServer/Aspire.Hosting.SqlServer.csproj index 0869f8378c..644b7e8802 100644 --- a/src/Aspire.Hosting.SqlServer/Aspire.Hosting.SqlServer.csproj +++ b/src/Aspire.Hosting.SqlServer/Aspire.Hosting.SqlServer.csproj @@ -14,7 +14,6 @@ - diff --git a/src/Aspire.Hosting.SqlServer/SqlServerBuilderExtensions.cs b/src/Aspire.Hosting.SqlServer/SqlServerBuilderExtensions.cs index 562db549d0..ce52227cd1 100644 --- a/src/Aspire.Hosting.SqlServer/SqlServerBuilderExtensions.cs +++ b/src/Aspire.Hosting.SqlServer/SqlServerBuilderExtensions.cs @@ -91,7 +91,7 @@ public static IResourceBuilder WithDataVolume(this IRes { ArgumentNullException.ThrowIfNull(builder); - return builder.WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "data"), "/var/opt/mssql", isReadOnly); + return builder.WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), "/var/opt/mssql", isReadOnly); } /// diff --git a/src/Aspire.Hosting.Valkey/Aspire.Hosting.Valkey.csproj b/src/Aspire.Hosting.Valkey/Aspire.Hosting.Valkey.csproj index 751d885526..8614cd03b2 100644 --- a/src/Aspire.Hosting.Valkey/Aspire.Hosting.Valkey.csproj +++ b/src/Aspire.Hosting.Valkey/Aspire.Hosting.Valkey.csproj @@ -13,10 +13,6 @@ 92 - - - - diff --git a/src/Aspire.Hosting.Valkey/ValkeyBuilderExtensions.cs b/src/Aspire.Hosting.Valkey/ValkeyBuilderExtensions.cs index 870f2d58dc..8b11fd5b00 100644 --- a/src/Aspire.Hosting.Valkey/ValkeyBuilderExtensions.cs +++ b/src/Aspire.Hosting.Valkey/ValkeyBuilderExtensions.cs @@ -102,7 +102,7 @@ public static IResourceBuilder AddValkey(this IDistributedApplic public static IResourceBuilder WithDataVolume(this IResourceBuilder builder, string? name = null, bool isReadOnly = false) { - builder.WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "data"), ValkeyContainerDataDirectory, + builder.WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), ValkeyContainerDataDirectory, isReadOnly); if (!isReadOnly) { diff --git a/src/Aspire.Hosting/Aspire.Hosting.csproj b/src/Aspire.Hosting/Aspire.Hosting.csproj index c77a4c6c2a..431c998c39 100644 --- a/src/Aspire.Hosting/Aspire.Hosting.csproj +++ b/src/Aspire.Hosting/Aspire.Hosting.csproj @@ -27,7 +27,6 @@ - diff --git a/src/Aspire.Hosting/PublicAPI.Unshipped.txt b/src/Aspire.Hosting/PublicAPI.Unshipped.txt index 1207d8eeae..285a3168b4 100644 --- a/src/Aspire.Hosting/PublicAPI.Unshipped.txt +++ b/src/Aspire.Hosting/PublicAPI.Unshipped.txt @@ -211,6 +211,7 @@ Aspire.Hosting.LaunchSettings Aspire.Hosting.LaunchSettings.LaunchSettings() -> void Aspire.Hosting.LaunchSettings.Profiles.get -> System.Collections.Generic.Dictionary! Aspire.Hosting.LaunchSettings.Profiles.set -> void +Aspire.Hosting.Utils.VolumeNameGenerator static Aspire.Hosting.ApplicationModel.CommandResults.Success() -> Aspire.Hosting.ApplicationModel.ExecuteCommandResult! static Aspire.Hosting.ApplicationModel.ResourceExtensions.GetEnvironmentVariableValuesAsync(this Aspire.Hosting.ApplicationModel.IResourceWithEnvironment! resource, Aspire.Hosting.DistributedApplicationOperation applicationOperation = Aspire.Hosting.DistributedApplicationOperation.Run) -> System.Threading.Tasks.ValueTask!> Aspire.Hosting.ApplicationModel.ResourceNotificationService.WaitForResourceAsync(string! resourceName, string? targetState = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! @@ -238,6 +239,7 @@ static Aspire.Hosting.ResourceBuilderExtensions.WithCommand(this Aspire.Hosti static Aspire.Hosting.ResourceBuilderExtensions.WithHealthCheck(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! key) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.ResourceBuilderExtensions.WithHttpHealthCheck(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string? path = null, int? statusCode = null, string? endpointName = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.ResourceBuilderExtensions.WithHttpsHealthCheck(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string? path = null, int? statusCode = null, string? endpointName = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.Utils.VolumeNameGenerator.Generate(Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! suffix) -> string! static readonly Aspire.Hosting.ApplicationModel.KnownResourceStates.Exited -> string! static readonly Aspire.Hosting.ApplicationModel.KnownResourceStates.FailedToStart -> string! static readonly Aspire.Hosting.ApplicationModel.KnownResourceStates.Finished -> string! diff --git a/src/Shared/VolumeNameGenerator.cs b/src/Aspire.Hosting/Utils/VolumeNameGenerator.cs similarity index 71% rename from src/Shared/VolumeNameGenerator.cs rename to src/Aspire.Hosting/Utils/VolumeNameGenerator.cs index adefee3cf8..b1e19464af 100644 --- a/src/Shared/VolumeNameGenerator.cs +++ b/src/Aspire.Hosting/Utils/VolumeNameGenerator.cs @@ -5,16 +5,26 @@ namespace Aspire.Hosting.Utils; -internal static class VolumeNameGenerator +/// +/// Utility class for generating volume names. +/// +public static class VolumeNameGenerator { - public static string CreateVolumeName(IResourceBuilder builder, string suffix) where T : IResource + /// + /// Generates a volume name with the form {applicationName}-{sha256 of apphost path}-{resourceName}-{suffix}, e.g. myapplication-a345f2451-postgres-data. + /// + /// The resource type. + /// The resource builder. + /// The suffix to append to the volume name. + /// The volume name. + /// + public static string Generate(IResourceBuilder builder, string suffix) where T : IResource { if (!HasOnlyValidChars(suffix)) { - throw new ArgumentException($"The suffix '{suffix}' contains invalid characters. Only [a-zA-Z0-9_.-] are allowed.", nameof(suffix)); + throw new ArgumentException($"The suffix '{suffix}' contains invalid characters. It must match [a-zA-Z0-9][a-zA-Z0-9_.-]*.", nameof(suffix)); } - // Creates a volume name with the form < c > $"{applicationName}-{sha256 of apphost path}-{resourceName}-{suffix}, e.g. "myapplication-a345f2451-postgres-data". // Create volume name like "{Sanitize(appname).Lower()}-{sha256.Lower()}-postgres-data" // Compute a short hash of the content root path to differentiate between multiple AppHost projects with similar volume names @@ -24,7 +34,7 @@ public static string CreateVolumeName(IResourceBuilder builder, string suf return $"{safeApplicationName}-{applicationHash}-{resourceName}-{suffix}"; } - public static string Sanitize(string name) + internal static string Sanitize(string name) { return string.Create(name.Length, name, static (s, name) => { diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureCosmosDBEmulatorFunctionalTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureCosmosDBEmulatorFunctionalTests.cs index eae1d8c5b2..de5199ebac 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureCosmosDBEmulatorFunctionalTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureCosmosDBEmulatorFunctionalTests.cs @@ -142,7 +142,7 @@ public async Task WithDataVolumeShouldPersistStateBetweenUsages() var cosmos1 = builder1.AddAzureCosmosDB("cosmos"); // Use a deterministic volume name to prevent them from exhausting the machines if deletion fails - var volumeName = VolumeNameGenerator.CreateVolumeName(cosmos1, nameof(WithDataVolumeShouldPersistStateBetweenUsages)); + var volumeName = VolumeNameGenerator.Generate(cosmos1, nameof(WithDataVolumeShouldPersistStateBetweenUsages)); var db1 = cosmos1.AddDatabase(databaseName) .RunAsEmulator(emulator => emulator.WithDataVolume(volumeName)); diff --git a/tests/Aspire.Hosting.Elasticsearch.Tests/ElasticsearchFunctionalTests.cs b/tests/Aspire.Hosting.Elasticsearch.Tests/ElasticsearchFunctionalTests.cs index fd0f38b7e8..fca3e81942 100644 --- a/tests/Aspire.Hosting.Elasticsearch.Tests/ElasticsearchFunctionalTests.cs +++ b/tests/Aspire.Hosting.Elasticsearch.Tests/ElasticsearchFunctionalTests.cs @@ -87,7 +87,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) if (useVolume) { // Use a deterministic volume name to prevent them from exhausting the machines if deletion fails - volumeName = VolumeNameGenerator.CreateVolumeName(elasticsearch1, nameof(WithDataShouldPersistStateBetweenUsages)); + volumeName = VolumeNameGenerator.Generate(elasticsearch1, nameof(WithDataShouldPersistStateBetweenUsages)); // if the volume already exists (because of a crashing previous run), delete it DockerUtils.AttemptDeleteDockerVolume(volumeName, throwOnFailure: true); diff --git a/tests/Aspire.Hosting.Garnet.Tests/Aspire.Hosting.Garnet.Tests.csproj b/tests/Aspire.Hosting.Garnet.Tests/Aspire.Hosting.Garnet.Tests.csproj index 3b8be43549..0ee73d4b90 100644 --- a/tests/Aspire.Hosting.Garnet.Tests/Aspire.Hosting.Garnet.Tests.csproj +++ b/tests/Aspire.Hosting.Garnet.Tests/Aspire.Hosting.Garnet.Tests.csproj @@ -15,7 +15,6 @@ - diff --git a/tests/Aspire.Hosting.Garnet.Tests/GarnetFunctionalTests.cs b/tests/Aspire.Hosting.Garnet.Tests/GarnetFunctionalTests.cs index b796438ff5..9d0a679747 100644 --- a/tests/Aspire.Hosting.Garnet.Tests/GarnetFunctionalTests.cs +++ b/tests/Aspire.Hosting.Garnet.Tests/GarnetFunctionalTests.cs @@ -119,7 +119,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) if (useVolume) { // Use a deterministic volume name to prevent them from exhausting the machines if deletion fails - volumeName = VolumeNameGenerator.CreateVolumeName(garnet1, nameof(WithDataShouldPersistStateBetweenUsages)); + volumeName = VolumeNameGenerator.Generate(garnet1, nameof(WithDataShouldPersistStateBetweenUsages)); // if the volume already exists (because of a crashing previous run), try to delete it DockerUtils.AttemptDeleteDockerVolume(volumeName); diff --git a/tests/Aspire.Hosting.Kafka.Tests/Aspire.Hosting.Kafka.Tests.csproj b/tests/Aspire.Hosting.Kafka.Tests/Aspire.Hosting.Kafka.Tests.csproj index 72b4c7c5fa..6a2636a474 100644 --- a/tests/Aspire.Hosting.Kafka.Tests/Aspire.Hosting.Kafka.Tests.csproj +++ b/tests/Aspire.Hosting.Kafka.Tests/Aspire.Hosting.Kafka.Tests.csproj @@ -12,7 +12,6 @@ - diff --git a/tests/Aspire.Hosting.Kafka.Tests/KafkaFunctionalTests.cs b/tests/Aspire.Hosting.Kafka.Tests/KafkaFunctionalTests.cs index dcb94c5c65..2792d599e9 100644 --- a/tests/Aspire.Hosting.Kafka.Tests/KafkaFunctionalTests.cs +++ b/tests/Aspire.Hosting.Kafka.Tests/KafkaFunctionalTests.cs @@ -130,7 +130,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) if (useVolume) { // Use a deterministic volume name to prevent them from exhausting the machines if deletion fails - volumeName = VolumeNameGenerator.CreateVolumeName(kafka1, nameof(WithDataShouldPersistStateBetweenUsages)); + volumeName = VolumeNameGenerator.Generate(kafka1, nameof(WithDataShouldPersistStateBetweenUsages)); // if the volume already exists (because of a crashing previous run), delete it DockerUtils.AttemptDeleteDockerVolume(volumeName, throwOnFailure: true); diff --git a/tests/Aspire.Hosting.Milvus.Tests/MilvusFunctionalTests.cs b/tests/Aspire.Hosting.Milvus.Tests/MilvusFunctionalTests.cs index fb404a8023..115883aa57 100644 --- a/tests/Aspire.Hosting.Milvus.Tests/MilvusFunctionalTests.cs +++ b/tests/Aspire.Hosting.Milvus.Tests/MilvusFunctionalTests.cs @@ -85,7 +85,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) if (useVolume) { // Use a deterministic volume name to prevent them from exhausting the machines if deletion fails - volumeName = VolumeNameGenerator.CreateVolumeName(milvus1, nameof(WithDataShouldPersistStateBetweenUsages)); + volumeName = VolumeNameGenerator.Generate(milvus1, nameof(WithDataShouldPersistStateBetweenUsages)); // if the volume already exists (because of a crashing previous run), delete it DockerUtils.AttemptDeleteDockerVolume(volumeName, throwOnFailure: true); diff --git a/tests/Aspire.Hosting.MongoDB.Tests/Aspire.Hosting.MongoDB.Tests.csproj b/tests/Aspire.Hosting.MongoDB.Tests/Aspire.Hosting.MongoDB.Tests.csproj index 453ed447f6..ff78511994 100644 --- a/tests/Aspire.Hosting.MongoDB.Tests/Aspire.Hosting.MongoDB.Tests.csproj +++ b/tests/Aspire.Hosting.MongoDB.Tests/Aspire.Hosting.MongoDB.Tests.csproj @@ -14,7 +14,6 @@ - diff --git a/tests/Aspire.Hosting.MongoDB.Tests/MongoDbFunctionalTests.cs b/tests/Aspire.Hosting.MongoDB.Tests/MongoDbFunctionalTests.cs index 4f29fddf9a..5680a2e57a 100644 --- a/tests/Aspire.Hosting.MongoDB.Tests/MongoDbFunctionalTests.cs +++ b/tests/Aspire.Hosting.MongoDB.Tests/MongoDbFunctionalTests.cs @@ -127,7 +127,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) if (useVolume) { // Use a deterministic volume name to prevent them from exhausting the machines if deletion fails - volumeName = VolumeNameGenerator.CreateVolumeName(mongodb1, nameof(WithDataShouldPersistStateBetweenUsages)); + volumeName = VolumeNameGenerator.Generate(mongodb1, nameof(WithDataShouldPersistStateBetweenUsages)); // if the volume already exists (because of a crashing previous run), delete it DockerUtils.AttemptDeleteDockerVolume(volumeName, throwOnFailure: true); diff --git a/tests/Aspire.Hosting.MySql.Tests/MySqlFunctionalTests.cs b/tests/Aspire.Hosting.MySql.Tests/MySqlFunctionalTests.cs index ff05bc5740..778b3cb1be 100644 --- a/tests/Aspire.Hosting.MySql.Tests/MySqlFunctionalTests.cs +++ b/tests/Aspire.Hosting.MySql.Tests/MySqlFunctionalTests.cs @@ -139,7 +139,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) if (useVolume) { // Use a deterministic volume name to prevent them from exhausting the machines if deletion fails - volumeName = VolumeNameGenerator.CreateVolumeName(mysql1, nameof(WithDataShouldPersistStateBetweenUsages)); + volumeName = VolumeNameGenerator.Generate(mysql1, nameof(WithDataShouldPersistStateBetweenUsages)); // if the volume already exists (because of a crashing previous run), delete it DockerUtils.AttemptDeleteDockerVolume(volumeName, throwOnFailure: true); diff --git a/tests/Aspire.Hosting.Nats.Tests/Aspire.Hosting.Nats.Tests.csproj b/tests/Aspire.Hosting.Nats.Tests/Aspire.Hosting.Nats.Tests.csproj index fa137269e4..497bfc379e 100644 --- a/tests/Aspire.Hosting.Nats.Tests/Aspire.Hosting.Nats.Tests.csproj +++ b/tests/Aspire.Hosting.Nats.Tests/Aspire.Hosting.Nats.Tests.csproj @@ -13,7 +13,6 @@ - diff --git a/tests/Aspire.Hosting.Nats.Tests/NatsFunctionalTests.cs b/tests/Aspire.Hosting.Nats.Tests/NatsFunctionalTests.cs index 723d29ab70..2be2da97be 100644 --- a/tests/Aspire.Hosting.Nats.Tests/NatsFunctionalTests.cs +++ b/tests/Aspire.Hosting.Nats.Tests/NatsFunctionalTests.cs @@ -75,7 +75,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) if (useVolume) { // Use a deterministic volume name to prevent them from exhausting the machines if deletion fails - volumeName = VolumeNameGenerator.CreateVolumeName(nats1, nameof(WithDataShouldPersistStateBetweenUsages)); + volumeName = VolumeNameGenerator.Generate(nats1, nameof(WithDataShouldPersistStateBetweenUsages)); // if the volume already exists (because of a crashing previous run), delete it DockerUtils.AttemptDeleteDockerVolume(volumeName, throwOnFailure: true); diff --git a/tests/Aspire.Hosting.Oracle.Tests/Aspire.Hosting.Oracle.Tests.csproj b/tests/Aspire.Hosting.Oracle.Tests/Aspire.Hosting.Oracle.Tests.csproj index 0a2b0195f8..0dc6946294 100644 --- a/tests/Aspire.Hosting.Oracle.Tests/Aspire.Hosting.Oracle.Tests.csproj +++ b/tests/Aspire.Hosting.Oracle.Tests/Aspire.Hosting.Oracle.Tests.csproj @@ -13,7 +13,6 @@ - diff --git a/tests/Aspire.Hosting.Oracle.Tests/OracleFunctionalTests.cs b/tests/Aspire.Hosting.Oracle.Tests/OracleFunctionalTests.cs index 579f5df30b..d59a1a5eb2 100644 --- a/tests/Aspire.Hosting.Oracle.Tests/OracleFunctionalTests.cs +++ b/tests/Aspire.Hosting.Oracle.Tests/OracleFunctionalTests.cs @@ -103,7 +103,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) if (useVolume) { // Use a deterministic volume name to prevent them from exhausting the machines if deletion fails - volumeName = VolumeNameGenerator.CreateVolumeName(oracle1, nameof(WithDataShouldPersistStateBetweenUsages)); + volumeName = VolumeNameGenerator.Generate(oracle1, nameof(WithDataShouldPersistStateBetweenUsages)); // If the volume already exists (because of a crashing previous run), try to delete it DockerUtils.AttemptDeleteDockerVolume(volumeName, throwOnFailure: true); diff --git a/tests/Aspire.Hosting.PostgreSQL.Tests/PostgresFunctionalTests.cs b/tests/Aspire.Hosting.PostgreSQL.Tests/PostgresFunctionalTests.cs index 31554f2465..983c0eeb90 100644 --- a/tests/Aspire.Hosting.PostgreSQL.Tests/PostgresFunctionalTests.cs +++ b/tests/Aspire.Hosting.PostgreSQL.Tests/PostgresFunctionalTests.cs @@ -214,7 +214,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) if (useVolume) { // Use a deterministic volume name to prevent them from exhausting the machines if deletion fails - volumeName = VolumeNameGenerator.CreateVolumeName(postgres1, nameof(WithDataShouldPersistStateBetweenUsages)); + volumeName = VolumeNameGenerator.Generate(postgres1, nameof(WithDataShouldPersistStateBetweenUsages)); // if the volume already exists (because of a crashing previous run), delete it DockerUtils.AttemptDeleteDockerVolume(volumeName, throwOnFailure: true); diff --git a/tests/Aspire.Hosting.Qdrant.Tests/Aspire.Hosting.Qdrant.Tests.csproj b/tests/Aspire.Hosting.Qdrant.Tests/Aspire.Hosting.Qdrant.Tests.csproj index 80c29342bb..aa5a474f2d 100644 --- a/tests/Aspire.Hosting.Qdrant.Tests/Aspire.Hosting.Qdrant.Tests.csproj +++ b/tests/Aspire.Hosting.Qdrant.Tests/Aspire.Hosting.Qdrant.Tests.csproj @@ -13,7 +13,6 @@ - diff --git a/tests/Aspire.Hosting.Qdrant.Tests/QdrantFunctionalTests.cs b/tests/Aspire.Hosting.Qdrant.Tests/QdrantFunctionalTests.cs index 4d2f3df823..3573281e04 100644 --- a/tests/Aspire.Hosting.Qdrant.Tests/QdrantFunctionalTests.cs +++ b/tests/Aspire.Hosting.Qdrant.Tests/QdrantFunctionalTests.cs @@ -106,7 +106,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) if (useVolume) { // Use a deterministic volume name to prevent them from exhausting the machines if deletion fails - volumeName = VolumeNameGenerator.CreateVolumeName(qdrant1, nameof(WithDataShouldPersistStateBetweenUsages)); + volumeName = VolumeNameGenerator.Generate(qdrant1, nameof(WithDataShouldPersistStateBetweenUsages)); // if the volume already exists (because of a crashing previous run), delete it DockerUtils.AttemptDeleteDockerVolume(volumeName, throwOnFailure: true); diff --git a/tests/Aspire.Hosting.RabbitMQ.Tests/Aspire.Hosting.RabbitMQ.Tests.csproj b/tests/Aspire.Hosting.RabbitMQ.Tests/Aspire.Hosting.RabbitMQ.Tests.csproj index 6e9e545e05..a8c80929d4 100644 --- a/tests/Aspire.Hosting.RabbitMQ.Tests/Aspire.Hosting.RabbitMQ.Tests.csproj +++ b/tests/Aspire.Hosting.RabbitMQ.Tests/Aspire.Hosting.RabbitMQ.Tests.csproj @@ -13,7 +13,6 @@ - diff --git a/tests/Aspire.Hosting.RabbitMQ.Tests/RabbitMQFunctionalTests.cs b/tests/Aspire.Hosting.RabbitMQ.Tests/RabbitMQFunctionalTests.cs index 7a2b9a4ac5..375a15041b 100644 --- a/tests/Aspire.Hosting.RabbitMQ.Tests/RabbitMQFunctionalTests.cs +++ b/tests/Aspire.Hosting.RabbitMQ.Tests/RabbitMQFunctionalTests.cs @@ -113,7 +113,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) if (useVolume) { // Use a deterministic volume name to prevent them from exhausting the machines if deletion fails - volumeName = VolumeNameGenerator.CreateVolumeName(rabbitMQ1, nameof(WithDataShouldPersistStateBetweenUsages)); + volumeName = VolumeNameGenerator.Generate(rabbitMQ1, nameof(WithDataShouldPersistStateBetweenUsages)); // if the volume already exists (because of a crashing previous run), delete it DockerUtils.AttemptDeleteDockerVolume(volumeName, throwOnFailure: true); diff --git a/tests/Aspire.Hosting.Redis.Tests/RedisFunctionalTests.cs b/tests/Aspire.Hosting.Redis.Tests/RedisFunctionalTests.cs index 0b01d42403..e04687f238 100644 --- a/tests/Aspire.Hosting.Redis.Tests/RedisFunctionalTests.cs +++ b/tests/Aspire.Hosting.Redis.Tests/RedisFunctionalTests.cs @@ -298,7 +298,7 @@ public async Task WithDataVolumeShouldPersistStateBetweenUsages() var redis1 = builder1.AddRedis("redis"); // Use a deterministic volume name to prevent them from exhausting the machines if deletion fails - var volumeName = VolumeNameGenerator.CreateVolumeName(redis1, nameof(WithDataVolumeShouldPersistStateBetweenUsages)); + var volumeName = VolumeNameGenerator.Generate(redis1, nameof(WithDataVolumeShouldPersistStateBetweenUsages)); redis1.WithDataVolume(volumeName); // if the volume already exists (because of a crashing previous run), delete it DockerUtils.AttemptDeleteDockerVolume(volumeName, throwOnFailure: true); diff --git a/tests/Aspire.Hosting.SqlServer.Tests/Aspire.Hosting.SqlServer.Tests.csproj b/tests/Aspire.Hosting.SqlServer.Tests/Aspire.Hosting.SqlServer.Tests.csproj index dab38c0c0c..0bfa65c52a 100644 --- a/tests/Aspire.Hosting.SqlServer.Tests/Aspire.Hosting.SqlServer.Tests.csproj +++ b/tests/Aspire.Hosting.SqlServer.Tests/Aspire.Hosting.SqlServer.Tests.csproj @@ -13,7 +13,6 @@ - diff --git a/tests/Aspire.Hosting.SqlServer.Tests/SqlServerFunctionalTests.cs b/tests/Aspire.Hosting.SqlServer.Tests/SqlServerFunctionalTests.cs index 5726d9d749..b4b87ab1f2 100644 --- a/tests/Aspire.Hosting.SqlServer.Tests/SqlServerFunctionalTests.cs +++ b/tests/Aspire.Hosting.SqlServer.Tests/SqlServerFunctionalTests.cs @@ -142,7 +142,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) if (useVolume) { // Use a deterministic volume name to prevent them from exhausting the machines if deletion fails - volumeName = VolumeNameGenerator.CreateVolumeName(sqlserver1, nameof(WithDataShouldPersistStateBetweenUsages)); + volumeName = VolumeNameGenerator.Generate(sqlserver1, nameof(WithDataShouldPersistStateBetweenUsages)); // if the volume already exists (because of a crashing previous run), delete it DockerUtils.AttemptDeleteDockerVolume(volumeName, throwOnFailure: true); diff --git a/tests/Aspire.Hosting.Tests/Utils/VolumeNameGeneratorTests.cs b/tests/Aspire.Hosting.Tests/Utils/VolumeNameGeneratorTests.cs index 2d6873dd24..f78d579571 100644 --- a/tests/Aspire.Hosting.Tests/Utils/VolumeNameGeneratorTests.cs +++ b/tests/Aspire.Hosting.Tests/Utils/VolumeNameGeneratorTests.cs @@ -18,7 +18,7 @@ public void VolumeGeneratorUsesUniqueName() var resource = builder.AddResource(new TestResource("myresource")); - var volumeName = CreateVolumeName(resource, "data"); + var volumeName = Generate(resource, "data"); Assert.Equal($"{volumePrefix}-{resource.Resource.Name}-data", volumeName); } @@ -30,7 +30,7 @@ public void ThrowsWhenSuffixContainsInvalidChars(string suffix) var builder = DistributedApplication.CreateBuilder(); var resource = builder.AddResource(new TestResource("myresource")); - Assert.Throws(nameof(suffix), () => CreateVolumeName(resource, suffix)); + Assert.Throws(nameof(suffix), () => Generate(resource, suffix)); } public static object[][] InvalidNameParts => [ @@ -61,8 +61,8 @@ public void VolumeNameDiffersBetweenPublishAndRun() var runResource = runBuilder.AddResource(new TestResource("myresource")); var publishResource = publishBuilder.AddResource(new TestResource("myresource")); - var runVolumeName = CreateVolumeName(runResource, "data"); - var publishVolumeName = CreateVolumeName(publishResource, "data"); + var runVolumeName = Generate(runResource, "data"); + var publishVolumeName = Generate(publishResource, "data"); Assert.Equal($"{runVolumePrefix}-{runResource.Resource.Name}-data", runVolumeName); Assert.Equal($"{publishVolumePrefix}-{publishResource.Resource.Name}-data", publishVolumeName); diff --git a/tests/Aspire.Hosting.Valkey.Tests/Aspire.Hosting.Valkey.Tests.csproj b/tests/Aspire.Hosting.Valkey.Tests/Aspire.Hosting.Valkey.Tests.csproj index cbaf03c0b6..10c457e1b0 100644 --- a/tests/Aspire.Hosting.Valkey.Tests/Aspire.Hosting.Valkey.Tests.csproj +++ b/tests/Aspire.Hosting.Valkey.Tests/Aspire.Hosting.Valkey.Tests.csproj @@ -12,7 +12,6 @@ - diff --git a/tests/Aspire.Hosting.Valkey.Tests/ValkeyFunctionalTests.cs b/tests/Aspire.Hosting.Valkey.Tests/ValkeyFunctionalTests.cs index 2ac91e1c12..70f6848a96 100644 --- a/tests/Aspire.Hosting.Valkey.Tests/ValkeyFunctionalTests.cs +++ b/tests/Aspire.Hosting.Valkey.Tests/ValkeyFunctionalTests.cs @@ -72,7 +72,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) if (useVolume) { // Use a deterministic volume name to prevent them from exhausting the machines if deletion fails - volumeName = VolumeNameGenerator.CreateVolumeName(valkey1, nameof(WithDataShouldPersistStateBetweenUsages)); + volumeName = VolumeNameGenerator.Generate(valkey1, nameof(WithDataShouldPersistStateBetweenUsages)); // if the volume already exists (because of a crashing previous run), delete it DockerUtils.AttemptDeleteDockerVolume(volumeName, throwOnFailure: true); From a98c3635419bfa31d353b1dfd7e06a54d8feaad0 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 30 Oct 2024 13:48:13 +0800 Subject: [PATCH 12/44] Add DefaultTimeout to dashboard tests (#6540) --- Aspire.sln | 7 ++- .../BrowserSecurityHeadersMiddlewareTests.cs | 9 ++-- .../ChannelExtensionsTests.cs | 19 +++---- .../Integration/DashboardClientAuthTests.cs | 13 ++--- .../FrontendBrowserTokenAuthTests.cs | 29 ++++++----- .../FrontendOpenIdConnectAuthTests.cs | 17 +++--- .../Integration/OtlpCorsHttpServiceTests.cs | 19 +++---- .../Integration/OtlpGrpcServiceTests.cs | 52 +++++++++---------- .../Integration/OtlpHttpServiceTests.cs | 52 +++++++++---------- .../Integration/Playwright/AppBarTests.cs | 14 ++--- .../BrowserTokenAuthenticationTests.cs | 28 +++++----- .../Integration/ResponseCompressionTests.cs | 9 ++-- .../Integration/StartupTests.cs | 52 +++++++++---------- .../LocalBrowserStorageTests.cs | 9 ++-- .../ValidateTokenMiddlewareTests.cs | 25 ++++----- .../Model/DashboardClientTests.cs | 23 ++++---- .../OtlpApiKeyAuthenticationHandlerTests.cs | 17 +++--- .../ResourceOutgoingPeerResolverTests.cs | 7 +-- .../TelemetryRepositoryTests/LogTests.cs | 13 ++--- 19 files changed, 217 insertions(+), 197 deletions(-) diff --git a/Aspire.sln b/Aspire.sln index 7972b48f7b..e3d7b7a579 100644 --- a/Aspire.sln +++ b/Aspire.sln @@ -643,6 +643,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HealthChecksSandbox.AppHost EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DaprServiceC", "playground\dapr\ServiceC\DaprServiceC.csproj", "{B26653B9-439E-4850-A7F8-43C6E5121952}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dashboard", "Dashboard", "{830F7CA9-8E51-4D62-832F-91F53F85B7AE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1738,7 +1740,7 @@ Global {31F5E4F3-AC4E-4538-BC7D-85BCF9CA686A} = {975F6F41-B455-451D-A312-098DE4A167B6} {A37AAFDB-545B-4599-806A-EFCB8B310446} = {975F6F41-B455-451D-A312-098DE4A167B6} {8CB12764-E469-4BB5-8554-5F9CA0F6DE18} = {975F6F41-B455-451D-A312-098DE4A167B6} - {1ABCD945-5CAA-4F30-A741-7A9DA919254A} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60} + {1ABCD945-5CAA-4F30-A741-7A9DA919254A} = {830F7CA9-8E51-4D62-832F-91F53F85B7AE} {E20280B8-8BE0-4967-AFC2-65FFCD6EC5E4} = {F534D4F8-5E3A-42FC-BCD7-4C2D6060F9C8} {6EAA089D-7ADD-4C74-B040-FD3D75DB5C75} = {C424395C-1235-41A4-BF55-07880A04368C} {9D9C360B-9DF1-4076-8416-66964427C8F3} = {C424395C-1235-41A4-BF55-07880A04368C} @@ -1820,7 +1822,7 @@ Global {157A434E-E3CA-4080-96CF-903CC3DF66E9} = {8AA07A14-A4A7-45EC-B0F6-4690B516B16D} {921CB408-5E37-4354-B4CF-EAE517F633DC} = {8AA07A14-A4A7-45EC-B0F6-4690B516B16D} {C774BE00-EE93-4148-B866-8F0F2BA1E473} = {C424395C-1235-41A4-BF55-07880A04368C} - {0870A667-FB0C-4758-AEAF-9E5F092AD7C1} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60} + {0870A667-FB0C-4758-AEAF-9E5F092AD7C1} = {830F7CA9-8E51-4D62-832F-91F53F85B7AE} {C4833DEC-0A4F-4504-B8D0-06C60B84119C} = {91F22EEA-EB23-425A-9B32-9438A0809F4B} {9CA94707-E801-444F-A582-D5BD0104CF9B} = {91F22EEA-EB23-425A-9B32-9438A0809F4B} {3216CF59-84B0-46FF-8572-D0AFB0155423} = {A7C6452C-FEDB-4883-9AE7-29892D260AA3} @@ -1998,6 +2000,7 @@ Global {B7345F72-712F-436C-AE18-CAF7CDD4A990} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0} {042DD8C6-A26C-4B06-80A1-FE7F8659C5BC} = {B7345F72-712F-436C-AE18-CAF7CDD4A990} {B26653B9-439E-4850-A7F8-43C6E5121952} = {57A42144-739E-49A7-BADB-BB8F5F20FA17} + {830F7CA9-8E51-4D62-832F-91F53F85B7AE} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6DCEDFEC-988E-4CB3-B45B-191EB5086E0C} diff --git a/tests/Aspire.Dashboard.Tests/BrowserSecurityHeadersMiddlewareTests.cs b/tests/Aspire.Dashboard.Tests/BrowserSecurityHeadersMiddlewareTests.cs index 366b014e82..99166c1a66 100644 --- a/tests/Aspire.Dashboard.Tests/BrowserSecurityHeadersMiddlewareTests.cs +++ b/tests/Aspire.Dashboard.Tests/BrowserSecurityHeadersMiddlewareTests.cs @@ -4,6 +4,7 @@ using Aspire.Dashboard.Authentication.Connection; using Aspire.Dashboard.Model; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Primitives; @@ -21,7 +22,7 @@ public async Task InvokeAsync_Development_AllowExternalFetch() var httpContext = new DefaultHttpContext(); // Act - await middleware.InvokeAsync(httpContext); + await middleware.InvokeAsync(httpContext).DefaultTimeout(); // Assert Assert.NotEqual(StringValues.Empty, httpContext.Response.Headers.ContentSecurityPolicy); @@ -36,7 +37,7 @@ public async Task InvokeAsync_Production_DenyExternalFetch() var httpContext = new DefaultHttpContext(); // Act - await middleware.InvokeAsync(httpContext); + await middleware.InvokeAsync(httpContext).DefaultTimeout(); // Assert Assert.NotEqual(StringValues.Empty, httpContext.Response.Headers.ContentSecurityPolicy); @@ -54,7 +55,7 @@ public async Task InvokeAsync_Scheme_ImageSourceChangesOnScheme(string scheme, s httpContext.Request.Scheme = scheme; // Act - await middleware.InvokeAsync(httpContext); + await middleware.InvokeAsync(httpContext).DefaultTimeout(); // Assert Assert.NotEqual(StringValues.Empty, httpContext.Response.Headers.ContentSecurityPolicy); @@ -70,7 +71,7 @@ public async Task InvokeAsync_Otlp_NotAdded() httpContext.Features.Set(new TestConnectionTypeFeature { ConnectionTypes = [ConnectionType.Otlp] }); // Act - await middleware.InvokeAsync(httpContext); + await middleware.InvokeAsync(httpContext).DefaultTimeout(); // Assert Assert.Equal(StringValues.Empty, httpContext.Response.Headers.ContentSecurityPolicy); diff --git a/tests/Aspire.Dashboard.Tests/ChannelExtensionsTests.cs b/tests/Aspire.Dashboard.Tests/ChannelExtensionsTests.cs index 8f73c37fc4..30f5353222 100644 --- a/tests/Aspire.Dashboard.Tests/ChannelExtensionsTests.cs +++ b/tests/Aspire.Dashboard.Tests/ChannelExtensionsTests.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Threading.Channels; using Aspire.Dashboard.Utils; +using Microsoft.AspNetCore.InternalTesting; using Xunit; namespace Aspire.Dashboard.Tests; @@ -31,7 +32,7 @@ public async Task GetBatchesAsync_CancellationToken_Exits() }); // Assert - await TaskHelpers.WaitIgnoreCancelAsync(readTask); + await TaskHelpers.WaitIgnoreCancelAsync(readTask).DefaultTimeout(); } [Fact] @@ -55,7 +56,7 @@ public async Task GetBatchesAsync_WithCancellation_Exits() }); // Assert - await TaskHelpers.WaitIgnoreCancelAsync(readTask); + await TaskHelpers.WaitIgnoreCancelAsync(readTask).DefaultTimeout(); } [Fact] @@ -87,19 +88,19 @@ public async Task GetBatchesAsync_MinReadInterval_WaitForNextRead() // Assert var stopwatch = Stopwatch.StartNew(); - var read1 = await resultChannel.Reader.ReadAsync(); + var read1 = await resultChannel.Reader.ReadAsync().DefaultTimeout(); Assert.Equal(["a", "b", "c"], read1.Single()); channel.Writer.TryWrite(["d", "e", "f"]); - var read2 = await resultChannel.Reader.ReadAsync(); + var read2 = await resultChannel.Reader.ReadAsync().DefaultTimeout(); Assert.Equal(["d", "e", "f"], read2.Single()); var elapsed = stopwatch.Elapsed; CustomAssert.AssertExceedsMinInterval(elapsed, minReadInterval); channel.Writer.Complete(); - await TaskHelpers.WaitIgnoreCancelAsync(readTask); + await TaskHelpers.WaitIgnoreCancelAsync(readTask).DefaultTimeout(); } [Fact] @@ -131,18 +132,18 @@ public async Task GetBatchesAsync_MinReadInterval_WithCancellation_Exit() // Assert var stopwatch = Stopwatch.StartNew(); - var read1 = await resultChannel.Reader.ReadAsync(); + var read1 = await resultChannel.Reader.ReadAsync().DefaultTimeout(); Assert.Equal(["a", "b", "c"], read1.Single()); channel.Writer.TryWrite(["d", "e", "f"]); - var read2Task = resultChannel.Reader.ReadAsync(); + var read2Task = resultChannel.Reader.ReadAsync().DefaultTimeout(); cts.Cancel(); - await TaskHelpers.WaitIgnoreCancelAsync(readTask); + await TaskHelpers.WaitIgnoreCancelAsync(readTask).DefaultTimeout(); try { - await read2Task; + await read2Task.DefaultTimeout(); } catch (ChannelClosedException) { diff --git a/tests/Aspire.Dashboard.Tests/Integration/DashboardClientAuthTests.cs b/tests/Aspire.Dashboard.Tests/Integration/DashboardClientAuthTests.cs index 73a841cd15..18737f1470 100644 --- a/tests/Aspire.Dashboard.Tests/Integration/DashboardClientAuthTests.cs +++ b/tests/Aspire.Dashboard.Tests/Integration/DashboardClientAuthTests.cs @@ -11,6 +11,7 @@ using Grpc.Core; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -40,10 +41,10 @@ public DashboardClientAuthTests(ITestOutputHelper testOutputHelper) public async Task ConnectsToResourceService_Unsecured(bool useHttps) { var loggerFactory = IntegrationTestHelpers.CreateLoggerFactory(_testOutputHelper); - await using var server = await CreateResourceServiceServerAsync(loggerFactory, useHttps); - await using var client = await CreateDashboardClientAsync(loggerFactory, server.Url, authMode: ResourceClientAuthMode.Unsecured); + await using var server = await CreateResourceServiceServerAsync(loggerFactory, useHttps).DefaultTimeout(); + await using var client = await CreateDashboardClientAsync(loggerFactory, server.Url, authMode: ResourceClientAuthMode.Unsecured).DefaultTimeout(); - var call = await server.Calls.ApplicationInformationCallsChannel.Reader.ReadAsync(); + var call = await server.Calls.ApplicationInformationCallsChannel.Reader.ReadAsync().DefaultTimeout(); Assert.NotNull(call.Request); Assert.NotNull(call.RequestHeaders); @@ -56,10 +57,10 @@ public async Task ConnectsToResourceService_Unsecured(bool useHttps) public async Task ConnectsToResourceService_ApiKey(bool useHttps) { var loggerFactory = IntegrationTestHelpers.CreateLoggerFactory(_testOutputHelper); - await using var server = await CreateResourceServiceServerAsync(loggerFactory, useHttps); - await using var client = await CreateDashboardClientAsync(loggerFactory, server.Url, authMode: ResourceClientAuthMode.ApiKey, configureOptions: options => options.ResourceServiceClient.ApiKey = "TestApiKey!"); + await using var server = await CreateResourceServiceServerAsync(loggerFactory, useHttps).DefaultTimeout(); + await using var client = await CreateDashboardClientAsync(loggerFactory, server.Url, authMode: ResourceClientAuthMode.ApiKey, configureOptions: options => options.ResourceServiceClient.ApiKey = "TestApiKey!").DefaultTimeout(); - var call = await server.Calls.ApplicationInformationCallsChannel.Reader.ReadAsync(); + var call = await server.Calls.ApplicationInformationCallsChannel.Reader.ReadAsync().DefaultTimeout(); Assert.NotNull(call.Request); Assert.NotNull(call.RequestHeaders); diff --git a/tests/Aspire.Dashboard.Tests/Integration/FrontendBrowserTokenAuthTests.cs b/tests/Aspire.Dashboard.Tests/Integration/FrontendBrowserTokenAuthTests.cs index b3dba26b6c..a296f82fd2 100644 --- a/tests/Aspire.Dashboard.Tests/Integration/FrontendBrowserTokenAuthTests.cs +++ b/tests/Aspire.Dashboard.Tests/Integration/FrontendBrowserTokenAuthTests.cs @@ -8,6 +8,7 @@ using Aspire.Dashboard.Configuration; using Aspire.Dashboard.Utils; using Aspire.Hosting; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; using Xunit; @@ -34,12 +35,12 @@ public async Task Get_Unauthenticated_RedirectToLogin() config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.BrowserToken.ToString(); config[DashboardConfigNames.DashboardFrontendBrowserTokenName.ConfigKey] = apiKey; }); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); using var client = new HttpClient { BaseAddress = new Uri($"http://{app.FrontendSingleEndPointAccessor().EndPoint}") }; // Act - var response = await client.GetAsync("/"); + var response = await client.GetAsync("/").DefaultTimeout(); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -56,19 +57,19 @@ public async Task Get_LoginPage_ValidToken_RedirectToApp() config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.BrowserToken.ToString(); config[DashboardConfigNames.DashboardFrontendBrowserTokenName.ConfigKey] = apiKey; }); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); using var client = new HttpClient { BaseAddress = new Uri($"http://{app.FrontendSingleEndPointAccessor().EndPoint}") }; // Act 1 - var response1 = await client.GetAsync(DashboardUrls.LoginUrl(returnUrl: DashboardUrls.TracesUrl(), token: apiKey)); + var response1 = await client.GetAsync(DashboardUrls.LoginUrl(returnUrl: DashboardUrls.TracesUrl(), token: apiKey)).DefaultTimeout(); // Assert 1 Assert.Equal(HttpStatusCode.OK, response1.StatusCode); Assert.Equal(DashboardUrls.TracesUrl(), response1.RequestMessage!.RequestUri!.PathAndQuery); // Act 2 - var response2 = await client.GetAsync(DashboardUrls.StructuredLogsUrl()); + var response2 = await client.GetAsync(DashboardUrls.StructuredLogsUrl()).DefaultTimeout(); // Assert 2 Assert.Equal(HttpStatusCode.OK, response2.StatusCode); @@ -87,12 +88,12 @@ public async Task Get_LoginPage_ValidToken_OtlpHttpConnection_Denied() config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.BrowserToken.ToString(); config[DashboardConfigNames.DashboardFrontendBrowserTokenName.ConfigKey] = apiKey; }, testSink: testSink); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); using var client = new HttpClient { BaseAddress = new Uri($"http://{app.OtlpServiceHttpEndPointAccessor().EndPoint}") }; // Act - var response = await client.GetAsync(DashboardUrls.LoginUrl(returnUrl: DashboardUrls.TracesUrl(), token: apiKey)); + var response = await client.GetAsync(DashboardUrls.LoginUrl(returnUrl: DashboardUrls.TracesUrl(), token: apiKey)).DefaultTimeout(); // Assert Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); @@ -111,12 +112,12 @@ public async Task Get_LoginPage_InvalidToken_RedirectToLoginWithoutToken() config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.BrowserToken.ToString(); config[DashboardConfigNames.DashboardFrontendBrowserTokenName.ConfigKey] = apiKey; }); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); using var client = new HttpClient { BaseAddress = new Uri($"http://{app.FrontendSingleEndPointAccessor().EndPoint}") }; // Act - var response = await client.GetAsync(DashboardUrls.LoginUrl(returnUrl: DashboardUrls.TracesUrl(), token: "Wrong!")); + var response = await client.GetAsync(DashboardUrls.LoginUrl(returnUrl: DashboardUrls.TracesUrl(), token: "Wrong!")).DefaultTimeout(); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -136,12 +137,12 @@ public async Task Post_ValidateTokenApi_AvailableBasedOnOptions(FrontendAuthMode config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = authMode.ToString(); config[DashboardConfigNames.DashboardFrontendBrowserTokenName.ConfigKey] = apiKey; }); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); using var client = new HttpClient { BaseAddress = new Uri($"http://{app.FrontendSingleEndPointAccessor().EndPoint}") }; // Act - var response = await client.PostAsync("/api/validatetoken?token=" + requestToken, content: null); + var response = await client.PostAsync("/api/validatetoken?token=" + requestToken, content: null).DefaultTimeout(); // Assert Assert.Equal(statusCode, response.StatusCode); @@ -164,7 +165,7 @@ public async Task LogOutput_NoToken_GeneratedTokenLogged() }, testSink: testSink); // Act - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); // Assert var l = testSink.Writes.Where(w => w.LoggerName == typeof(DashboardWebApplication).FullName).ToList(); @@ -225,7 +226,7 @@ public async Task LogOutput_AnyIP_LoginLinkLocalhost(string frontendUrl, string }, testSink: testSink); // Act - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); // Assert var l = testSink.Writes.Where(w => w.LoggerName == typeof(DashboardWebApplication).FullName).ToList(); @@ -252,7 +253,7 @@ public async Task LogOutput_InContainer_LoginLinkContainerMessage() }, testSink: testSink); // Act - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); // Assert var l = testSink.Writes.Where(w => w.LoggerName == typeof(DashboardWebApplication).FullName).ToList(); diff --git a/tests/Aspire.Dashboard.Tests/Integration/FrontendOpenIdConnectAuthTests.cs b/tests/Aspire.Dashboard.Tests/Integration/FrontendOpenIdConnectAuthTests.cs index bd2d429e34..ea69717bad 100644 --- a/tests/Aspire.Dashboard.Tests/Integration/FrontendOpenIdConnectAuthTests.cs +++ b/tests/Aspire.Dashboard.Tests/Integration/FrontendOpenIdConnectAuthTests.cs @@ -5,6 +5,7 @@ using System.Web; using Aspire.Dashboard.Authentication; using Aspire.Hosting; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.Logging.Testing; using Xunit; using Xunit.Abstractions; @@ -16,7 +17,7 @@ public class FrontendOpenIdConnectAuthTests(ITestOutputHelper testOutputHelper) [Fact] public async Task Get_Unauthenticated_RedirectsToAuthority() { - await using var authority = await MockOpenIdAuthority.CreateAsync(); + await using var authority = await MockOpenIdAuthority.CreateAsync().DefaultTimeout(); await using var app = IntegrationTestHelpers.CreateDashboardWebApplication( testOutputHelper, @@ -25,7 +26,7 @@ public async Task Get_Unauthenticated_RedirectsToAuthority() ConfigureOpenIdConnect(config, authority); }); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); var handler = new HttpClientHandler() { @@ -36,7 +37,7 @@ public async Task Get_Unauthenticated_RedirectsToAuthority() using var client = new HttpClient(handler) { BaseAddress = new Uri($"http://{app.FrontendSingleEndPointAccessor().EndPoint}") }; // Act - var response = await client.GetAsync("/"); + var response = await client.GetAsync("/").DefaultTimeout(); // Assert Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); @@ -53,13 +54,13 @@ public async Task Get_Unauthenticated_RedirectsToAuthority() Assert.Equal("code", query.Get("response_type")); Assert.Equal("openid profile", query.Get("scope")); - await app.StopAsync(); + await app.StopAsync().DefaultTimeout(); } [Fact] public async Task Get_Unauthenticated_OtlpHttpConnection_Denied() { - await using var authority = await MockOpenIdAuthority.CreateAsync(); + await using var authority = await MockOpenIdAuthority.CreateAsync().DefaultTimeout(); var testSink = new TestSink(); await using var app = IntegrationTestHelpers.CreateDashboardWebApplication( @@ -70,7 +71,7 @@ public async Task Get_Unauthenticated_OtlpHttpConnection_Denied() }, testSink: testSink); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); var handler = new HttpClientHandler() { @@ -81,7 +82,7 @@ public async Task Get_Unauthenticated_OtlpHttpConnection_Denied() using var client = new HttpClient(handler) { BaseAddress = new Uri($"http://{app.OtlpServiceHttpEndPointAccessor().EndPoint}") }; // Act - var response = await client.GetAsync("/"); + var response = await client.GetAsync("/").DefaultTimeout(); // Assert Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); @@ -89,7 +90,7 @@ public async Task Get_Unauthenticated_OtlpHttpConnection_Denied() var log = testSink.Writes.Single(s => s.LoggerName == typeof(FrontendCompositeAuthenticationHandler).FullName && s.EventId.Name == "AuthenticationSchemeNotAuthenticatedWithFailure"); Assert.Equal("FrontendComposite was not authenticated. Failure message: Connection type Frontend is not enabled on this connection.", log.Message); - await app.StopAsync(); + await app.StopAsync().DefaultTimeout(); } private static void ConfigureOpenIdConnect(Dictionary config, MockOpenIdAuthority.Authority authority) diff --git a/tests/Aspire.Dashboard.Tests/Integration/OtlpCorsHttpServiceTests.cs b/tests/Aspire.Dashboard.Tests/Integration/OtlpCorsHttpServiceTests.cs index 8f099eedf0..a83369eacc 100644 --- a/tests/Aspire.Dashboard.Tests/Integration/OtlpCorsHttpServiceTests.cs +++ b/tests/Aspire.Dashboard.Tests/Integration/OtlpCorsHttpServiceTests.cs @@ -3,6 +3,7 @@ using System.Net; using Aspire.Hosting; +using Microsoft.AspNetCore.InternalTesting; using Xunit; using Xunit.Abstractions; @@ -22,7 +23,7 @@ public async Task ReceivePreflight_OtlpHttpEndPoint_NoCorsConfiguration_NotFound { // Arrange await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.OtlpServiceHttpEndPointAccessor().EndPoint}"); @@ -32,7 +33,7 @@ public async Task ReceivePreflight_OtlpHttpEndPoint_NoCorsConfiguration_NotFound preflightRequest.Headers.TryAddWithoutValidation("Origin", "http://localhost:8000"); // Act - var responseMessage = await httpClient.SendAsync(preflightRequest); + var responseMessage = await httpClient.SendAsync(preflightRequest).DefaultTimeout(); // Assert Assert.Equal(HttpStatusCode.NotFound, responseMessage.StatusCode); @@ -46,7 +47,7 @@ public async Task ReceivePreflight_OtlpHttpEndPoint_ValidCorsOrigin_Success() { config[DashboardConfigNames.DashboardOtlpCorsAllowedOriginsKeyName.ConfigKey] = "http://localhost:8000, http://localhost:8001"; }); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.OtlpServiceHttpEndPointAccessor().EndPoint}"); @@ -56,7 +57,7 @@ public async Task ReceivePreflight_OtlpHttpEndPoint_ValidCorsOrigin_Success() preflightRequest1.Headers.TryAddWithoutValidation("Access-Control-Request-Headers", "x-requested-with,x-custom,Content-Type"); preflightRequest1.Headers.TryAddWithoutValidation("Origin", "http://localhost:8000"); - var responseMessage1 = await httpClient.SendAsync(preflightRequest1); + var responseMessage1 = await httpClient.SendAsync(preflightRequest1).DefaultTimeout(); // Assert 1 Assert.Equal(HttpStatusCode.NoContent, responseMessage1.StatusCode); @@ -70,7 +71,7 @@ public async Task ReceivePreflight_OtlpHttpEndPoint_ValidCorsOrigin_Success() preflightRequest2.Headers.TryAddWithoutValidation("Access-Control-Request-Headers", "x-requested-with,x-custom,Content-Type"); preflightRequest2.Headers.TryAddWithoutValidation("Origin", "http://localhost:8001"); - var responseMessage2 = await httpClient.SendAsync(preflightRequest2); + var responseMessage2 = await httpClient.SendAsync(preflightRequest2).DefaultTimeout(); // Assert 2 Assert.Equal(HttpStatusCode.NoContent, responseMessage2.StatusCode); @@ -87,7 +88,7 @@ public async Task ReceivePreflight_OtlpHttpEndPoint_InvalidCorsOrigin_NoCorsHead { config[DashboardConfigNames.DashboardOtlpCorsAllowedOriginsKeyName.ConfigKey] = "http://localhost:8000"; }); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.OtlpServiceHttpEndPointAccessor().EndPoint}"); @@ -97,7 +98,7 @@ public async Task ReceivePreflight_OtlpHttpEndPoint_InvalidCorsOrigin_NoCorsHead preflightRequest.Headers.TryAddWithoutValidation("Origin", "http://localhost:8001"); // Act - var responseMessage = await httpClient.SendAsync(preflightRequest); + var responseMessage = await httpClient.SendAsync(preflightRequest).DefaultTimeout(); // Assert Assert.Equal(HttpStatusCode.NoContent, responseMessage.StatusCode); @@ -115,7 +116,7 @@ public async Task ReceivePreflight_OtlpHttpEndPoint_AnyOrigin_Success() config[DashboardConfigNames.DashboardOtlpCorsAllowedOriginsKeyName.ConfigKey] = "*"; config[DashboardConfigNames.DashboardOtlpCorsAllowedHeadersKeyName.ConfigKey] = "*"; }); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.OtlpServiceHttpEndPointAccessor().EndPoint}"); @@ -125,7 +126,7 @@ public async Task ReceivePreflight_OtlpHttpEndPoint_AnyOrigin_Success() preflightRequest.Headers.TryAddWithoutValidation("Origin", "http://localhost:8000"); // Act - var responseMessage = await httpClient.SendAsync(preflightRequest); + var responseMessage = await httpClient.SendAsync(preflightRequest).DefaultTimeout(); // Assert Assert.Equal(HttpStatusCode.NoContent, responseMessage.StatusCode); diff --git a/tests/Aspire.Dashboard.Tests/Integration/OtlpGrpcServiceTests.cs b/tests/Aspire.Dashboard.Tests/Integration/OtlpGrpcServiceTests.cs index 1649dd3d24..ba68491475 100644 --- a/tests/Aspire.Dashboard.Tests/Integration/OtlpGrpcServiceTests.cs +++ b/tests/Aspire.Dashboard.Tests/Integration/OtlpGrpcServiceTests.cs @@ -30,15 +30,15 @@ public async Task CallService_OtlpGrpcEndPoint_Success() { // Arrange await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); using var channel = IntegrationTestHelpers.CreateGrpcChannel($"http://{app.OtlpServiceGrpcEndPointAccessor().EndPoint}", _testOutputHelper); var client = new LogsService.LogsServiceClient(channel); // Act var response = client.ExportAsync(new ExportLogsServiceRequest()); - var message = await response.ResponseAsync; - var headers = await response.ResponseHeadersAsync; + var message = await response.ResponseAsync.DefaultTimeout(); + var headers = await response.ResponseHeadersAsync.DefaultTimeout(); // Assert Assert.Null(headers.GetValue("content-security-policy")); @@ -55,13 +55,13 @@ public async Task CallService_OtlpGrpcEndPoint_RequiredApiKeyMissing_Failure() config[DashboardConfigNames.DashboardOtlpAuthModeName.ConfigKey] = OtlpAuthMode.ApiKey.ToString(); config[DashboardConfigNames.DashboardOtlpPrimaryApiKeyName.ConfigKey] = apiKey; }); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); using var channel = IntegrationTestHelpers.CreateGrpcChannel($"http://{app.OtlpServiceGrpcEndPointAccessor().EndPoint}", _testOutputHelper); var client = new LogsService.LogsServiceClient(channel); // Act - var ex = await Assert.ThrowsAsync(() => client.ExportAsync(new ExportLogsServiceRequest()).ResponseAsync); + var ex = await Assert.ThrowsAsync(() => client.ExportAsync(new ExportLogsServiceRequest()).ResponseAsync).DefaultTimeout(); // Assert Assert.Equal(StatusCode.Unauthenticated, ex.StatusCode); @@ -77,7 +77,7 @@ public async Task CallService_OtlpGrpcEndPoint_RequiredApiKeyWrong_Failure() config[DashboardConfigNames.DashboardOtlpAuthModeName.ConfigKey] = OtlpAuthMode.ApiKey.ToString(); config[DashboardConfigNames.DashboardOtlpPrimaryApiKeyName.ConfigKey] = apiKey; }); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); using var channel = IntegrationTestHelpers.CreateGrpcChannel($"http://{app.OtlpServiceGrpcEndPointAccessor().EndPoint}", _testOutputHelper); var client = new LogsService.LogsServiceClient(channel); @@ -88,7 +88,7 @@ public async Task CallService_OtlpGrpcEndPoint_RequiredApiKeyWrong_Failure() }; // Act - var ex = await Assert.ThrowsAsync(() => client.ExportAsync(new ExportLogsServiceRequest(), metadata).ResponseAsync); + var ex = await Assert.ThrowsAsync(() => client.ExportAsync(new ExportLogsServiceRequest(), metadata).ResponseAsync).DefaultTimeout(); // Assert Assert.Equal(StatusCode.Unauthenticated, ex.StatusCode); @@ -104,7 +104,7 @@ public async Task CallService_OtlpGrpcEndPoint_RequiredApiKeySent_Success() config[DashboardConfigNames.DashboardOtlpAuthModeName.ConfigKey] = OtlpAuthMode.ApiKey.ToString(); config[DashboardConfigNames.DashboardOtlpPrimaryApiKeyName.ConfigKey] = apiKey; }); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); using var channel = IntegrationTestHelpers.CreateGrpcChannel($"http://{app.OtlpServiceGrpcEndPointAccessor().EndPoint}", _testOutputHelper); var client = new LogsService.LogsServiceClient(channel); @@ -115,7 +115,7 @@ public async Task CallService_OtlpGrpcEndPoint_RequiredApiKeySent_Success() }; // Act - var response = await client.ExportAsync(new ExportLogsServiceRequest(), metadata); + var response = await client.ExportAsync(new ExportLogsServiceRequest(), metadata).ResponseAsync.DefaultTimeout(); // Assert Assert.Equal(0, response.PartialSuccess.RejectedLogRecords); @@ -133,7 +133,7 @@ public async Task CallService_OtlpGrpcEndPoint_RequiredSecondaryApiKeySent_Succe config[DashboardConfigNames.DashboardOtlpPrimaryApiKeyName.ConfigKey] = apiKey; config[DashboardConfigNames.DashboardOtlpSecondaryApiKeyName.ConfigKey] = secondaryApiKey; }); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); using var channel = IntegrationTestHelpers.CreateGrpcChannel($"http://{app.OtlpServiceGrpcEndPointAccessor().EndPoint}", _testOutputHelper); var client = new LogsService.LogsServiceClient(channel); @@ -144,7 +144,7 @@ public async Task CallService_OtlpGrpcEndPoint_RequiredSecondaryApiKeySent_Succe }; // Act - var response = await client.ExportAsync(new ExportLogsServiceRequest(), metadata); + var response = await client.ExportAsync(new ExportLogsServiceRequest(), metadata).ResponseAsync.DefaultTimeout(); // Assert Assert.Equal(0, response.PartialSuccess.RejectedLogRecords); @@ -172,13 +172,13 @@ public async Task CallService_OtlpGrpcEndPoint_ExternalFile_FileChanged_UseConfi } }; logger.LogInformation("Writing original JSON file."); - await File.WriteAllTextAsync(configPath, configJson.ToString()); + await File.WriteAllTextAsync(configPath, configJson.ToString()).DefaultTimeout(); await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(loggerFactory, config => { config[DashboardConfigNames.DashboardConfigFilePathName.ConfigKey] = configPath; }); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); using var channel = IntegrationTestHelpers.CreateGrpcChannel($"http://{app.OtlpServiceGrpcEndPointAccessor().EndPoint}", loggerFactory); var client = new LogsService.LogsServiceClient(channel); @@ -189,7 +189,7 @@ public async Task CallService_OtlpGrpcEndPoint_ExternalFile_FileChanged_UseConfi }; // Act 1 - var response1 = await client.ExportAsync(new ExportLogsServiceRequest(), metadata); + var response1 = await client.ExportAsync(new ExportLogsServiceRequest(), metadata).ResponseAsync.DefaultTimeout(); // Assert 1 Assert.Equal(0, response1.PartialSuccess.RejectedLogRecords); @@ -215,7 +215,7 @@ public async Task CallService_OtlpGrpcEndPoint_ExternalFile_FileChanged_UseConfi }; logger.LogInformation("Writing new JSON file."); - await File.WriteAllTextAsync(configPath, configJson.ToString()); + await File.WriteAllTextAsync(configPath, configJson.ToString()).DefaultTimeout(); logger.LogInformation("Waiting for options change."); var options = await tcs.Task; @@ -225,7 +225,7 @@ public async Task CallService_OtlpGrpcEndPoint_ExternalFile_FileChanged_UseConfi // Act 2 logger.LogInformation("Client sends new request with old API key."); - var ex = await Assert.ThrowsAsync(() => client.ExportAsync(new ExportLogsServiceRequest(), metadata).ResponseAsync); + var ex = await Assert.ThrowsAsync(() => client.ExportAsync(new ExportLogsServiceRequest(), metadata).ResponseAsync).DefaultTimeout(); // Assert 2 Assert.Equal(StatusCode.Unauthenticated, ex.StatusCode); @@ -242,7 +242,7 @@ public async Task CallService_BrowserEndPoint_Failure() // Change dashboard to HTTPS so the caller can negotiate a HTTP/2 connection. config[DashboardConfigNames.DashboardFrontendUrlName.ConfigKey] = "https://127.0.0.1:0"; }); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); using var channel = IntegrationTestHelpers.CreateGrpcChannel( $"https://{app.FrontendSingleEndPointAccessor().EndPoint}", @@ -254,7 +254,7 @@ public async Task CallService_BrowserEndPoint_Failure() var client = new LogsService.LogsServiceClient(channel); // Act - var ex = await Assert.ThrowsAsync(() => client.ExportAsync(new ExportLogsServiceRequest()).ResponseAsync); + var ex = await Assert.ThrowsAsync(() => client.ExportAsync(new ExportLogsServiceRequest()).ResponseAsync).DefaultTimeout(); // Assert Assert.Equal(StatusCode.Unauthenticated, ex.StatusCode); @@ -273,13 +273,13 @@ public async Task CallService_OtlpEndpoint_RequiredClientCertificateMissing_Fail config[DashboardConfigNames.DashboardOtlpAuthModeName.ConfigKey] = OtlpAuthMode.ClientCertificate.ToString(); }); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); using var channel = IntegrationTestHelpers.CreateGrpcChannel($"https://{app.OtlpServiceGrpcEndPointAccessor().EndPoint}", _testOutputHelper); var client = new LogsService.LogsServiceClient(channel); // Act - var ex = await Assert.ThrowsAsync(() => client.ExportAsync(new ExportLogsServiceRequest()).ResponseAsync); + var ex = await Assert.ThrowsAsync(() => client.ExportAsync(new ExportLogsServiceRequest()).ResponseAsync).DefaultTimeout(); // Assert // StatusCode can change depending upon order of execution inside HttpClient. @@ -307,7 +307,7 @@ public async Task CallService_OtlpEndpoint_RequiredClientCertificateValid_Succes config["Dashboard:Otlp:CertificateAuthOptions:AllowedCertificateTypes"] = "SelfSigned"; config["Dashboard:Otlp:CertificateAuthOptions:ValidateValidityPeriod"] = "false"; }); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); var clientCertificate = TestCertificateLoader.GetTestCertificate("eku.client.pfx"); using var channel = IntegrationTestHelpers.CreateGrpcChannel( @@ -318,7 +318,7 @@ public async Task CallService_OtlpEndpoint_RequiredClientCertificateValid_Succes var client = new LogsService.LogsServiceClient(channel); // Act - var response = await client.ExportAsync(new ExportLogsServiceRequest()); + var response = await client.ExportAsync(new ExportLogsServiceRequest()).ResponseAsync.DefaultTimeout(); // Assert Assert.Equal(0, response.PartialSuccess.RejectedLogRecords); @@ -344,7 +344,7 @@ public async Task CallService_OtlpEndpoint_RequiredClientCertificateSHA1Thumbpri config["Dashboard:Otlp:CertificateAuthOptions:AllowedCertificateTypes"] = "SelfSigned"; config["Dashboard:Otlp:CertificateAuthOptions:ValidateValidityPeriod"] = "false"; }); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); using var channel = IntegrationTestHelpers.CreateGrpcChannel( $"https://{app.OtlpServiceGrpcEndPointAccessor().EndPoint}", @@ -358,7 +358,7 @@ public async Task CallService_OtlpEndpoint_RequiredClientCertificateSHA1Thumbpri var client = new LogsService.LogsServiceClient(channel); // Act - var ex = await Assert.ThrowsAsync(() => client.ExportAsync(new ExportLogsServiceRequest()).ResponseAsync); + var ex = await Assert.ThrowsAsync(() => client.ExportAsync(new ExportLogsServiceRequest()).ResponseAsync).DefaultTimeout(); // Assert Assert.Equal(StatusCode.Unauthenticated, ex.StatusCode); @@ -382,7 +382,7 @@ public async Task CallService_OtlpEndpoint_RequiredClientCertificateValid_NotInA config["Authentication:Schemes:Certificate:AllowedCertificateTypes"] = "SelfSigned"; config["Authentication:Schemes:Certificate:ValidateValidityPeriod"] = "false"; }); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); var clientCertificates = new X509CertificateCollection(new[] { TestCertificateLoader.GetTestCertificate("eku.client.pfx") }); using var channel = IntegrationTestHelpers.CreateGrpcChannel( @@ -397,7 +397,7 @@ public async Task CallService_OtlpEndpoint_RequiredClientCertificateValid_NotInA var client = new LogsService.LogsServiceClient(channel); // Act - var ex = await Assert.ThrowsAsync(() => client.ExportAsync(new ExportLogsServiceRequest()).ResponseAsync); + var ex = await Assert.ThrowsAsync(() => client.ExportAsync(new ExportLogsServiceRequest()).ResponseAsync).DefaultTimeout(); // Assert Assert.Equal(StatusCode.Unauthenticated, ex.StatusCode); diff --git a/tests/Aspire.Dashboard.Tests/Integration/OtlpHttpServiceTests.cs b/tests/Aspire.Dashboard.Tests/Integration/OtlpHttpServiceTests.cs index 1933ff8757..9aea2ad1e7 100644 --- a/tests/Aspire.Dashboard.Tests/Integration/OtlpHttpServiceTests.cs +++ b/tests/Aspire.Dashboard.Tests/Integration/OtlpHttpServiceTests.cs @@ -34,7 +34,7 @@ public async Task CallService_OtlpHttpEndPoint_BigData_Success() { // Arrange await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.OtlpServiceHttpEndPointAccessor().EndPoint}"); @@ -44,10 +44,10 @@ public async Task CallService_OtlpHttpEndPoint_BigData_Success() content.Headers.TryAddWithoutValidation("content-type", OtlpHttpEndpointsBuilder.ProtobufContentType); // Act - var responseMessage = await httpClient.PostAsync("/v1/logs", content); + var responseMessage = await httpClient.PostAsync("/v1/logs", content).DefaultTimeout(); responseMessage.EnsureSuccessStatusCode(); - var response = ExportLogsServiceResponse.Parser.ParseFrom(await responseMessage.Content.ReadAsByteArrayAsync()); + var response = ExportLogsServiceResponse.Parser.ParseFrom(await responseMessage.Content.ReadAsByteArrayAsync().DefaultTimeout()); // Assert Assert.Equal(OtlpHttpEndpointsBuilder.ProtobufContentType, responseMessage.Content.Headers.GetValues("content-type").Single()); @@ -60,7 +60,7 @@ public async Task CallService_OtlpHttpEndPoint_ExceedRequestLimit_Failure() { // Arrange await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.OtlpServiceHttpEndPointAccessor().EndPoint}"); @@ -70,7 +70,7 @@ public async Task CallService_OtlpHttpEndPoint_ExceedRequestLimit_Failure() content.Headers.TryAddWithoutValidation("content-type", OtlpHttpEndpointsBuilder.ProtobufContentType); // Act - var responseMessage = await httpClient.PostAsync("/v1/logs", content); + var responseMessage = await httpClient.PostAsync("/v1/logs", content).DefaultTimeout(); // Assert Assert.Equal(HttpStatusCode.BadRequest, responseMessage.StatusCode); @@ -106,7 +106,7 @@ public async Task CallService_OtlpHttpEndPoint_RequiredApiKeyMissing_Failure() config[DashboardConfigNames.DashboardOtlpAuthModeName.ConfigKey] = OtlpAuthMode.ApiKey.ToString(); config[DashboardConfigNames.DashboardOtlpPrimaryApiKeyName.ConfigKey] = apiKey; }); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.OtlpServiceHttpEndPointAccessor().EndPoint}"); @@ -114,7 +114,7 @@ public async Task CallService_OtlpHttpEndPoint_RequiredApiKeyMissing_Failure() content.Headers.TryAddWithoutValidation("content-type", OtlpHttpEndpointsBuilder.ProtobufContentType); // Act - var responseMessage = await httpClient.PostAsync("/v1/logs", content); + var responseMessage = await httpClient.PostAsync("/v1/logs", content).DefaultTimeout(); // Assert Assert.Equal(HttpStatusCode.Unauthorized, responseMessage.StatusCode); @@ -130,7 +130,7 @@ public async Task CallService_OtlpHttpEndPoint_RequiredApiKeyWrong_Failure() config[DashboardConfigNames.DashboardOtlpAuthModeName.ConfigKey] = OtlpAuthMode.ApiKey.ToString(); config[DashboardConfigNames.DashboardOtlpPrimaryApiKeyName.ConfigKey] = apiKey; }); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.OtlpServiceHttpEndPointAccessor().EndPoint}"); @@ -142,7 +142,7 @@ public async Task CallService_OtlpHttpEndPoint_RequiredApiKeyWrong_Failure() requestMessage.Headers.TryAddWithoutValidation(OtlpApiKeyAuthenticationHandler.ApiKeyHeaderName, "WRONG"); // Act - var responseMessage = await httpClient.SendAsync(requestMessage); + var responseMessage = await httpClient.SendAsync(requestMessage).DefaultTimeout(); // Assert Assert.Equal(HttpStatusCode.Unauthorized, responseMessage.StatusCode); @@ -158,7 +158,7 @@ public async Task CallService_OtlpGrpcEndPoint_RequiredApiKeySent_Success() config[DashboardConfigNames.DashboardOtlpAuthModeName.ConfigKey] = OtlpAuthMode.ApiKey.ToString(); config[DashboardConfigNames.DashboardOtlpPrimaryApiKeyName.ConfigKey] = apiKey; }); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.OtlpServiceHttpEndPointAccessor().EndPoint}"); @@ -170,10 +170,10 @@ public async Task CallService_OtlpGrpcEndPoint_RequiredApiKeySent_Success() requestMessage.Headers.TryAddWithoutValidation(OtlpApiKeyAuthenticationHandler.ApiKeyHeaderName, apiKey); // Act - var responseMessage = await httpClient.SendAsync(requestMessage); + var responseMessage = await httpClient.SendAsync(requestMessage).DefaultTimeout(); responseMessage.EnsureSuccessStatusCode(); - var response = ExportLogsServiceResponse.Parser.ParseFrom(await responseMessage.Content.ReadAsByteArrayAsync()); + var response = ExportLogsServiceResponse.Parser.ParseFrom(await responseMessage.Content.ReadAsByteArrayAsync().DefaultTimeout()); // Assert Assert.Equal(OtlpHttpEndpointsBuilder.ProtobufContentType, responseMessage.Content.Headers.GetValues("content-type").Single()); @@ -192,7 +192,7 @@ public async Task CallService_BrowserEndPoint_Failure() // Change dashboard to HTTPS so the caller can negotiate a HTTP/2 connection. config[DashboardConfigNames.DashboardFrontendUrlName.ConfigKey] = "https://127.0.0.1:0"; }); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); using var httpClient = IntegrationTestHelpers.CreateHttpClient($"https://{app.FrontendSingleEndPointAccessor().EndPoint}", validationCallback: cert => @@ -204,7 +204,7 @@ public async Task CallService_BrowserEndPoint_Failure() content.Headers.TryAddWithoutValidation("content-type", OtlpHttpEndpointsBuilder.ProtobufContentType); // Act - var responseMessage = await httpClient.PostAsync("/v1/logs", content); + var responseMessage = await httpClient.PostAsync("/v1/logs", content).DefaultTimeout(); // Assert Assert.Equal(HttpStatusCode.Unauthorized, responseMessage.StatusCode); @@ -222,7 +222,7 @@ public async Task CallService_OtlpHttpEndPoint_UnsupportedContentType_Failure(st { dictionary[DashboardConfigNames.DashboardOtlpHttpUrlName.ConfigKey] = "http://127.0.0.1:0"; }); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); var endpoint = app.OtlpServiceHttpEndPointAccessor(); using var client = new HttpClient { BaseAddress = new Uri($"http://{endpoint.EndPoint}") }; @@ -234,7 +234,7 @@ public async Task CallService_OtlpHttpEndPoint_UnsupportedContentType_Failure(st } // Act - var responseMessage = await client.PostAsync("/v1/logs", content); + var responseMessage = await client.PostAsync("/v1/logs", content).DefaultTimeout(); // Assert Assert.Equal(HttpStatusCode.UnsupportedMediaType, responseMessage.StatusCode); @@ -250,7 +250,7 @@ public async Task CallService_OtlpHttpEndPoint_UnsupportedMethods_Failure(string { dictionary[DashboardConfigNames.DashboardOtlpHttpUrlName.ConfigKey] = "http://127.0.0.1:0"; }); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); var endpoint = app.OtlpServiceHttpEndPointAccessor(); using var client = new HttpClient { BaseAddress = new Uri($"http://{endpoint.EndPoint}") }; @@ -261,7 +261,7 @@ public async Task CallService_OtlpHttpEndPoint_UnsupportedMethods_Failure(string requestMessage.Content = content; // Act - var responseMessage = await client.SendAsync(requestMessage); + var responseMessage = await client.SendAsync(requestMessage).DefaultTimeout(); // Assert Assert.Equal(HttpStatusCode.NotFound, responseMessage.StatusCode); @@ -275,7 +275,7 @@ public async Task CallService_OtlpHttpEndPoint_Logs_Success() { dictionary[DashboardConfigNames.DashboardOtlpHttpUrlName.ConfigKey] = "http://127.0.0.1:0"; }); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); var endpoint = app.OtlpServiceHttpEndPointAccessor(); using var client = new HttpClient { BaseAddress = new Uri($"http://{endpoint.EndPoint}") }; @@ -284,7 +284,7 @@ public async Task CallService_OtlpHttpEndPoint_Logs_Success() using var content = new ByteArrayContent(request.ToByteArray()); content.Headers.TryAddWithoutValidation("content-type", OtlpHttpEndpointsBuilder.ProtobufContentType); - var responseMessage = await client.PostAsync("/v1/logs", content); + var responseMessage = await client.PostAsync("/v1/logs", content).DefaultTimeout(); responseMessage.EnsureSuccessStatusCode(); // Act @@ -304,7 +304,7 @@ public async Task CallService_OtlpHttpEndPoint_Traces_Success() { dictionary[DashboardConfigNames.DashboardOtlpHttpUrlName.ConfigKey] = "http://127.0.0.1:0"; }); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); var endpoint = app.OtlpServiceHttpEndPointAccessor(); using var client = new HttpClient { BaseAddress = new Uri($"http://{endpoint.EndPoint}") }; @@ -313,11 +313,11 @@ public async Task CallService_OtlpHttpEndPoint_Traces_Success() using var content = new ByteArrayContent(request.ToByteArray()); content.Headers.TryAddWithoutValidation("content-type", OtlpHttpEndpointsBuilder.ProtobufContentType); - var responseMessage = await client.PostAsync("/v1/traces", content); + var responseMessage = await client.PostAsync("/v1/traces", content).DefaultTimeout(); responseMessage.EnsureSuccessStatusCode(); // Act - var response = ExportTraceServiceResponse.Parser.ParseFrom(await responseMessage.Content.ReadAsByteArrayAsync()); + var response = ExportTraceServiceResponse.Parser.ParseFrom(await responseMessage.Content.ReadAsByteArrayAsync().DefaultTimeout()); // Assert Assert.Equal(OtlpHttpEndpointsBuilder.ProtobufContentType, responseMessage.Content.Headers.GetValues("content-type").Single()); @@ -333,7 +333,7 @@ public async Task CallService_OtlpHttpEndPoint_Metrics_Success() { dictionary[DashboardConfigNames.DashboardOtlpHttpUrlName.ConfigKey] = "http://127.0.0.1:0"; }); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); var endpoint = app.OtlpServiceHttpEndPointAccessor(); using var client = new HttpClient { BaseAddress = new Uri($"http://{endpoint.EndPoint}") }; @@ -342,11 +342,11 @@ public async Task CallService_OtlpHttpEndPoint_Metrics_Success() using var content = new ByteArrayContent(request.ToByteArray()); content.Headers.TryAddWithoutValidation("content-type", OtlpHttpEndpointsBuilder.ProtobufContentType); - var responseMessage = await client.PostAsync("/v1/metrics", content); + var responseMessage = await client.PostAsync("/v1/metrics", content).DefaultTimeout(); responseMessage.EnsureSuccessStatusCode(); // Act - var response = ExportMetricsServiceResponse.Parser.ParseFrom(await responseMessage.Content.ReadAsByteArrayAsync()); + var response = ExportMetricsServiceResponse.Parser.ParseFrom(await responseMessage.Content.ReadAsByteArrayAsync().DefaultTimeout()); // Assert Assert.Equal(OtlpHttpEndpointsBuilder.ProtobufContentType, responseMessage.Content.Headers.GetValues("content-type").Single()); diff --git a/tests/Aspire.Dashboard.Tests/Integration/Playwright/AppBarTests.cs b/tests/Aspire.Dashboard.Tests/Integration/Playwright/AppBarTests.cs index 2cfcaae25c..a2dd5ab618 100644 --- a/tests/Aspire.Dashboard.Tests/Integration/Playwright/AppBarTests.cs +++ b/tests/Aspire.Dashboard.Tests/Integration/Playwright/AppBarTests.cs @@ -24,11 +24,11 @@ public async Task AppBar_Change_Theme() // Arrange await RunTestAsync(async page => { - await PlaywrightFixture.GoToHomeAndWaitForDataGridLoad(page); + await PlaywrightFixture.GoToHomeAndWaitForDataGridLoad(page).DefaultTimeout(); - await SetAndVerifyTheme(Dialogs.SettingsDialogSystemTheme, null); // don't guess system theme - await SetAndVerifyTheme(Dialogs.SettingsDialogLightTheme, "light"); - await SetAndVerifyTheme(Dialogs.SettingsDialogDarkTheme, "dark"); + await SetAndVerifyTheme(Dialogs.SettingsDialogSystemTheme, null).DefaultTimeout(); // don't guess system theme + await SetAndVerifyTheme(Dialogs.SettingsDialogLightTheme, "light").DefaultTimeout(); + await SetAndVerifyTheme(Dialogs.SettingsDialogDarkTheme, "dark").DefaultTimeout(); async Task SetAndVerifyTheme(string checkboxText, string? expected) { @@ -70,9 +70,9 @@ public async Task AppBar_Change_Theme_ReloadPage() // Arrange await RunTestAsync(async page => { - await SetAndVerifyTheme(Dialogs.SettingsDialogSystemTheme, null); // don't guess system theme - await SetAndVerifyTheme(Dialogs.SettingsDialogLightTheme, "light"); - await SetAndVerifyTheme(Dialogs.SettingsDialogDarkTheme, "dark"); + await SetAndVerifyTheme(Dialogs.SettingsDialogSystemTheme, null).DefaultTimeout(); // don't guess system theme + await SetAndVerifyTheme(Dialogs.SettingsDialogLightTheme, "light").DefaultTimeout(); + await SetAndVerifyTheme(Dialogs.SettingsDialogDarkTheme, "dark").DefaultTimeout(); async Task SetAndVerifyTheme(string checkboxText, string? expected) { diff --git a/tests/Aspire.Dashboard.Tests/Integration/Playwright/BrowserTokenAuthenticationTests.cs b/tests/Aspire.Dashboard.Tests/Integration/Playwright/BrowserTokenAuthenticationTests.cs index e48b2c4968..9768497ed7 100644 --- a/tests/Aspire.Dashboard.Tests/Integration/Playwright/BrowserTokenAuthenticationTests.cs +++ b/tests/Aspire.Dashboard.Tests/Integration/Playwright/BrowserTokenAuthenticationTests.cs @@ -5,6 +5,7 @@ using Aspire.Dashboard.Tests.Integration.Playwright.Infrastructure; using Aspire.Hosting; using Aspire.Workload.Tests; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.Playwright; using Xunit; @@ -34,21 +35,22 @@ public async Task BrowserToken_LoginPage_Success_RedirectToResources() await RunTestAsync(async page => { // Act - var response = await page.GotoAsync("/"); + var response = await page.GotoAsync("/").DefaultTimeout(); var uri = new Uri(response!.Url); Assert.Equal("/login?returnUrl=%2F", uri.PathAndQuery); var tokenTextBox = page.GetByRole(AriaRole.Textbox); - await tokenTextBox.FillAsync("VALID_TOKEN"); + await tokenTextBox.FillAsync("VALID_TOKEN").DefaultTimeout(); var submitButton = page.GetByRole(AriaRole.Button); - await submitButton.ClickAsync(); + await submitButton.ClickAsync().DefaultTimeout(); // Assert await Assertions .Expect(page.GetByText(MockDashboardClient.TestResource1.DisplayName)) - .ToBeVisibleAsync(); + .ToBeVisibleAsync() + .DefaultTimeout(); }); } @@ -59,21 +61,22 @@ public async Task BrowserToken_LoginPage_Failure_DisplayFailureMessage() await RunTestAsync(async page => { // Act - var response = await page.GotoAsync("/"); + var response = await page.GotoAsync("/").DefaultTimeout(); var uri = new Uri(response!.Url); Assert.Equal("/login?returnUrl=%2F", uri.PathAndQuery); var tokenTextBox = page.GetByRole(AriaRole.Textbox); - await tokenTextBox.FillAsync("INVALID_TOKEN"); + await tokenTextBox.FillAsync("INVALID_TOKEN").DefaultTimeout(); var submitButton = page.GetByRole(AriaRole.Button); - await submitButton.ClickAsync(); + await submitButton.ClickAsync().DefaultTimeout(); // Assert await Assertions .Expect(page.GetByText("Invalid token")) - .ToBeVisibleAsync(); + .ToBeVisibleAsync() + .DefaultTimeout(); }); } @@ -84,12 +87,13 @@ public async Task BrowserToken_QueryStringToken_Success_RestrictToResources() await RunTestAsync(async page => { // Act - await page.GotoAsync("/login?t=VALID_TOKEN"); + await page.GotoAsync("/login?t=VALID_TOKEN").DefaultTimeout(); // Assert await Assertions .Expect(page.GetByText(MockDashboardClient.TestResource1.DisplayName)) - .ToBeVisibleAsync(); + .ToBeVisibleAsync() + .DefaultTimeout(); }); } @@ -100,10 +104,10 @@ public async Task BrowserToken_QueryStringToken_Failure_DisplayLoginPage() await RunTestAsync(async page => { // Act - await page.GotoAsync("/login?t=INVALID_TOKEN"); + await page.GotoAsync("/login?t=INVALID_TOKEN").DefaultTimeout(); var submitButton = page.GetByRole(AriaRole.Button); - var name = await submitButton.GetAttributeAsync("name"); + var name = await submitButton.GetAttributeAsync("name").DefaultTimeout(); // Assert Assert.Equal("submit-token", name); diff --git a/tests/Aspire.Dashboard.Tests/Integration/ResponseCompressionTests.cs b/tests/Aspire.Dashboard.Tests/Integration/ResponseCompressionTests.cs index 0d6ef83f6a..f019ee5529 100644 --- a/tests/Aspire.Dashboard.Tests/Integration/ResponseCompressionTests.cs +++ b/tests/Aspire.Dashboard.Tests/Integration/ResponseCompressionTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Dashboard.Utils; +using Microsoft.AspNetCore.InternalTesting; using System.Net; using System.Net.Http.Headers; using Xunit; @@ -16,7 +17,7 @@ public async Task Html_Responses_Are_Not_Compressed() { // Arrange await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(testOutputHelper); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); using var httpClientHandler = new HttpClientHandler { AutomaticDecompression = DecompressionMethods.None }; using var client = new HttpClient(httpClientHandler) { BaseAddress = new Uri($"http://{app.FrontendSingleEndPointAccessor().EndPoint}") }; @@ -24,7 +25,7 @@ public async Task Html_Responses_Are_Not_Compressed() // Act 1 var request = new HttpRequestMessage(HttpMethod.Get, DashboardUrls.StructuredLogsBasePath); request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("br")); - var response = await client.SendAsync(request); + var response = await client.SendAsync(request).DefaultTimeout(); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -38,7 +39,7 @@ public async Task Static_Asset_Responses_Are_Compressed(string path) { // Arrange await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(testOutputHelper); - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); using var httpClientHandler = new HttpClientHandler { AutomaticDecompression = DecompressionMethods.None }; using var client = new HttpClient(httpClientHandler) { BaseAddress = new Uri($"http://{app.FrontendSingleEndPointAccessor().EndPoint}") }; @@ -46,7 +47,7 @@ public async Task Static_Asset_Responses_Are_Compressed(string path) // Act 1 var request = new HttpRequestMessage(HttpMethod.Get, path); request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("br")); - var response = await client.SendAsync(request); + var response = await client.SendAsync(request).DefaultTimeout(); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); diff --git a/tests/Aspire.Dashboard.Tests/Integration/StartupTests.cs b/tests/Aspire.Dashboard.Tests/Integration/StartupTests.cs index cf73ffaf35..ad3b9e5ab1 100644 --- a/tests/Aspire.Dashboard.Tests/Integration/StartupTests.cs +++ b/tests/Aspire.Dashboard.Tests/Integration/StartupTests.cs @@ -37,7 +37,7 @@ public async Task EndPointAccessors_AppStarted_EndPointPortsAssigned() }); // Act - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); // Assert Assert.Collection(app.FrontendEndPointsAccessor, @@ -69,7 +69,7 @@ public async Task EndPointAccessors_AppStarted_IPv4OrIPv6() }); // Act - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); // Assert, Assert.Collection(app.FrontendEndPointsAccessor, @@ -137,7 +137,7 @@ public async Task Configuration_ConfigFilePathDoesntExist_Error() { data[DashboardConfigNames.DashboardConfigFilePathName.ConfigKey] = configFilePath; }); - }); + }).DefaultTimeout(); // Assert Assert.Contains(configFilePath, ex.Message); @@ -182,7 +182,7 @@ public async Task Configuration_FileConfigDirectoryReloadsChanges_Success() const string initialFrontendBrowserToken = "InitialSecretContent"; const string changedFrontendBrowserToken = "NewSecretContent"; var fileConfigDirectory = Directory.CreateTempSubdirectory(); - var browserTokenConfigFile = await CreateBrowserTokenConfigFileAsync(fileConfigDirectory, initialFrontendBrowserToken); + var browserTokenConfigFile = await CreateBrowserTokenConfigFileAsync(fileConfigDirectory, initialFrontendBrowserToken).DefaultTimeout(); try { var loggerFactory = IntegrationTestHelpers.CreateLoggerFactory(testOutputHelper); @@ -203,7 +203,7 @@ public async Task Configuration_FileConfigDirectoryReloadsChanges_Success() var initialBrowserTokenProvidedByConfiguration = localBuilder?.Configuration[DashboardConfigNames.DashboardFrontendBrowserTokenName.ConfigKey]; // update the browser token's config file and get the new value - await File.WriteAllTextAsync(browserTokenConfigFile, changedFrontendBrowserToken); + await File.WriteAllTextAsync(browserTokenConfigFile, changedFrontendBrowserToken).DefaultTimeout(); // Assert Assert.Equal(initialFrontendBrowserToken, initialBrowserTokenProvidedByConfiguration); @@ -234,7 +234,7 @@ public async Task Configuration_FileConfigDirectoryDoesntExist_Error() { data[DashboardConfigNames.DashboardFileConfigDirectoryName.ConfigKey] = fileConfigDirectory; }); - }); + }).DefaultTimeout(); // Assert Assert.Contains(fileConfigDirectory, ex.Message); @@ -252,7 +252,7 @@ public async Task Configuration_OptionsMonitor_CanReadConfiguration() }); // Act - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); // Assert Assert.Equal(OtlpAuthMode.ApiKey, app.DashboardOptionsMonitor.CurrentValue.Otlp.AuthMode); @@ -277,7 +277,7 @@ await ServerRetryHelper.BindPortWithRetry(async port => }); // Act - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); }, NullLogger.Instance); // Assert @@ -296,20 +296,20 @@ await ServerRetryHelper.BindPortWithRetry(async port => BaseAddress = new Uri($"https://{app.FrontendSingleEndPointAccessor().EndPoint}") }; var request = new HttpRequestMessage(HttpMethod.Get, "/"); - var response = await httpClient.SendAsync(request); + var response = await httpClient.SendAsync(request).DefaultTimeout(); response.EnsureSuccessStatusCode(); // Check OTLP service using var channel = IntegrationTestHelpers.CreateGrpcChannel($"https://{app.FrontendSingleEndPointAccessor().EndPoint}", testOutputHelper); var client = new LogsService.LogsServiceClient(channel); - var serviceResponse = await client.ExportAsync(new ExportLogsServiceRequest()); + var serviceResponse = await client.ExportAsync(new ExportLogsServiceRequest()).ResponseAsync.DefaultTimeout(); Assert.Equal(0, serviceResponse.PartialSuccess.RejectedLogRecords); } finally { if (app is not null) { - await app.DisposeAsync(); + await app.DisposeAsync().DefaultTimeout(); } } } @@ -334,7 +334,7 @@ await ServerRetryHelper.BindPortWithRetry(async port => testSink: testSink); // Act - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); }, NullLogger.Instance); // Assert @@ -359,7 +359,7 @@ await ServerRetryHelper.BindPortWithRetry(async port => { if (app is not null) { - await app.DisposeAsync(); + await app.DisposeAsync().DefaultTimeout(); } } } @@ -384,7 +384,7 @@ await ServerRetryHelper.BindPortWithRetry(async port => testSink: testSink); // Act - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); }, NullLogger.Instance); // Assert @@ -397,7 +397,7 @@ await ServerRetryHelper.BindPortWithRetry(async port => BaseAddress = new Uri($"http://{app.FrontendSingleEndPointAccessor().EndPoint}") }; var request = new HttpRequestMessage(HttpMethod.Get, "/"); - var responseMessage = await httpClient.SendAsync(request); + var responseMessage = await httpClient.SendAsync(request).DefaultTimeout(); responseMessage.EnsureSuccessStatusCode(); // Check OTLP service @@ -407,7 +407,7 @@ await ServerRetryHelper.BindPortWithRetry(async port => responseMessage = await httpClient.PostAsync("/v1/logs", content); responseMessage.EnsureSuccessStatusCode(); - var response = ExportLogsServiceResponse.Parser.ParseFrom(await responseMessage.Content.ReadAsByteArrayAsync()); + var response = ExportLogsServiceResponse.Parser.ParseFrom(await responseMessage.Content.ReadAsByteArrayAsync().DefaultTimeout()); Assert.Equal(OtlpHttpEndpointsBuilder.ProtobufContentType, responseMessage.Content.Headers.GetValues("content-type").Single()); Assert.False(responseMessage.Headers.Contains("content-security-policy")); @@ -417,7 +417,7 @@ await ServerRetryHelper.BindPortWithRetry(async port => { if (app is not null) { - await app.DisposeAsync(); + await app.DisposeAsync().DefaultTimeout(); } } } @@ -451,7 +451,7 @@ public async Task Configuration_AllowAnonymous_NoError() }); // Act - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); // Assert Assert.Equal(FrontendAuthMode.Unsecured, app.DashboardOptionsMonitor.CurrentValue.Frontend.AuthMode); @@ -492,7 +492,7 @@ public async Task Configuration_Logging_OverrideDefaults() clearLogFilterRules: false); // Assert - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); var options = app.Services.GetRequiredService>(); @@ -518,7 +518,7 @@ public async Task Configuration_Logging_FileConfig_OverrideDefaults() } } }; - await File.WriteAllTextAsync(configFilePath, configJson.ToString()); + await File.WriteAllTextAsync(configFilePath, configJson.ToString()).DefaultTimeout(); try { @@ -531,7 +531,7 @@ public async Task Configuration_Logging_FileConfig_OverrideDefaults() clearLogFilterRules: false); // Assert - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); var options = app.Services.GetRequiredService>(); @@ -553,7 +553,7 @@ public async Task LogOutput_DynamicPort_PortResolvedInLogs() await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(testOutputHelper, testSink: testSink); // Act - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); // Assert var l = testSink.Writes.Where(w => w.LoggerName == typeof(DashboardWebApplication).FullName).ToList(); @@ -623,14 +623,14 @@ await ServerRetryHelper.BindPortsWithRetry(async ports => }, testSink: testSink); // Act - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); }, NullLogger.Instance, portCount: 3); } finally { if (app is not null) { - await app.DisposeAsync(); + await app.DisposeAsync().DefaultTimeout(); } } @@ -685,12 +685,12 @@ public async Task EndPointAccessors_AppStarted_BrowserGet_Success() await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(testOutputHelper); // Act - await app.StartAsync(); + await app.StartAsync().DefaultTimeout(); using var client = new HttpClient { BaseAddress = new Uri($"http://{app.FrontendSingleEndPointAccessor().EndPoint}") }; // Act - var response = await client.GetAsync("/"); + var response = await client.GetAsync("/").DefaultTimeout(); // Assert response.EnsureSuccessStatusCode(); diff --git a/tests/Aspire.Dashboard.Tests/LocalBrowserStorageTests.cs b/tests/Aspire.Dashboard.Tests/LocalBrowserStorageTests.cs index 0740cc6e20..d93c4cab8b 100644 --- a/tests/Aspire.Dashboard.Tests/LocalBrowserStorageTests.cs +++ b/tests/Aspire.Dashboard.Tests/LocalBrowserStorageTests.cs @@ -5,6 +5,7 @@ using Aspire.Dashboard.Model.BrowserStorage; using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage; using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.JSInterop; using Xunit; @@ -32,7 +33,7 @@ public async Task SetUnprotectedAsync_JSInvokedWithJson(object? value, string re var localStorage = CreateBrowserLocalStorage(testJsonRuntime); // Act - await localStorage.SetUnprotectedAsync("MyKey", value); + await localStorage.SetUnprotectedAsync("MyKey", value).DefaultTimeout(); // Assert Assert.Equal("localStorage.setItem", identifier); @@ -57,7 +58,7 @@ public async Task GetUnprotectedAsync_HasValue_Success() var localStorage = CreateBrowserLocalStorage(testJsonRuntime); // Act - var result = await localStorage.GetUnprotectedAsync("MyKey"); + var result = await localStorage.GetUnprotectedAsync("MyKey").DefaultTimeout(); // Assert Assert.True(result.Success); @@ -83,7 +84,7 @@ public async Task GetUnprotectedAsync_NoValue_Failure() var localStorage = CreateBrowserLocalStorage(testJsonRuntime); // Act - var result = await localStorage.GetUnprotectedAsync("MyKey"); + var result = await localStorage.GetUnprotectedAsync("MyKey").DefaultTimeout(); // Assert Assert.False(result.Success); @@ -108,7 +109,7 @@ public async Task GetUnprotectedAsync_InvalidValue_Failure() var localStorage = CreateBrowserLocalStorage(testJsonRuntime); // Act - var result = await localStorage.GetUnprotectedAsync("MyKey"); + var result = await localStorage.GetUnprotectedAsync("MyKey").DefaultTimeout(); // Assert Assert.False(result.Success); diff --git a/tests/Aspire.Dashboard.Tests/Middleware/ValidateTokenMiddlewareTests.cs b/tests/Aspire.Dashboard.Tests/Middleware/ValidateTokenMiddlewareTests.cs index 77add73762..e887dba46e 100644 --- a/tests/Aspire.Dashboard.Tests/Middleware/ValidateTokenMiddlewareTests.cs +++ b/tests/Aspire.Dashboard.Tests/Middleware/ValidateTokenMiddlewareTests.cs @@ -5,6 +5,7 @@ using Aspire.Dashboard.Model; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -17,48 +18,48 @@ public class ValidateTokenMiddlewareTests [Fact] public async Task ValidateToken_NotBrowserTokenAuth_RedirectedToHomepage() { - using var host = await SetUpHostAsync(FrontendAuthMode.Unsecured, string.Empty); - var response = await host.GetTestClient().GetAsync("/login?t=test"); + using var host = await SetUpHostAsync(FrontendAuthMode.Unsecured, string.Empty).DefaultTimeout(); + var response = await host.GetTestClient().GetAsync("/login?t=test").DefaultTimeout(); Assert.Equal("/", response.Headers.Location?.OriginalString); } [Fact] public async Task ValidateToken_NotBrowserTokenAuth_RedirectedToReturnUrl() { - using var host = await SetUpHostAsync(FrontendAuthMode.Unsecured, string.Empty); - var response = await host.GetTestClient().GetAsync("/login?t=test&returnUrl=/test"); + using var host = await SetUpHostAsync(FrontendAuthMode.Unsecured, string.Empty).DefaultTimeout(); + var response = await host.GetTestClient().GetAsync("/login?t=test&returnUrl=/test").DefaultTimeout(); Assert.Equal("/test", response.Headers.Location?.OriginalString); } [Fact] public async Task ValidateToken_BrowserTokenAuth_WrongToken_RedirectsToLogin() { - using var host = await SetUpHostAsync(FrontendAuthMode.BrowserToken, "token"); - var response = await host.GetTestClient().GetAsync("/login?t=wrong"); + using var host = await SetUpHostAsync(FrontendAuthMode.BrowserToken, "token").DefaultTimeout(); + var response = await host.GetTestClient().GetAsync("/login?t=wrong").DefaultTimeout(); Assert.Equal("/login", response.Headers.Location?.OriginalString); } [Fact] public async Task ValidateToken_BrowserTokenAuth_WrongToken_RedirectsToLogin_WithReturnUrl() { - using var host = await SetUpHostAsync(FrontendAuthMode.BrowserToken, "token"); - var response = await host.GetTestClient().GetAsync("/login?t=wrong&returnUrl=/test"); + using var host = await SetUpHostAsync(FrontendAuthMode.BrowserToken, "token").DefaultTimeout(); + var response = await host.GetTestClient().GetAsync("/login?t=wrong&returnUrl=/test").DefaultTimeout(); Assert.Equal("/login?returnUrl=%2ftest", response.Headers.Location?.OriginalString); } [Fact] public async Task ValidateToken_BrowserTokenAuth_RightToken_RedirectsToHome() { - using var host = await SetUpHostAsync(FrontendAuthMode.BrowserToken, "token"); - var response = await host.GetTestClient().GetAsync("/login?t=token"); + using var host = await SetUpHostAsync(FrontendAuthMode.BrowserToken, "token").DefaultTimeout(); + var response = await host.GetTestClient().GetAsync("/login?t=token").DefaultTimeout(); Assert.Equal("/", response.Headers.Location?.OriginalString); } [Fact] public async Task ValidateToken_BrowserTokenAuth_RightToken_RedirectsToReturnUrl() { - using var host = await SetUpHostAsync(FrontendAuthMode.BrowserToken, "token"); - var response = await host.GetTestClient().GetAsync("/login?t=token&returnUrl=/test"); + using var host = await SetUpHostAsync(FrontendAuthMode.BrowserToken, "token").DefaultTimeout(); + var response = await host.GetTestClient().GetAsync("/login?t=token&returnUrl=/test").DefaultTimeout(); Assert.Equal("/test", response.Headers.Location?.OriginalString); } diff --git a/tests/Aspire.Dashboard.Tests/Model/DashboardClientTests.cs b/tests/Aspire.Dashboard.Tests/Model/DashboardClientTests.cs index 3e5c76cefc..a9797e78b2 100644 --- a/tests/Aspire.Dashboard.Tests/Model/DashboardClientTests.cs +++ b/tests/Aspire.Dashboard.Tests/Model/DashboardClientTests.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Xunit; +using Microsoft.AspNetCore.InternalTesting; namespace Aspire.Dashboard.Tests.Model; @@ -49,7 +50,7 @@ public async Task SubscribeResources_OnCancel_ChannelRemoved() Assert.Equal(0, instance.OutgoingResourceSubscriberCount); - var (_, subscription) = await client.SubscribeResourcesAsync(CancellationToken.None); + var (_, subscription) = await client.SubscribeResourcesAsync(CancellationToken.None).DefaultTimeout(); Assert.Equal(1, instance.OutgoingResourceSubscriberCount); @@ -60,9 +61,9 @@ public async Task SubscribeResources_OnCancel_ChannelRemoved() } }); - await cts.CancelAsync(); + cts.Cancel(); - await TaskHelpers.WaitIgnoreCancelAsync(readTask); + await TaskHelpers.WaitIgnoreCancelAsync(readTask).DefaultTimeout(); Assert.Equal(0, instance.OutgoingResourceSubscriberCount); } @@ -77,7 +78,7 @@ public async Task SubscribeResources_OnDispose_ChannelRemoved() Assert.Equal(0, instance.OutgoingResourceSubscriberCount); - var (_, subscription) = await client.SubscribeResourcesAsync(CancellationToken.None); + var (_, subscription) = await client.SubscribeResourcesAsync(CancellationToken.None).DefaultTimeout(); Assert.Equal(1, instance.OutgoingResourceSubscriberCount); @@ -88,11 +89,11 @@ public async Task SubscribeResources_OnDispose_ChannelRemoved() } }); - await instance.DisposeAsync(); + await instance.DisposeAsync().DefaultTimeout(); Assert.Equal(0, instance.OutgoingResourceSubscriberCount); - await TaskHelpers.WaitIgnoreCancelAsync(readTask); + await TaskHelpers.WaitIgnoreCancelAsync(readTask).DefaultTimeout(); } [Fact] @@ -100,9 +101,9 @@ public async Task SubscribeResources_ThrowsIfDisposed() { await using IDashboardClient client = CreateResourceServiceClient(); - await client.DisposeAsync(); + await client.DisposeAsync().DefaultTimeout(); - await Assert.ThrowsAsync(() => client.SubscribeResourcesAsync(CancellationToken.None)); + await Assert.ThrowsAsync(() => client.SubscribeResourcesAsync(CancellationToken.None)).DefaultTimeout(); } [Fact] @@ -115,11 +116,11 @@ public async Task SubscribeResources_IncreasesSubscriberCount() Assert.Equal(0, instance.OutgoingResourceSubscriberCount); - _ = await client.SubscribeResourcesAsync(CancellationToken.None); + _ = await client.SubscribeResourcesAsync(CancellationToken.None).DefaultTimeout(); Assert.Equal(1, instance.OutgoingResourceSubscriberCount); - await instance.DisposeAsync(); + await instance.DisposeAsync().DefaultTimeout(); Assert.Equal(0, instance.OutgoingResourceSubscriberCount); } @@ -144,7 +145,7 @@ public async Task SubscribeResources_HasInitialData_InitialDataReturned() CreatedAt = Timestamp.FromDateTime(DateTime.UtcNow), }]); - var (initialData, subscription) = await subscribeTask; + var (initialData, subscription) = await subscribeTask.DefaultTimeout(); Assert.Single(initialData); } diff --git a/tests/Aspire.Dashboard.Tests/OtlpApiKeyAuthenticationHandlerTests.cs b/tests/Aspire.Dashboard.Tests/OtlpApiKeyAuthenticationHandlerTests.cs index 7cd5c6d360..7827da851a 100644 --- a/tests/Aspire.Dashboard.Tests/OtlpApiKeyAuthenticationHandlerTests.cs +++ b/tests/Aspire.Dashboard.Tests/OtlpApiKeyAuthenticationHandlerTests.cs @@ -6,6 +6,7 @@ using Aspire.Dashboard.Configuration; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Xunit; @@ -18,10 +19,10 @@ public class OtlpApiKeyAuthenticationHandlerTests public async Task AuthenticateAsync_NoHeader_Failure() { // Arrange - var handler = await CreateAuthHandlerAsync(primaryApiKey: "abc", secondaryApiKey: null, otlpApiKeyHeader: null); + var handler = await CreateAuthHandlerAsync(primaryApiKey: "abc", secondaryApiKey: null, otlpApiKeyHeader: null).DefaultTimeout(); // Act - var result = await handler.AuthenticateAsync(); + var result = await handler.AuthenticateAsync().DefaultTimeout(); // Assert Assert.NotNull(result.Failure); @@ -32,10 +33,10 @@ public async Task AuthenticateAsync_NoHeader_Failure() public async Task AuthenticateAsync_BigApiKeys_NoMatch_Failure() { // Arrange - var handler = await CreateAuthHandlerAsync(primaryApiKey: new string('!', 1000), secondaryApiKey: null, otlpApiKeyHeader: new string('!', 999)); + var handler = await CreateAuthHandlerAsync(primaryApiKey: new string('!', 1000), secondaryApiKey: null, otlpApiKeyHeader: new string('!', 999)).DefaultTimeout(); // Act - var result = await handler.AuthenticateAsync(); + var result = await handler.AuthenticateAsync().DefaultTimeout(); // Assert Assert.NotNull(result.Failure); @@ -46,10 +47,10 @@ public async Task AuthenticateAsync_BigApiKeys_NoMatch_Failure() public async Task AuthenticateAsync_BigApiKeys_Match_Success() { // Arrange - var handler = await CreateAuthHandlerAsync(primaryApiKey: new string('!', 1000), secondaryApiKey: null, otlpApiKeyHeader: new string('!', 1000)); + var handler = await CreateAuthHandlerAsync(primaryApiKey: new string('!', 1000), secondaryApiKey: null, otlpApiKeyHeader: new string('!', 1000)).DefaultTimeout(); // Act - var result = await handler.AuthenticateAsync(); + var result = await handler.AuthenticateAsync().DefaultTimeout(); // Assert Assert.Null(result.Failure); @@ -63,10 +64,10 @@ public async Task AuthenticateAsync_BigApiKeys_Match_Success() public async Task AuthenticateAsync_MatchHeader_Success(string primaryApiKey, string? secondaryApiKey, string otlpApiKeyHeader, bool success) { // Arrange - var handler = await CreateAuthHandlerAsync(primaryApiKey, secondaryApiKey, otlpApiKeyHeader); + var handler = await CreateAuthHandlerAsync(primaryApiKey, secondaryApiKey, otlpApiKeyHeader).DefaultTimeout(); // Act - var result = await handler.AuthenticateAsync(); + var result = await handler.AuthenticateAsync().DefaultTimeout(); // Assert Assert.Equal(success, result.Failure == null); diff --git a/tests/Aspire.Dashboard.Tests/ResourceOutgoingPeerResolverTests.cs b/tests/Aspire.Dashboard.Tests/ResourceOutgoingPeerResolverTests.cs index 64d431c765..5509fa4209 100644 --- a/tests/Aspire.Dashboard.Tests/ResourceOutgoingPeerResolverTests.cs +++ b/tests/Aspire.Dashboard.Tests/ResourceOutgoingPeerResolverTests.cs @@ -5,6 +5,7 @@ using System.Threading.Channels; using Aspire.Dashboard.Model; using Aspire.Tests.Shared.DashboardModel; +using Microsoft.AspNetCore.InternalTesting; using Xunit; namespace Aspire.Dashboard.Tests; @@ -138,17 +139,17 @@ public async Task OnPeerChanges_DataUpdates_EventRaised() GetChanges())); // Assert 1 - readValue = await resultChannel.Reader.ReadAsync(); + readValue = await resultChannel.Reader.ReadAsync().DefaultTimeout(); Assert.Equal(1, readValue); // Act 2 await sourceChannel.Writer.WriteAsync(new ResourceViewModelChange(ResourceViewModelChangeType.Upsert, CreateResource("test2"))); // Assert 2 - readValue = await resultChannel.Reader.ReadAsync(); + readValue = await resultChannel.Reader.ReadAsync().DefaultTimeout(); Assert.Equal(2, readValue); - await resolver.DisposeAsync(); + await resolver.DisposeAsync().DefaultTimeout(); async IAsyncEnumerable> GetChanges([EnumeratorCancellation] CancellationToken cancellationToken = default) { diff --git a/tests/Aspire.Dashboard.Tests/TelemetryRepositoryTests/LogTests.cs b/tests/Aspire.Dashboard.Tests/TelemetryRepositoryTests/LogTests.cs index 5d30846c64..0630b0edd8 100644 --- a/tests/Aspire.Dashboard.Tests/TelemetryRepositoryTests/LogTests.cs +++ b/tests/Aspire.Dashboard.Tests/TelemetryRepositoryTests/LogTests.cs @@ -8,6 +8,7 @@ using Aspire.Dashboard.Otlp.Storage; using Aspire.Dashboard.Tests.Integration; using Google.Protobuf.Collections; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.Logging; using OpenTelemetry.Proto.Logs.V1; using Xunit; @@ -434,7 +435,7 @@ public async Task Subscriptions_AddLog() // Assert 1 Assert.Equal(0, addContext1.FailureCount); - await newApplicationsTcs.Task; + await newApplicationsTcs.Task.DefaultTimeout(); var applications = repository.GetApplications(); Assert.Collection(applications, @@ -469,7 +470,7 @@ public async Task Subscriptions_AddLog() } }); - await newLogsTcs.Task; + await newLogsTcs.Task.DefaultTimeout(); // Assert 2 Assert.Equal(0, addContext2.FailureCount); @@ -563,10 +564,10 @@ public async Task Subscription_RaisedFromDifferentContext_InitialContextPreserve }); } - await task; + await task.DefaultTimeout(); // Assert - var callbackValue = await tcs.Task; + var callbackValue = await tcs.Task.DefaultTimeout(); Assert.Equal("CustomValue", callbackValue); } @@ -704,7 +705,7 @@ public async Task Subscription_MultipleUpdates_MinExecuteIntervalApplied() }); // Assert - var read1 = await resultChannel.Reader.ReadAsync(); + var read1 = await resultChannel.Reader.ReadAsync().DefaultTimeout(); Assert.Equal(1, read1); logger.LogInformation("Received log 1 callback"); @@ -725,7 +726,7 @@ public async Task Subscription_MultipleUpdates_MinExecuteIntervalApplied() } }); - var read2 = await resultChannel.Reader.ReadAsync(); + var read2 = await resultChannel.Reader.ReadAsync().DefaultTimeout(); Assert.Equal(2, read2); logger.LogInformation("Received log 2 callback"); From 79407c6353da90375117f0497903e3aa17600636 Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Wed, 30 Oct 2024 00:50:55 -0500 Subject: [PATCH 13/44] Extract EventHubs and Cosmos Tests from EndToEndTests and into Aspire.Hosting.Azure.Tests (#6538) * Extract EventHubs functional test to Azure tests. * Extract CosmosDB tests from EndToEnd tests --- .../IntegrationServicesFixture.cs | 19 ------- .../IntegrationServicesTests.cs | 25 ---------- .../Aspire.Hosting.Azure.Tests.csproj | 5 +- .../AzureCosmosDBEmulatorFunctionalTests.cs | 29 ++++++++--- .../AzureEventHubsExtensionsTests.cs | 49 +++++++++++++++++-- .../DistributedApplicationTests.cs | 4 +- .../ManifestGenerationTests.cs | 21 +------- tests/cosmos.module.bicep | 37 -------------- tests/eventhubns.module.bicep | 36 -------------- tests/testproject/Common/TestResourceNames.cs | 5 +- .../TestProject.AppHost/TestProgram.cs | 10 ---- .../TestProject.AppHost.csproj | 3 -- .../Cosmos/CosmosExtensions.cs | 48 ------------------ .../Cosmos/EFCoreCosmosDbContext.cs | 14 ------ .../Cosmos/EFCoreCosmosExtensions.cs | 25 ---------- .../EventHubs/EventHubsExtensions.cs | 36 -------------- .../Program.cs | 43 ---------------- .../TestProject.IntegrationServiceA.csproj | 3 -- 18 files changed, 71 insertions(+), 341 deletions(-) delete mode 100644 tests/cosmos.module.bicep delete mode 100644 tests/eventhubns.module.bicep delete mode 100644 tests/testproject/TestProject.IntegrationServiceA/Cosmos/CosmosExtensions.cs delete mode 100644 tests/testproject/TestProject.IntegrationServiceA/Cosmos/EFCoreCosmosDbContext.cs delete mode 100644 tests/testproject/TestProject.IntegrationServiceA/Cosmos/EFCoreCosmosExtensions.cs delete mode 100644 tests/testproject/TestProject.IntegrationServiceA/EventHubs/EventHubsExtensions.cs diff --git a/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs b/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs index 416e09c528..2528adde17 100644 --- a/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs +++ b/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Runtime.InteropServices; using Xunit; using Xunit.Abstractions; using Aspire.TestProject; @@ -100,8 +99,6 @@ public Task DumpComponentLogsAsync(TestResourceNames resource, ITestOutputHelper string component = resource switch { - TestResourceNames.cosmos or TestResourceNames.efcosmos => "cosmos", - TestResourceNames.eventhubs => "eventhubs", TestResourceNames.postgres or TestResourceNames.efnpgsql => "postgres", TestResourceNames.redis => "redis", _ => throw new ArgumentException($"Unknown resource: {resource}") @@ -133,8 +130,6 @@ private static TestResourceNames GetResourcesToSkip() { TestResourceNames resourcesToInclude = TestScenario switch { - "cosmos" => TestResourceNames.cosmos | TestResourceNames.efcosmos, - "eventhubs" => TestResourceNames.eventhubs, "basicservices" => TestResourceNames.redis | TestResourceNames.postgres | TestResourceNames.efnpgsql, @@ -144,20 +139,6 @@ private static TestResourceNames GetResourcesToSkip() TestResourceNames resourcesToSkip = TestResourceNames.All & ~resourcesToInclude; - // always skip cosmos on macos/arm64 - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && RuntimeInformation.ProcessArchitecture == Architecture.Arm64) - { - resourcesToSkip |= TestResourceNames.cosmos; - } - if (string.IsNullOrEmpty(TestScenario)) - { - // no scenario specified - if (BuildEnvironment.IsRunningOnCI) - { - resourcesToSkip |= TestResourceNames.cosmos; - } - } - // always skip the dashboard resourcesToSkip |= TestResourceNames.dashboard; diff --git a/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs b/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs index 208d1da67a..226686adff 100644 --- a/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs +++ b/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs @@ -1,12 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Runtime.InteropServices; using Xunit; using Xunit.Abstractions; using Aspire.TestProject; using Aspire.Workload.Tests; -using Microsoft.DotNet.XUnitExtensions; namespace Aspire.EndToEnd.Tests; @@ -45,29 +43,6 @@ public Task VerifyComponentWorks(TestResourceNames resourceName) }); [Fact] - [Trait("scenario", "eventhubs")] - public Task VerifyAzureEventHubsComponentWorks() - => VerifyComponentWorks(TestResourceNames.eventhubs); - - [ActiveIssue("https://github.com/dotnet/aspire/issues/5820")] - [ConditionalTheory] - [Trait("scenario", "cosmos")] - [InlineData(TestResourceNames.cosmos)] - [InlineData(TestResourceNames.efcosmos)] - public Task VerifyCosmosComponentWorks(TestResourceNames resourceName) - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && RuntimeInformation.ProcessArchitecture == Architecture.Arm64) - { - throw new SkipTestException($"Skipping 'cosmos' test because the emulator isn't supported on macOS ARM64."); - } - - return VerifyComponentWorks(resourceName); - } - - [Fact] - // Include all the scenarios here so this test gets run for all of them. - // https://github.com/dotnet/aspire/issues/5820 - // [Trait("scenario", "cosmos")] [Trait("scenario", "basicservices")] public Task VerifyHealthyOnIntegrationServiceA() => RunTestAsync(async () => diff --git a/tests/Aspire.Hosting.Azure.Tests/Aspire.Hosting.Azure.Tests.csproj b/tests/Aspire.Hosting.Azure.Tests/Aspire.Hosting.Azure.Tests.csproj index 04321282d3..c28a6677a8 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Aspire.Hosting.Azure.Tests.csproj +++ b/tests/Aspire.Hosting.Azure.Tests/Aspire.Hosting.Azure.Tests.csproj @@ -23,11 +23,12 @@ + + + - - diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureCosmosDBEmulatorFunctionalTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureCosmosDBEmulatorFunctionalTests.cs index de5199ebac..55e22117f7 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureCosmosDBEmulatorFunctionalTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureCosmosDBEmulatorFunctionalTests.cs @@ -5,6 +5,7 @@ using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Utils; using Microsoft.Azure.Cosmos; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; @@ -59,7 +60,7 @@ public async Task VerifyWaitForOnCosmosDBEmulatorBlocksDependentResources() await app.StopAsync(); } - [Fact(Skip = "Using CosmosDB emulator in integration tests leads to flaky tests")] + [Fact(Skip = "Using CosmosDB emulator in integration tests leads to flaky tests - https://github.com/dotnet/aspire/issues/5820")] [RequiresDocker(Reason = "CosmosDB emulator is needed for this test")] public async Task VerifyCosmosResource() { @@ -89,13 +90,9 @@ public async Task VerifyCosmosResource() await app.StartAsync(); var hb = Host.CreateApplicationBuilder(); - - hb.Configuration.AddInMemoryCollection(new Dictionary - { - [$"ConnectionStrings:{db.Resource.Name}"] = await db.Resource.ConnectionStringExpression.GetValueAsync(default) - }); - + hb.Configuration[$"ConnectionStrings:{db.Resource.Name}"] = await db.Resource.ConnectionStringExpression.GetValueAsync(default); hb.AddAzureCosmosClient(db.Resource.Name); + hb.AddCosmosDbContext(db.Resource.Name, databaseName); using var host = hb.Build(); @@ -104,6 +101,7 @@ public async Task VerifyCosmosResource() // This needs to be outside the pipeline because when the CosmosClient is disposed, // there is an exception in the pipeline using var cosmosClient = host.Services.GetRequiredService(); + using var dbContext = host.Services.GetRequiredService(); await pipeline.ExecuteAsync(async token => { @@ -115,10 +113,15 @@ await pipeline.ExecuteAsync(async token => Assert.True(results.Count == 1); Assert.True(results.First() == 1); + + await dbContext.Database.EnsureCreatedAsync(token); + dbContext.AddRange([new Entry(), new Entry()]); + var count = await dbContext.SaveChangesAsync(token); + Assert.Equal(2, count); }, cts.Token); } - [Fact(Skip = "Using CosmosDB emulator in integration tests leads to flaky tests")] + [Fact(Skip = "Using CosmosDB emulator in integration tests leads to flaky tests - https://github.com/dotnet/aspire/issues/5820")] [RequiresDocker] public async Task WithDataVolumeShouldPersistStateBetweenUsages() { @@ -246,3 +249,13 @@ await pipeline.ExecuteAsync(async token => DockerUtils.AttemptDeleteDockerVolume(volumeName); } } + +public class EFCoreCosmosDbContext(DbContextOptions options) : DbContext(options) +{ + public DbSet Entries { get; set; } +} + +public record Entry +{ + public Guid Id { get; set; } = Guid.NewGuid(); +} diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureEventHubsExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureEventHubsExtensionsTests.cs index dd1e987667..1e98ec6bfd 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureEventHubsExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureEventHubsExtensionsTests.cs @@ -1,13 +1,18 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Aspire.Hosting.Utils; -using Aspire.Hosting.Azure.EventHubs; -using Xunit; +using System.Text; +using Aspire.Components.Common.Tests; using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Azure.EventHubs; +using Aspire.Hosting.Utils; +using Azure.Messaging.EventHubs; +using Azure.Messaging.EventHubs.Consumer; +using Azure.Messaging.EventHubs.Producer; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; -using Aspire.Components.Common.Tests; +using Microsoft.Extensions.Hosting; +using Xunit; using Xunit.Abstractions; namespace Aspire.Hosting.Azure.Tests; @@ -56,6 +61,40 @@ public async Task VerifyWaitForOnEventHubsEmulatorBlocksDependentResources() await app.StopAsync(); } + [Fact] + [RequiresDocker] + public async Task VerifyAzureEventHubsEmulatorResource() + { + using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper); + var eventHub = builder.AddAzureEventHubs("eventhubns") + .RunAsEmulator() + .AddEventHub("hub"); + + using var app = builder.Build(); + await app.StartAsync(); + + var hb = Host.CreateApplicationBuilder(); + hb.Configuration["ConnectionStrings:eventhubns"] = await eventHub.Resource.ConnectionStringExpression.GetValueAsync(CancellationToken.None); + hb.AddAzureEventHubProducerClient("eventhubns", settings => settings.EventHubName = "hub"); + hb.AddAzureEventHubConsumerClient("eventhubns", settings => settings.EventHubName = "hub"); + + using var host = hb.Build(); + await host.StartAsync(); + + var producerClient = host.Services.GetRequiredService(); + var consumerClient = host.Services.GetRequiredService(); + + // If no exception is thrown when awaited, the Event Hubs service has acknowledged + // receipt and assumed responsibility for delivery of the set of events to its partition. + await producerClient.SendAsync([new EventData(Encoding.UTF8.GetBytes("hello worlds"))]); + + await foreach (var partitionEvent in consumerClient.ReadEventsAsync(new ReadEventOptions { MaximumWaitTime = TimeSpan.FromSeconds(5) })) + { + Assert.Equal("hello worlds", Encoding.UTF8.GetString(partitionEvent.Data.EventBody.ToArray())); + break; + } + } + [Fact] public void AzureEventHubsUseEmulatorCallbackWithWithDataBindMountResultsInBindMountAnnotationWithDefaultPath() { @@ -79,7 +118,7 @@ public void AzureEventHubsUseEmulatorCallbackWithWithDataBindMountResultsInBindM using var builder = TestDistributedApplicationBuilder.Create(); var eventHubs = builder.AddAzureEventHubs("eh").RunAsEmulator(configureContainer: builder => { - builder.WithDataBindMount("mydata"); + builder.WithDataBindMount("mydata"); }); // Ignoring the annotation created for the custom Config.json file diff --git a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs index 3621eb5a10..11b5c57c05 100644 --- a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs +++ b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs @@ -611,9 +611,7 @@ public async Task KubernetesHasResourceNameForContainersAndExes() var expectedContainerResources = new HashSet() { "redis", - "postgres", - "cosmos", - "eventhubns" + "postgres" }; await foreach (var resource in s.WatchAsync().DefaultTimeout()) diff --git a/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs b/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs index 0b47c442cd..c07b2cbe82 100644 --- a/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs +++ b/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs @@ -418,9 +418,7 @@ public void VerifyTestProgramFullManifest() "HTTP_PORTS": "{integrationservicea.bindings.http.targetPort}", "SKIP_RESOURCES": "None", "ConnectionStrings__redis": "{redis.connectionString}", - "ConnectionStrings__postgresdb": "{postgresdb.connectionString}", - "ConnectionStrings__cosmos": "{cosmos.connectionString}", - "ConnectionStrings__eventhubns": "{eventhubns.connectionString}" + "ConnectionStrings__postgresdb": "{postgresdb.connectionString}" }, "bindings": { "http": { @@ -472,23 +470,6 @@ public void VerifyTestProgramFullManifest() "type": "value.v0", "connectionString": "{postgres.connectionString};Database=postgresdb" }, - "cosmos": { - "type": "azure.bicep.v0", - "connectionString": "{cosmos.secretOutputs.connectionString}", - "path": "cosmos.module.bicep", - "params": { - "keyVaultName": "" - } - }, - "eventhubns": { - "type": "azure.bicep.v0", - "connectionString": "{eventhubns.outputs.eventHubsEndpoint}", - "path": "eventhubns.module.bicep", - "params": { - "principalId": "", - "principalType": "" - } - }, "postgres-password": { "type": "parameter.v0", "value": "{postgres-password.inputs.value}", diff --git a/tests/cosmos.module.bicep b/tests/cosmos.module.bicep deleted file mode 100644 index cf2940e9ef..0000000000 --- a/tests/cosmos.module.bicep +++ /dev/null @@ -1,37 +0,0 @@ -@description('The location for the resource(s) to be deployed.') -param location string = resourceGroup().location - -param keyVaultName string - -resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { - name: keyVaultName -} - -resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2024-08-15' = { - name: take('cosmos-${uniqueString(resourceGroup().id)}', 44) - location: location - properties: { - locations: [ - { - locationName: location - failoverPriority: 0 - } - ] - consistencyPolicy: { - defaultConsistencyLevel: 'Session' - } - databaseAccountOfferType: 'Standard' - } - kind: 'GlobalDocumentDB' - tags: { - 'aspire-resource-name': 'cosmos' - } -} - -resource connectionString 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = { - name: 'connectionString' - properties: { - value: 'AccountEndpoint=${cosmos.properties.documentEndpoint};AccountKey=${cosmos.listKeys().primaryMasterKey}' - } - parent: keyVault -} \ No newline at end of file diff --git a/tests/eventhubns.module.bicep b/tests/eventhubns.module.bicep deleted file mode 100644 index c013957eb5..0000000000 --- a/tests/eventhubns.module.bicep +++ /dev/null @@ -1,36 +0,0 @@ -@description('The location for the resource(s) to be deployed.') -param location string = resourceGroup().location - -param sku string = 'Standard' - -param principalId string - -param principalType string - -resource eventhubns 'Microsoft.EventHub/namespaces@2024-01-01' = { - name: take('eventhubns-${uniqueString(resourceGroup().id)}', 256) - location: location - sku: { - name: sku - } - tags: { - 'aspire-resource-name': 'eventhubns' - } -} - -resource eventhubns_AzureEventHubsDataOwner 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(eventhubns.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f526a384-b230-433a-b45c-95f59c4a2dec')) - properties: { - principalId: principalId - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f526a384-b230-433a-b45c-95f59c4a2dec') - principalType: principalType - } - scope: eventhubns -} - -resource hub 'Microsoft.EventHub/namespaces/eventhubs@2024-01-01' = { - name: 'hub' - parent: eventhubns -} - -output eventHubsEndpoint string = eventhubns.properties.serviceBusEndpoint \ No newline at end of file diff --git a/tests/testproject/Common/TestResourceNames.cs b/tests/testproject/Common/TestResourceNames.cs index 80ec042b6c..205c47e95f 100644 --- a/tests/testproject/Common/TestResourceNames.cs +++ b/tests/testproject/Common/TestResourceNames.cs @@ -7,14 +7,11 @@ namespace Aspire.TestProject; public enum TestResourceNames { None = 0, - cosmos = 1 << 0, dashboard = 1 << 1, postgres = 1 << 7, redis = 1 << 9, efnpgsql = 1 << 11, - eventhubs = 1 << 13, - efcosmos = 1 << 17, - All = cosmos | dashboard | postgres | redis | efnpgsql | eventhubs | efcosmos + All = dashboard | postgres | redis | efnpgsql } public static class TestResourceNamesExtensions diff --git a/tests/testproject/TestProject.AppHost/TestProgram.cs b/tests/testproject/TestProject.AppHost/TestProgram.cs index e6956dc222..a38a3be630 100644 --- a/tests/testproject/TestProject.AppHost/TestProgram.cs +++ b/tests/testproject/TestProject.AppHost/TestProgram.cs @@ -83,16 +83,6 @@ private TestProgram( .AddDatabase(postgresDbName); IntegrationServiceABuilder = IntegrationServiceABuilder.WithReference(postgres); } - if (!resourcesToSkip.HasFlag(TestResourceNames.cosmos) || !resourcesToSkip.HasFlag(TestResourceNames.efcosmos)) - { - var cosmos = AppBuilder.AddAzureCosmosDB("cosmos").RunAsEmulator(); - IntegrationServiceABuilder = IntegrationServiceABuilder.WithReference(cosmos); - } - if (!resourcesToSkip.HasFlag(TestResourceNames.eventhubs)) - { - var eventHub = AppBuilder.AddAzureEventHubs("eventhubns").RunAsEmulator().AddEventHub("hub"); - IntegrationServiceABuilder = IntegrationServiceABuilder.WithReference(eventHub); - } } AppBuilder.Services.AddHostedService(); diff --git a/tests/testproject/TestProject.AppHost/TestProject.AppHost.csproj b/tests/testproject/TestProject.AppHost/TestProject.AppHost.csproj index 52ec70b694..2995a30adf 100644 --- a/tests/testproject/TestProject.AppHost/TestProject.AppHost.csproj +++ b/tests/testproject/TestProject.AppHost/TestProject.AppHost.csproj @@ -9,9 +9,6 @@ - - - diff --git a/tests/testproject/TestProject.IntegrationServiceA/Cosmos/CosmosExtensions.cs b/tests/testproject/TestProject.IntegrationServiceA/Cosmos/CosmosExtensions.cs deleted file mode 100644 index 9db35db9d2..0000000000 --- a/tests/testproject/TestProject.IntegrationServiceA/Cosmos/CosmosExtensions.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Text; -using Aspire.TestProject; -using Microsoft.Azure.Cosmos; -using Polly; - -public static class CosmosExtensions -{ - public static void MapCosmosApi(this WebApplication app) - { - app.MapGet("/cosmos/verify", VerifyCosmosAsync); - } - - private static async Task VerifyCosmosAsync(CosmosClient cosmosClient) - { - StringBuilder errorMessageBuilder = new(); - try - { - ResiliencePipeline pipeline = ResilienceUtils.GetDefaultResiliencePipelineBuilder(args => - { - errorMessageBuilder.AppendLine($"{Environment.NewLine}Service retry #{args.AttemptNumber} due to {args.Outcome.Exception}"); - return ValueTask.CompletedTask; - }).Build(); - - var db = await pipeline.ExecuteAsync( - async token => (await cosmosClient.CreateDatabaseIfNotExistsAsync("db", cancellationToken: token)).Database); - - var container = (await db.CreateContainerIfNotExistsAsync("todos", "/id")).Container; - - var id = Guid.NewGuid().ToString(); - var title = "Do some work."; - - var item = await container.CreateItemAsync(new - { - id, - title - }); - - return item.Resource.id == id ? Results.Ok() : Results.Problem(); - } - catch (Exception e) - { - return Results.Problem($"Error: {e}{Environment.NewLine}** Previous retries: {errorMessageBuilder}"); - } - } -} diff --git a/tests/testproject/TestProject.IntegrationServiceA/Cosmos/EFCoreCosmosDbContext.cs b/tests/testproject/TestProject.IntegrationServiceA/Cosmos/EFCoreCosmosDbContext.cs deleted file mode 100644 index 3d5b0acbc5..0000000000 --- a/tests/testproject/TestProject.IntegrationServiceA/Cosmos/EFCoreCosmosDbContext.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.EntityFrameworkCore; - -public class EFCoreCosmosDbContext(DbContextOptions options) : DbContext(options) -{ - public DbSet Entries { get; set; } -} - -public record Entry -{ - public Guid Id { get; set; } = Guid.NewGuid(); -} diff --git a/tests/testproject/TestProject.IntegrationServiceA/Cosmos/EFCoreCosmosExtensions.cs b/tests/testproject/TestProject.IntegrationServiceA/Cosmos/EFCoreCosmosExtensions.cs deleted file mode 100644 index 8c4233ff4d..0000000000 --- a/tests/testproject/TestProject.IntegrationServiceA/Cosmos/EFCoreCosmosExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -public static class EFCoreCosmosExtensions -{ - public static void MapEFCoreCosmosApi(this WebApplication app) - { - app.MapGet("/efcosmos/verify", VerifyEFCoreCosmosAsync); - } - - private static async Task VerifyEFCoreCosmosAsync(EFCoreCosmosDbContext dbContext) - { - try - { - await dbContext.Database.EnsureCreatedAsync(); - dbContext.AddRange([new Entry(), new Entry()]); - var count = await dbContext.SaveChangesAsync(); - return count == 2 ? Results.Ok() : Results.Problem($"Expected 2 entries but got {count}"); - } - catch (Exception e) - { - return Results.Problem(e.ToString()); - } - } -} diff --git a/tests/testproject/TestProject.IntegrationServiceA/EventHubs/EventHubsExtensions.cs b/tests/testproject/TestProject.IntegrationServiceA/EventHubs/EventHubsExtensions.cs deleted file mode 100644 index c194898ac4..0000000000 --- a/tests/testproject/TestProject.IntegrationServiceA/EventHubs/EventHubsExtensions.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Text; -using Azure.Messaging.EventHubs; -using Azure.Messaging.EventHubs.Producer; -using Azure.Messaging.EventHubs.Consumer; - -public static class EventHubsExtensions -{ - public static void MapEventHubsApi(this WebApplication app) - { - app.MapGet("/eventhubs/verify", VerifyEventHubsAsync); - } - - private static async Task VerifyEventHubsAsync(EventHubProducerClient producerClient, EventHubConsumerClient consumerClient) - { - try - { - // If no exception is thrown when awaited, the Event Hubs service has acknowledged - // receipt and assumed responsibility for delivery of the set of events to its partition. - await producerClient.SendAsync([new EventData(Encoding.UTF8.GetBytes("hello worlds"))]); - - await foreach (var partition in consumerClient.ReadEventsAsync(new ReadEventOptions { MaximumWaitTime = TimeSpan.FromSeconds(5) })) - { - return Results.Ok(); - } - - return Results.Problem("No events were read."); - } - catch (Exception e) - { - return Results.Problem($"Error: {e}{Environment.NewLine}**"); - } - } -} diff --git a/tests/testproject/TestProject.IntegrationServiceA/Program.cs b/tests/testproject/TestProject.IntegrationServiceA/Program.cs index 6cf3f820af..bcc60798b9 100644 --- a/tests/testproject/TestProject.IntegrationServiceA/Program.cs +++ b/tests/testproject/TestProject.IntegrationServiceA/Program.cs @@ -21,34 +21,6 @@ { builder.AddNpgsqlDbContext("postgresdb"); } -if (!resourcesToSkip.HasFlag(TestResourceNames.eventhubs)) -{ - builder.AddAzureEventHubProducerClient("eventhubsns", settings => settings.EventHubName = "hub"); - builder.AddAzureEventHubConsumerClient("eventhubsns", settings => settings.EventHubName = "hub"); -} - -if (!resourcesToSkip.HasFlag(TestResourceNames.cosmos) || !resourcesToSkip.HasFlag(TestResourceNames.efcosmos)) -{ - builder.AddAzureCosmosClient("cosmos"); -} - -if (!resourcesToSkip.HasFlag(TestResourceNames.efcosmos)) -{ - builder.AddCosmosDbContext("cosmos", "cosmos"); -} - -if (!resourcesToSkip.HasFlag(TestResourceNames.eventhubs)) -{ - builder.AddAzureEventHubProducerClient("eventhubns", settings => - { - settings.EventHubName = "hub"; - }); - - builder.AddAzureEventHubConsumerClient("eventhubns", settings => - { - settings.EventHubName = "hub"; - }); -} // Ensure healthChecks are added. Some components like Cosmos // don't add this @@ -76,19 +48,4 @@ app.MapNpgsqlEFCoreApi(); } -if (!resourcesToSkip.HasFlag(TestResourceNames.cosmos)) -{ - app.MapCosmosApi(); -} - -if (!resourcesToSkip.HasFlag(TestResourceNames.efcosmos)) -{ - app.MapEFCoreCosmosApi(); -} - -if (!resourcesToSkip.HasFlag(TestResourceNames.eventhubs)) -{ - app.MapEventHubsApi(); -} - app.Run(); diff --git a/tests/testproject/TestProject.IntegrationServiceA/TestProject.IntegrationServiceA.csproj b/tests/testproject/TestProject.IntegrationServiceA/TestProject.IntegrationServiceA.csproj index bdfd67ce17..a47e0f6965 100644 --- a/tests/testproject/TestProject.IntegrationServiceA/TestProject.IntegrationServiceA.csproj +++ b/tests/testproject/TestProject.IntegrationServiceA/TestProject.IntegrationServiceA.csproj @@ -11,12 +11,9 @@ - - - From 13bcaa03819f5e342d96bdffed92a50c7175198d Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 30 Oct 2024 22:26:00 +0800 Subject: [PATCH 14/44] Add tooltip to error logs link (#6546) * Add tooltip to error logs link * Fix one error tooltip --- .../ResourcesGridColumns/UnreadLogErrorsBadge.razor | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Aspire.Dashboard/Components/ResourcesGridColumns/UnreadLogErrorsBadge.razor b/src/Aspire.Dashboard/Components/ResourcesGridColumns/UnreadLogErrorsBadge.razor index 76b5ebf8c3..abe86b3f87 100644 --- a/src/Aspire.Dashboard/Components/ResourcesGridColumns/UnreadLogErrorsBadge.razor +++ b/src/Aspire.Dashboard/Components/ResourcesGridColumns/UnreadLogErrorsBadge.razor @@ -5,7 +5,9 @@ @if (_unviewedCount > 0) { - Date: Wed, 30 Oct 2024 23:23:05 +0000 Subject: [PATCH 15/44] Update dependencies from https://github.com/microsoft/usvc-apiserver build 0.8.13 (#6560) Microsoft.DeveloperControlPlane.darwin-amd64 , Microsoft.DeveloperControlPlane.darwin-arm64 , Microsoft.DeveloperControlPlane.linux-amd64 , Microsoft.DeveloperControlPlane.linux-arm64 , Microsoft.DeveloperControlPlane.windows-386 , Microsoft.DeveloperControlPlane.windows-amd64 , Microsoft.DeveloperControlPlane.windows-arm64 From Version 0.8.11 -> To Version 0.8.13 Co-authored-by: dotnet-maestro[bot] --- eng/Version.Details.xml | 28 ++++++++++++++-------------- eng/Versions.props | 14 +++++++------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 9029bac634..b469d18fd0 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,33 +1,33 @@ - + https://github.com/microsoft/usvc-apiserver - cc584ed90a91ef8b818f9537ffd1d11639c2448a + 9582c787e7963a2acf11111e5bc5a4938e7fe3ab - + https://github.com/microsoft/usvc-apiserver - cc584ed90a91ef8b818f9537ffd1d11639c2448a + 9582c787e7963a2acf11111e5bc5a4938e7fe3ab - + https://github.com/microsoft/usvc-apiserver - cc584ed90a91ef8b818f9537ffd1d11639c2448a + 9582c787e7963a2acf11111e5bc5a4938e7fe3ab - + https://github.com/microsoft/usvc-apiserver - cc584ed90a91ef8b818f9537ffd1d11639c2448a + 9582c787e7963a2acf11111e5bc5a4938e7fe3ab - + https://github.com/microsoft/usvc-apiserver - cc584ed90a91ef8b818f9537ffd1d11639c2448a + 9582c787e7963a2acf11111e5bc5a4938e7fe3ab - + https://github.com/microsoft/usvc-apiserver - cc584ed90a91ef8b818f9537ffd1d11639c2448a + 9582c787e7963a2acf11111e5bc5a4938e7fe3ab - + https://github.com/microsoft/usvc-apiserver - cc584ed90a91ef8b818f9537ffd1d11639c2448a + 9582c787e7963a2acf11111e5bc5a4938e7fe3ab https://github.com/dotnet/extensions diff --git a/eng/Versions.props b/eng/Versions.props index 24642005aa..9985929f8e 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -27,13 +27,13 @@ 8.0.100-rtm.23512.16 - 0.8.11 - 0.8.11 - 0.8.11 - 0.8.11 - 0.8.11 - 0.8.11 - 0.8.11 + 0.8.13 + 0.8.13 + 0.8.13 + 0.8.13 + 0.8.13 + 0.8.13 + 0.8.13 9.0.0-beta.24516.2 9.0.0-beta.24516.2 From 92b8506f05f41bf2ba6634ed6b0dd63a46b64ca1 Mon Sep 17 00:00:00 2001 From: Dan Moseley Date: Wed, 30 Oct 2024 17:26:25 -0600 Subject: [PATCH 16/44] fix typo in code (#6562) --- .../AzureContainerAppsInfrastructure.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs index 243b1d561d..b89800bac5 100644 --- a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs +++ b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs @@ -153,7 +153,7 @@ public void BuildContainerApp(AzureResourceInfrastructure c) }; // TODO: Add managed identities only when required - AddManagedIdentites(containerAppResource); + AddManagedIdentities(containerAppResource); containerAppResource.EnvironmentId = containerAppIdParam; @@ -857,7 +857,7 @@ private void AddSecrets(ContainerAppConfiguration config) } } - private void AddManagedIdentites(ContainerApp app) + private void AddManagedIdentities(ContainerApp app) { if (_managedIdentityIdParameter is null) { From a764ccaba76f1d8f3b3b6891843170321cfdc8e6 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 31 Oct 2024 14:06:45 +0800 Subject: [PATCH 17/44] Move env vars to bottom of resource details (#6552) --- .../Components/Controls/ResourceDetails.razor | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/Aspire.Dashboard/Components/Controls/ResourceDetails.razor b/src/Aspire.Dashboard/Components/Controls/ResourceDetails.razor index b19d704ad4..c43801ffbb 100644 --- a/src/Aspire.Dashboard/Components/Controls/ResourceDetails.razor +++ b/src/Aspire.Dashboard/Components/Controls/ResourceDetails.razor @@ -71,19 +71,6 @@ HighlightText="@_filter" GridTemplateColumns="1fr 1.5fr" /> - -
- - @FilteredEnvironmentVariables.Count() - -
- -
@if (Resource.IsContainer()) { @@ -173,6 +160,18 @@ + +
+ + @FilteredEnvironmentVariables.Count() + +
+ +
From 34d6aabf330dd4ea0bf69fca138c8c1ba1250fce Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 31 Oct 2024 14:35:34 +0800 Subject: [PATCH 18/44] Don't display telemetry actions when no telemetry (#6568) --- .../Controls/ResourceActions.razor.cs | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/src/Aspire.Dashboard/Components/Controls/ResourceActions.razor.cs b/src/Aspire.Dashboard/Components/Controls/ResourceActions.razor.cs index a245d8826c..fd1af1c7ac 100644 --- a/src/Aspire.Dashboard/Components/Controls/ResourceActions.razor.cs +++ b/src/Aspire.Dashboard/Components/Controls/ResourceActions.razor.cs @@ -81,44 +81,44 @@ protected override void OnParametersSet() // Show telemetry menu items if there is telemetry for the resource. var hasTelemetryApplication = TelemetryRepository.GetApplicationByCompositeName(Resource.Name) != null; - var telemetryTooltip = !hasTelemetryApplication ? Loc[nameof(Resources.Resources.ResourceActionTelemetryTooltip)] : string.Empty; - _menuItems.Add(new MenuButtonItem { IsDivider = true }); - _menuItems.Add(new MenuButtonItem + if (hasTelemetryApplication) { - Text = Loc[nameof(Resources.Resources.ResourceActionStructuredLogsText)], - Icon = s_structuredLogsIcon, - OnClick = () => + var telemetryTooltip = !hasTelemetryApplication ? Loc[nameof(Resources.Resources.ResourceActionTelemetryTooltip)] : string.Empty; + _menuItems.Add(new MenuButtonItem { IsDivider = true }); + _menuItems.Add(new MenuButtonItem { - NavigationManager.NavigateTo(DashboardUrls.StructuredLogsUrl(resource: GetResourceName(Resource))); - return Task.CompletedTask; - }, - Tooltip = telemetryTooltip, - IsDisabled = !hasTelemetryApplication - }); - _menuItems.Add(new MenuButtonItem - { - Text = Loc[nameof(Resources.Resources.ResourceActionTracesText)], - Icon = s_tracesIcon, - OnClick = () => + Text = Loc[nameof(Resources.Resources.ResourceActionStructuredLogsText)], + Icon = s_structuredLogsIcon, + OnClick = () => + { + NavigationManager.NavigateTo(DashboardUrls.StructuredLogsUrl(resource: GetResourceName(Resource))); + return Task.CompletedTask; + }, + Tooltip = telemetryTooltip + }); + _menuItems.Add(new MenuButtonItem { - NavigationManager.NavigateTo(DashboardUrls.TracesUrl(resource: GetResourceName(Resource))); - return Task.CompletedTask; - }, - Tooltip = telemetryTooltip, - IsDisabled = !hasTelemetryApplication - }); - _menuItems.Add(new MenuButtonItem - { - Text = Loc[nameof(Resources.Resources.ResourceActionMetricsText)], - Icon = s_metricsIcon, - OnClick = () => + Text = Loc[nameof(Resources.Resources.ResourceActionTracesText)], + Icon = s_tracesIcon, + OnClick = () => + { + NavigationManager.NavigateTo(DashboardUrls.TracesUrl(resource: GetResourceName(Resource))); + return Task.CompletedTask; + }, + Tooltip = telemetryTooltip + }); + _menuItems.Add(new MenuButtonItem { - NavigationManager.NavigateTo(DashboardUrls.MetricsUrl(resource: GetResourceName(Resource))); - return Task.CompletedTask; - }, - Tooltip = telemetryTooltip, - IsDisabled = !hasTelemetryApplication - }); + Text = Loc[nameof(Resources.Resources.ResourceActionMetricsText)], + Icon = s_metricsIcon, + OnClick = () => + { + NavigationManager.NavigateTo(DashboardUrls.MetricsUrl(resource: GetResourceName(Resource))); + return Task.CompletedTask; + }, + Tooltip = telemetryTooltip + }); + } // If display is desktop then we display highlighted commands next to the ... button. if (ViewportInformation.IsDesktop) From 8da37a2c850574b306508d12f4e38d1dc8d98afc Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 31 Oct 2024 19:04:17 -0400 Subject: [PATCH 19/44] [ci] playground tests - show live output from the tests (#6564) * [ci] playground tests - show live output from the tests * Address review feedback from @ damianedwards Add `ShowLiveOutput=true` for playground, endtoend, and workload tests. * address review feedbacvk from @ bradwilson --- tests/Aspire.EndToEnd.Tests/.runsettings | 3 +++ tests/Aspire.Playground.Tests/.runsettings | 3 +++ tests/Aspire.Workload.Tests/.runsettings | 3 +++ tests/helix/send-to-helix-buildonhelixtests.targets | 2 +- 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/Aspire.EndToEnd.Tests/.runsettings b/tests/Aspire.EndToEnd.Tests/.runsettings index 591a45e7ab..348b55730e 100644 --- a/tests/Aspire.EndToEnd.Tests/.runsettings +++ b/tests/Aspire.EndToEnd.Tests/.runsettings @@ -1,5 +1,8 @@ + + true + 900000 diff --git a/tests/Aspire.Playground.Tests/.runsettings b/tests/Aspire.Playground.Tests/.runsettings index d70cb0f813..1eb28e0858 100644 --- a/tests/Aspire.Playground.Tests/.runsettings +++ b/tests/Aspire.Playground.Tests/.runsettings @@ -1,5 +1,8 @@ + + true + 1200000 diff --git a/tests/Aspire.Workload.Tests/.runsettings b/tests/Aspire.Workload.Tests/.runsettings index d70cb0f813..1eb28e0858 100644 --- a/tests/Aspire.Workload.Tests/.runsettings +++ b/tests/Aspire.Workload.Tests/.runsettings @@ -1,5 +1,8 @@ + + true + 1200000 diff --git a/tests/helix/send-to-helix-buildonhelixtests.targets b/tests/helix/send-to-helix-buildonhelixtests.targets index bf9241cd4f..5a4fadc180 100644 --- a/tests/helix/send-to-helix-buildonhelixtests.targets +++ b/tests/helix/send-to-helix-buildonhelixtests.targets @@ -35,7 +35,7 @@ <_TestBlameArguments Include="--blame-crash-dump-type full" /> - <_TestRunCommandArguments Include="dotnet test -s .runsettings --results-directory $(_HelixLogsPath)" /> + <_TestRunCommandArguments Include="dotnet test -s .runsettings --results-directory $(_HelixLogsPath) -v:n" /> <_TestRunCommandArguments Include="@(_TestBlameArguments, ' ')" />
From 3e9773a7c06ecff3f2b743d8d596bc42ec795be0 Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Thu, 31 Oct 2024 21:56:59 -0500 Subject: [PATCH 20/44] Add more AsProvisioningParameter overloads (#6583) Adding more AsProvisioningParameter overloads to allow for easily referencing endpoints and any custom ReferenceExpression in a bicep parameter. Fix #6534 --- .../AzureProvisioningResourceExtensions.cs | 73 +++++++++++-- .../PublicAPI.Unshipped.txt | 2 + ...zureProvisioningResourceExtensionsTests.cs | 103 ++++++++++++++++++ 3 files changed, 167 insertions(+), 11 deletions(-) create mode 100644 tests/Aspire.Hosting.Azure.Tests/AzureProvisioningResourceExtensionsTests.cs diff --git a/src/Aspire.Hosting.Azure/AzureProvisioningResourceExtensions.cs b/src/Aspire.Hosting.Azure/AzureProvisioningResourceExtensions.cs index 33b8d27f00..b0405c0b15 100644 --- a/src/Aspire.Hosting.Azure/AzureProvisioningResourceExtensions.cs +++ b/src/Aspire.Hosting.Azure/AzureProvisioningResourceExtensions.cs @@ -73,17 +73,7 @@ public static ProvisioningParameter AsProvisioningParameter(this IResourceBuilde infrastructure.AspireResource.Parameters[parameterName] = parameterResourceBuilder.Resource; - var parameter = infrastructure.GetParameters().FirstOrDefault(p => p.BicepIdentifier == parameterName); - if (parameter is null) - { - parameter = new ProvisioningParameter(parameterName, typeof(string)) - { - IsSecure = parameterResourceBuilder.Resource.Secret - }; - infrastructure.Add(parameter); - } - - return parameter; + return GetOrAddParameter(infrastructure, parameterName, parameterResourceBuilder.Resource.Secret); } /// @@ -112,10 +102,71 @@ public static ProvisioningParameter AsProvisioningParameter(this BicepOutputRefe infrastructure.AspireResource.Parameters[parameterName] = outputReference; + return GetOrAddParameter(infrastructure, parameterName); + } + + /// + /// Creates a new in , or reuses an existing bicep parameter if one with + /// the same name already exists, that corresponds to . + /// + /// + /// The to use for the value of the . + /// + /// The that contains the . + /// The name of the parameter to be assigned. + /// + /// The corresponding that was found or newly created. + /// + /// + /// This is useful when assigning a to the value of an Aspire . + /// + public static ProvisioningParameter AsProvisioningParameter(this EndpointReference endpointReference, AzureResourceInfrastructure infrastructure, string parameterName) + { + ArgumentNullException.ThrowIfNull(endpointReference); + ArgumentNullException.ThrowIfNull(infrastructure); + ArgumentException.ThrowIfNullOrEmpty(parameterName); + + infrastructure.AspireResource.Parameters[parameterName] = endpointReference; + + return GetOrAddParameter(infrastructure, parameterName); + } + + /// + /// Creates a new in , or reuses an existing bicep parameter if one with + /// the same name already exists, that corresponds to . + /// + /// + /// The that represents the value to use for the . + /// + /// The that contains the . + /// The name of the parameter to be assigned. + /// + /// The corresponding that was found or newly created. + /// + /// + /// This is useful when assigning a to the value of an Aspire . + /// + public static ProvisioningParameter AsProvisioningParameter(this ReferenceExpression expression, AzureResourceInfrastructure infrastructure, string parameterName) + { + ArgumentNullException.ThrowIfNull(expression); + ArgumentNullException.ThrowIfNull(infrastructure); + ArgumentException.ThrowIfNullOrEmpty(parameterName); + + infrastructure.AspireResource.Parameters[parameterName] = expression; + + return GetOrAddParameter(infrastructure, parameterName); + } + + private static ProvisioningParameter GetOrAddParameter(AzureResourceInfrastructure infrastructure, string parameterName, bool? isSecure = null) + { var parameter = infrastructure.GetParameters().FirstOrDefault(p => p.BicepIdentifier == parameterName); if (parameter is null) { parameter = new ProvisioningParameter(parameterName, typeof(string)); + if (isSecure.HasValue) + { + parameter.IsSecure = isSecure.Value; + }; infrastructure.Add(parameter); } diff --git a/src/Aspire.Hosting.Azure/PublicAPI.Unshipped.txt b/src/Aspire.Hosting.Azure/PublicAPI.Unshipped.txt index 830233db18..5ab7eba5d7 100644 --- a/src/Aspire.Hosting.Azure/PublicAPI.Unshipped.txt +++ b/src/Aspire.Hosting.Azure/PublicAPI.Unshipped.txt @@ -34,7 +34,9 @@ override Aspire.Hosting.Azure.AzureProvisioningResource.GetBicepTemplateString() static Aspire.Hosting.AzureBicepResourceExtensions.WithParameter(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! name, Aspire.Hosting.ApplicationModel.EndpointReference! value) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.AzureBicepResourceExtensions.WithParameter(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! name, Aspire.Hosting.ApplicationModel.ReferenceExpression! value) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.AzureProvisioningResourceExtensions.AddAzureInfrastructure(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, System.Action! configureInfrastructure) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.AzureProvisioningResourceExtensions.AsProvisioningParameter(this Aspire.Hosting.ApplicationModel.EndpointReference! endpointReference, Aspire.Hosting.Azure.AzureResourceInfrastructure! infrastructure, string! parameterName) -> Azure.Provisioning.ProvisioningParameter! static Aspire.Hosting.AzureProvisioningResourceExtensions.AsProvisioningParameter(this Aspire.Hosting.ApplicationModel.IResourceBuilder! parameterResourceBuilder, Aspire.Hosting.Azure.AzureResourceInfrastructure! infrastructure, string? parameterName = null) -> Azure.Provisioning.ProvisioningParameter! +static Aspire.Hosting.AzureProvisioningResourceExtensions.AsProvisioningParameter(this Aspire.Hosting.ApplicationModel.ReferenceExpression! expression, Aspire.Hosting.Azure.AzureResourceInfrastructure! infrastructure, string! parameterName) -> Azure.Provisioning.ProvisioningParameter! static Aspire.Hosting.AzureProvisioningResourceExtensions.AsProvisioningParameter(this Aspire.Hosting.Azure.BicepOutputReference! outputReference, Aspire.Hosting.Azure.AzureResourceInfrastructure! infrastructure, string? parameterName = null) -> Azure.Provisioning.ProvisioningParameter! static Aspire.Hosting.AzureProvisioningResourceExtensions.ConfigureInfrastructure(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, System.Action! configure) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.AzureResourceExtensions.GetBicepIdentifier(this Aspire.Hosting.ApplicationModel.IAzureResource! resource) -> string! diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureProvisioningResourceExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureProvisioningResourceExtensionsTests.cs new file mode 100644 index 0000000000..b2981bd1c1 --- /dev/null +++ b/tests/Aspire.Hosting.Azure.Tests/AzureProvisioningResourceExtensionsTests.cs @@ -0,0 +1,103 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Utils; +using Azure.Provisioning.AppContainers; +using Xunit; +using Xunit.Abstractions; + +namespace Aspire.Hosting.Azure.Tests; + +public class AzureProvisioningResourceExtensionsTests(ITestOutputHelper output) +{ + [Fact] + public async Task AsProvisioningParameterTests() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var apiProject = builder.AddProject("api", launchProfileName: null) + .WithHttpsEndpoint(); + + var endpointReference = apiProject.GetEndpoint("https"); + var referenceExpression = ReferenceExpression.Create($"prefix:{endpointReference.Property(EndpointProperty.Host)}:{endpointReference.Property(EndpointProperty.Port)}"); + + var resource1 = builder.AddAzureInfrastructure("resource1", infrastructure => + { + var endpointAddressParam = endpointReference.AsProvisioningParameter(infrastructure, parameterName: "endpointAddressParam"); + var someExpressionParam = referenceExpression.AsProvisioningParameter(infrastructure, parameterName: "someExpressionParam"); + + var app = new ContainerApp("app"); + app.Template.Scale.Rules = + [ + new ContainerAppScaleRule() + { + Name = "temp", + Custom = new ContainerAppCustomScaleRule() + { + CustomScaleRuleType= "external", + Metadata = + { + { "address", endpointAddressParam }, + { "someExpression", someExpressionParam }, + } + } + } + ]; + infrastructure.Add(app); + }); + + var manifest = await ManifestUtils.GetManifestWithBicep(resource1.Resource); + + var expectedManifest = """ + { + "type": "azure.bicep.v0", + "path": "resource1.module.bicep", + "params": { + "endpointAddressParam": "{api.bindings.https.url}", + "someExpressionParam": "prefix:{api.bindings.https.host}:{api.bindings.https.port}" + } + } + """; + Assert.Equal(expectedManifest, manifest.ManifestNode.ToString()); + + var expectedBicep = """ + @description('The location for the resource(s) to be deployed.') + param location string = resourceGroup().location + + param endpointAddressParam string + + param someExpressionParam string + + resource app 'Microsoft.App/containerApps@2024-03-01' = { + name: take('app-${uniqueString(resourceGroup().id)}', 32) + location: location + properties: { + template: { + scale: { + rules: [ + { + name: 'temp' + custom: { + type: 'external' + metadata: { + address: endpointAddressParam + someExpression: someExpressionParam + } + } + } + ] + } + } + } + } + """; + output.WriteLine(manifest.BicepText); + Assert.Equal(expectedBicep, manifest.BicepText); + } + + private sealed class Project : IProjectMetadata + { + public string ProjectPath => "project"; + } +} From 6d1d86927aa9d821f907103c00624b01fcd9d497 Mon Sep 17 00:00:00 2001 From: Michael Collins Date: Thu, 31 Oct 2024 23:19:57 -0700 Subject: [PATCH 21/44] Add SchedulerHostAddress to DaprSidecarOptions (#6478) I enhanced DaprSidecarOptions by adding the SchedulerHostAddress property. This will allow the developer to specify the host address of the Dapr scheduler service if not running on localhost (for example, if developing using Docker Compose, the scheduler service will be running on a different host). I enhanced the DaprDistributedApplicationLifecycleHook to pass the --scheduler-host-address parameter to the Dapr sidecar if the DaprSidecarOptions.SchedulerHostAddress property is specified. Resolves dotnet/aspire#6477 --- .../DaprDistributedApplicationLifecycleHook.cs | 2 ++ src/Aspire.Hosting.Dapr/DaprSidecarOptions.cs | 9 +++++++++ src/Aspire.Hosting.Dapr/PublicAPI.Unshipped.txt | 3 ++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Aspire.Hosting.Dapr/DaprDistributedApplicationLifecycleHook.cs b/src/Aspire.Hosting.Dapr/DaprDistributedApplicationLifecycleHook.cs index 417a4a7cda..cf0520fdfd 100644 --- a/src/Aspire.Hosting.Dapr/DaprDistributedApplicationLifecycleHook.cs +++ b/src/Aspire.Hosting.Dapr/DaprDistributedApplicationLifecycleHook.cs @@ -142,6 +142,7 @@ public async Task BeforeStartAsync(DistributedApplicationModel appModel, Cancell ModelNamedArg("--resources-path", aggregateResourcesPaths), ModelNamedArg("--run-file", NormalizePath(sidecarOptions?.RunFile)), ModelNamedArg("--runtime-path", NormalizePath(sidecarOptions?.RuntimePath)), + ModelNamedArg("--scheduler-host-address", sidecarOptions?.SchedulerHostAddress), ModelNamedArg("--unix-domain-socket", sidecarOptions?.UnixDomainSocket), PostOptionsArgs(Args(sidecarOptions?.Command))); @@ -262,6 +263,7 @@ public async Task BeforeStartAsync(DistributedApplicationModel appModel, Cancell context.Writer.TryWriteStringArray("resourcesPath", sidecarOptions?.ResourcesPaths.Select(path => context.GetManifestRelativePath(path))); context.Writer.TryWriteString("runFile", context.GetManifestRelativePath(sidecarOptions?.RunFile)); context.Writer.TryWriteString("runtimePath", context.GetManifestRelativePath(sidecarOptions?.RuntimePath)); + context.Writer.TryWriteString("schedulerHostAddress", sidecarOptions?.SchedulerHostAddress); context.Writer.TryWriteString("unixDomainSocket", sidecarOptions?.UnixDomainSocket); context.Writer.WriteEndObject(); diff --git a/src/Aspire.Hosting.Dapr/DaprSidecarOptions.cs b/src/Aspire.Hosting.Dapr/DaprSidecarOptions.cs index 48109aee7c..547272f839 100644 --- a/src/Aspire.Hosting.Dapr/DaprSidecarOptions.cs +++ b/src/Aspire.Hosting.Dapr/DaprSidecarOptions.cs @@ -157,6 +157,15 @@ public sealed record DaprSidecarOptions /// public string? RuntimePath { get; init; } + /// + /// Gets or sets the address of the scheduler service. + /// + /// + /// The format is either "hostname" for the default port or "hostname:port" for a custom port. + /// The default is "localhost". + /// + public string? SchedulerHostAddress { get; init; } + /// /// Gets or sets the path to a Unix Domain Socket (UDS) directory. /// diff --git a/src/Aspire.Hosting.Dapr/PublicAPI.Unshipped.txt b/src/Aspire.Hosting.Dapr/PublicAPI.Unshipped.txt index 074c6ad103..9ca12865a2 100644 --- a/src/Aspire.Hosting.Dapr/PublicAPI.Unshipped.txt +++ b/src/Aspire.Hosting.Dapr/PublicAPI.Unshipped.txt @@ -1,2 +1,3 @@ #nullable enable - +Aspire.Hosting.Dapr.DaprSidecarOptions.SchedulerHostAddress.get -> string? +Aspire.Hosting.Dapr.DaprSidecarOptions.SchedulerHostAddress.init -> void From 40fb2e6e946609cf8f2fceac8d6ead5ed53d1f74 Mon Sep 17 00:00:00 2001 From: paule96 Date: Fri, 1 Nov 2024 07:56:48 +0100 Subject: [PATCH 22/44] Improve devcontainer setup (#6570) * bring the changes from the old PR to this branch * remove a warning that is not anymore true * Apply suggestions from code review --------- Co-authored-by: David Fowler --- .devcontainer/devcontainer.json | 20 ++++++++++---------- docs/machine-requirements.md | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index f4d1001aba..77909ce69a 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,27 +3,28 @@ { "name": "C# (.NET)", // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - "image": "mcr.microsoft.com/devcontainers/dotnet:1-9.0", + "image": "mcr.microsoft.com/devcontainers/dotnet:1-9.0-bookworm", "features": { "ghcr.io/devcontainers/features/azure-cli:1": {}, "ghcr.io/azure/azure-dev/azd:0": {}, - "ghcr.io/devcontainers/features/docker-in-docker": {}, + "ghcr.io/devcontainers/features/docker-in-docker:2": {}, + "ghcr.io/devcontainers/features/powershell:1": {}, "ghcr.io/devcontainers/features/dotnet": { "additionalVersions": [ "8.0.403" ] + }, + "ghcr.io/dapr/cli/dapr-cli": { + "version": "1.12.0" } }, - "hostRequirements": { "cpus": 8, "memory": "32gb", "storage": "64gb" }, - // Features to add to the dev container. More info: https://containers.dev/features. // "features": {}, - // Use 'forwardPorts' to make a list of ports inside the container available locally. "forwardPorts": [ 15887, @@ -68,13 +69,13 @@ "otherPortsAttributes": { "onAutoForward": "ignore" }, - // Use 'postCreateCommand' to run commands after the container is created. "customizations": { "vscode": { "extensions": [ - "ms-dotnettools.csdevkit", + "ms-dotnettools.vscodeintellicode-csharp", "ms-azuretools.vscode-bicep", + "EditorConfig.EditorConfig", "ms-azuretools.azure-dev" ], "settings": { @@ -84,11 +85,10 @@ } }, "onCreateCommand": "dotnet restore", + "postCreateCommand": "dapr init", "postStartCommand": "dotnet dev-certs https --trust" - // Configure tool-specific properties. // "customizations": {}, - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. // "remoteUser": "root" -} +} \ No newline at end of file diff --git a/docs/machine-requirements.md b/docs/machine-requirements.md index 0c05d6b4f0..996d72a8d4 100644 --- a/docs/machine-requirements.md +++ b/docs/machine-requirements.md @@ -26,3 +26,22 @@ When you install, ensure that both: * [Windows](https://podman.io/docs/installation#windows) * [macOS](https://podman.io/docs/installation#macos) * [Linux](https://podman.io/docs/installation#installing-on-linux) + +## (Windows / Linux / Mac) DevContainer in VS Code + +On Windows you could also use VS Code with the DevContainers extension. This requires that you have installed a container engine. +Currently it's only tested with Docker Desktop. + +> :warning: This will use around 16GB of RAM, after you loaded the solution. + +### Install VS Code with DevContainers Extension + +* [VS Code](https://code.visualstudio.com/Download) +* [DevContainers Extension](https://marketplace.visualstudio.com/items?itemName=ms-VS Code-remote.remote-containers) + +## (Browser) Codespaces + +Just start the Codespaces in your fork. The initialization of the code space takes around 5 mins. After that you can open the solution. +This will take on the free version of Codespace around 10 mins. + +> :warning: With the free version of Codespaces the development experience can be less than ideal. We recommend using at least a Codespace with 16GB of RAM or use your local VS Code / DevContainers instance. From 9d06327ad22bd77e6d3a74d2187c49561b7ee01e Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Fri, 1 Nov 2024 10:31:36 -0700 Subject: [PATCH 23/44] Fix feed for daily builds from main branch (#6472) * Fix feed for daily builds from main branch * Update using-latest-daily.md --- docs/using-latest-daily.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/using-latest-daily.md b/docs/using-latest-daily.md index f0d6bde983..6fa07c784b 100644 --- a/docs/using-latest-daily.md +++ b/docs/using-latest-daily.md @@ -18,16 +18,17 @@ dotnet new nugetconfig The latest builds are pushed to a special feed, which you need to add: ```sh -dotnet nuget add source --name dotnet8 https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet8/nuget/v3/index.json +dotnet nuget add source --name dotnet9 https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json ``` If you use [Package Source Mapping](https://learn.microsoft.com/en-us/nuget/consume-packages/package-source-mapping), you'll also need to add the following mappings to your NuGet.config ```xml - + + ``` From bcfa2dfbcbfc61d8ac34e88690eefce32e2a72cf Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Sun, 3 Nov 2024 03:25:48 +1100 Subject: [PATCH 24/44] Rewrite dashboard URL in the console. (#6591) * Rewrite dashboard URL in the console. * Include launch.json (and add Copilot extension). * Can't get serverReadyAction working. * Fix compiler error. * PR feedback. --- .devcontainer/devcontainer.json | 4 +- .gitignore | 3 - .vscode/launch.json | 14 ++++ .../CodespacesResourceUrlRewriterService.cs | 76 +++++++++++++++++++ .../Codespaces/CodespacesUrlRewriter.cs | 74 ++++-------------- .../Dashboard/DashboardLifecycleHook.cs | 14 +++- .../DistributedApplicationBuilder.cs | 3 +- .../Dashboard/DashboardLifecycleHookTests.cs | 12 ++- 8 files changed, 130 insertions(+), 70 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 src/Aspire.Hosting/Codespaces/CodespacesResourceUrlRewriterService.cs diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 77909ce69a..021ae50c2a 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -76,7 +76,9 @@ "ms-dotnettools.vscodeintellicode-csharp", "ms-azuretools.vscode-bicep", "EditorConfig.EditorConfig", - "ms-azuretools.azure-dev" + "ms-azuretools.azure-dev", + "GitHub.copilot", + "GitHub.copilot-chat" ], "settings": { "remote.autoForwardPorts": false, diff --git a/.gitignore b/.gitignore index 79d0b07c38..a4618b1543 100644 --- a/.gitignore +++ b/.gitignore @@ -116,9 +116,6 @@ Session.vim .netrwhist *~ -# Visual Studio Code -.vscode/ - # Private test configuration and binaries. config.ps1 **/IISApplications diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000..a97fe76634 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "request": "launch", + "name": "C#: WaitFor (Debug)", + "type": "dotnet", + "projectPath": "${workspaceFolder}/playground/waitfor/WaitForSandbox.AppHost/WaitForSandbox.AppHost.csproj" + } + ] +} diff --git a/src/Aspire.Hosting/Codespaces/CodespacesResourceUrlRewriterService.cs b/src/Aspire.Hosting/Codespaces/CodespacesResourceUrlRewriterService.cs new file mode 100644 index 0000000000..a1ddc926d9 --- /dev/null +++ b/src/Aspire.Hosting/Codespaces/CodespacesResourceUrlRewriterService.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using Aspire.Hosting.ApplicationModel; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Aspire.Hosting.Codespaces; + +internal sealed class CodespacesResourceUrlRewriterService(ILogger logger, IOptions options, CodespacesUrlRewriter codespaceUrlRewriter, ResourceNotificationService resourceNotificationService) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + if (!options.Value.IsCodespace) + { + logger.LogTrace("Not running in Codespaces, skipping URL rewriting."); + return; + } + + do + { + try + { + var resourceEvents = resourceNotificationService.WatchAsync(stoppingToken); + + await foreach (var resourceEvent in resourceEvents.ConfigureAwait(false)) + { + Dictionary? remappedUrls = null; + + foreach (var originalUrlSnapshot in resourceEvent.Snapshot.Urls) + { + var uri = new Uri(originalUrlSnapshot.Url); + + if (!originalUrlSnapshot.IsInternal && (uri.Scheme == "http" || uri.Scheme == "https") && uri.Host == "localhost") + { + remappedUrls ??= new(); + + var newUrlSnapshot = originalUrlSnapshot with + { + // The format of GitHub Codespaces URLs comprises the codespace + // name (from the CODESPACE_NAME environment variable, the port, + // and the port forwarding domain (via GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN + // which is typically ".app.github.dev". The VSCode instance is typically + // hosted at codespacename.github.dev whereas the forwarded ports + // would be at codespacename-port.app.github.dev. + Url = codespaceUrlRewriter.RewriteUrl(uri) + }; + + remappedUrls.Add(originalUrlSnapshot, newUrlSnapshot); + } + } + + if (remappedUrls is not null) + { + var transformedUrls = from originalUrl in resourceEvent.Snapshot.Urls + select remappedUrls.TryGetValue(originalUrl, out var remappedUrl) ? remappedUrl : originalUrl; + + await resourceNotificationService.PublishUpdateAsync(resourceEvent.Resource, resourceEvent.ResourceId, s => s with + { + Urls = transformedUrls.ToImmutableArray() + }).ConfigureAwait(false); + } + } + } + catch (Exception ex) when (!stoppingToken.IsCancellationRequested) + { + // When debugging sometimes we'll get cancelled here but we don't want + // to tear down the loop. We only want to crash out when the service's + // cancellation token is signaled. + logger.LogTrace(ex, "Codespace URL rewriting loop threw an exception but was ignored."); + } + } while (!stoppingToken.IsCancellationRequested); + } +} diff --git a/src/Aspire.Hosting/Codespaces/CodespacesUrlRewriter.cs b/src/Aspire.Hosting/Codespaces/CodespacesUrlRewriter.cs index 4d6a113c72..14ccb52935 100644 --- a/src/Aspire.Hosting/Codespaces/CodespacesUrlRewriter.cs +++ b/src/Aspire.Hosting/Codespaces/CodespacesUrlRewriter.cs @@ -1,76 +1,32 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Immutable; -using Aspire.Hosting.ApplicationModel; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace Aspire.Hosting.Codespaces; -internal sealed class CodespacesUrlRewriter(ILogger logger, IOptions options, ResourceNotificationService resourceNotificationService) : BackgroundService +internal sealed class CodespacesUrlRewriter(IOptions options) { - protected override async Task ExecuteAsync(CancellationToken stoppingToken) + public string RewriteUrl(string url) { + ArgumentNullException.ThrowIfNullOrWhiteSpace(url); + if (!options.Value.IsCodespace) { - logger.LogTrace("Not running in Codespaces, skipping URL rewriting."); - return; + return url; } - do - { - try - { - var resourceEvents = resourceNotificationService.WatchAsync(stoppingToken); - - await foreach (var resourceEvent in resourceEvents.ConfigureAwait(false)) - { - Dictionary? remappedUrls = null; - - foreach (var originalUrlSnapshot in resourceEvent.Snapshot.Urls) - { - var uri = new Uri(originalUrlSnapshot.Url); - - if (!originalUrlSnapshot.IsInternal && (uri.Scheme == "http" || uri.Scheme == "https") && uri.Host == "localhost") - { - remappedUrls ??= new(); - - var newUrlSnapshot = originalUrlSnapshot with - { - // The format of GitHub Codespaces URLs comprises the codespace - // name (from the CODESPACE_NAME environment variable, the port, - // and the port forwarding domain (via GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN - // which is typically ".app.github.dev". The VSCode instance is typically - // hosted at codespacename.github.dev whereas the forwarded ports - // would be at codespacename-port.app.github.dev. - Url = $"{uri.Scheme}://{options.Value.CodespaceName}-{uri.Port}.{options.Value.PortForwardingDomain}{uri.AbsolutePath}" - }; - - remappedUrls.Add(originalUrlSnapshot, newUrlSnapshot); - } - } + return RewriteUrl(new Uri(url, UriKind.Absolute)); + } - if (remappedUrls is not null) - { - var transformedUrls = from originalUrl in resourceEvent.Snapshot.Urls - select remappedUrls.TryGetValue(originalUrl, out var remappedUrl) ? remappedUrl : originalUrl; + public string RewriteUrl(Uri uri) + { + if (!options.Value.IsCodespace) + { + return uri.ToString(); + } - await resourceNotificationService.PublishUpdateAsync(resourceEvent.Resource, resourceEvent.ResourceId, s => s with - { - Urls = transformedUrls.ToImmutableArray() - }).ConfigureAwait(false); - } - } - } - catch (Exception ex) when (!stoppingToken.IsCancellationRequested) - { - // When debugging sometimes we'll get cancelled here but we don't want - // to tear down the loop. We only want to crash out when the service's - // cancellation token is signaled. - logger.LogTrace(ex, "Codespace URL rewriting loop threw an exception but was ignored."); - } - } while (!stoppingToken.IsCancellationRequested); + var codespacesUrl = $"{uri.Scheme}://{options.Value.CodespaceName}-{uri.Port}.{options.Value.PortForwardingDomain}{uri.AbsolutePath}"; + return codespacesUrl; } } diff --git a/src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs b/src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs index 69fa9f2b7a..a67e6af414 100644 --- a/src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs +++ b/src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs @@ -9,6 +9,7 @@ using Aspire.Dashboard.ConsoleLogs; using Aspire.Dashboard.Model; using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Codespaces; using Aspire.Hosting.Dcp; using Aspire.Hosting.Lifecycle; using Aspire.Hosting.Utils; @@ -29,7 +30,8 @@ internal sealed class DashboardLifecycleHook(IConfiguration configuration, ResourceLoggerService resourceLoggerService, ILoggerFactory loggerFactory, DcpNameGenerator nameGenerator, - IHostApplicationLifetime hostApplicationLifetime) : IDistributedApplicationLifecycleHook, IAsyncDisposable + IHostApplicationLifetime hostApplicationLifetime, + CodespacesUrlRewriter codespaceUrlRewriter) : IDistributedApplicationLifecycleHook, IAsyncDisposable { private Task? _dashboardLogsTask; private CancellationTokenSource? _dashboardLogsCts; @@ -235,14 +237,18 @@ private void ConfigureAspireDashboardResource(IResource dashboardResource) // We need to print out the url so that dotnet watch can launch the dashboard // technically this is too early, but it's late ne - if (StringUtils.TryGetUriFromDelimitedString(dashboardUrls, ";", out var firstDashboardUrl)) + if (!StringUtils.TryGetUriFromDelimitedString(dashboardUrls, ";", out var firstDashboardUrl)) { - distributedApplicationLogger.LogInformation("Now listening on: {DashboardUrl}", firstDashboardUrl.ToString().TrimEnd('/')); + return; } + var dashboardUrl = codespaceUrlRewriter.RewriteUrl(firstDashboardUrl.ToString()); + + distributedApplicationLogger.LogInformation("Now listening on: {DashboardUrl}", dashboardUrl.TrimEnd('/')); + if (!string.IsNullOrEmpty(browserToken)) { - LoggingHelpers.WriteDashboardUrl(distributedApplicationLogger, dashboardUrls, browserToken, isContainer: false); + LoggingHelpers.WriteDashboardUrl(distributedApplicationLogger, dashboardUrl, browserToken, isContainer: false); } })); } diff --git a/src/Aspire.Hosting/DistributedApplicationBuilder.cs b/src/Aspire.Hosting/DistributedApplicationBuilder.cs index be5d170963..def5cd589b 100644 --- a/src/Aspire.Hosting/DistributedApplicationBuilder.cs +++ b/src/Aspire.Hosting/DistributedApplicationBuilder.cs @@ -288,7 +288,8 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options) // Codespaces _innerBuilder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, ConfigureCodespacesOptions>()); - _innerBuilder.Services.AddHostedService(); + _innerBuilder.Services.AddSingleton(); + _innerBuilder.Services.AddHostedService(); Eventing.Subscribe(BuiltInDistributedApplicationEventSubscriptionHandlers.InitializeDcpAnnotations); } diff --git a/tests/Aspire.Hosting.Tests/Dashboard/DashboardLifecycleHookTests.cs b/tests/Aspire.Hosting.Tests/Dashboard/DashboardLifecycleHookTests.cs index b66eedde69..5e5351ec4d 100644 --- a/tests/Aspire.Hosting.Tests/Dashboard/DashboardLifecycleHookTests.cs +++ b/tests/Aspire.Hosting.Tests/Dashboard/DashboardLifecycleHookTests.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Text.Json; using System.Threading.Channels; +using Aspire.Hosting.Codespaces; using Aspire.Hosting.ConsoleLogs; using Aspire.Hosting.Dashboard; using Aspire.Hosting.Dcp; @@ -99,8 +100,13 @@ private static DashboardLifecycleHook CreateHook( ResourceLoggerService resourceLoggerService, ResourceNotificationService resourceNotificationService, IConfiguration configuration, - ILoggerFactory? loggerFactory = null) + ILoggerFactory? loggerFactory = null, + IOptions? codespacesOptions = null + ) { + codespacesOptions ??= Options.Create(new CodespacesOptions()); + var rewriter = new CodespacesUrlRewriter(codespacesOptions); + return new DashboardLifecycleHook( configuration, Options.Create(new DashboardOptions { DashboardPath = "test.dll" }), @@ -111,7 +117,9 @@ private static DashboardLifecycleHook CreateHook( resourceLoggerService, loggerFactory ?? NullLoggerFactory.Instance, new DcpNameGenerator(configuration, Options.Create(new DcpOptions())), - new TestHostApplicationLifetime()); + new TestHostApplicationLifetime(), + rewriter + ); } public static IEnumerable Data() From d9f056e334eb8bf1928af382dfbdd69e4b2ccbd7 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sun, 3 Nov 2024 21:38:55 +0800 Subject: [PATCH 25/44] Improve handling empty values in GridValue (#6554) --- .../Components/Controls/GridValue.razor | 32 +++++++++++-------- .../Components/Controls/PropertyGrid.razor | 2 +- .../Components/Controls/PropertyGrid.razor.cs | 5 +++ 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/Aspire.Dashboard/Components/Controls/GridValue.razor b/src/Aspire.Dashboard/Components/Controls/GridValue.razor index 0627f6153d..e90ae7f434 100644 --- a/src/Aspire.Dashboard/Components/Controls/GridValue.razor +++ b/src/Aspire.Dashboard/Components/Controls/GridValue.razor @@ -15,16 +15,23 @@ else { - @ContentBeforeValue - @if (EnableHighlighting && !string.IsNullOrEmpty(HighlightText)) + @if (ContentBeforeValue == null && ContentAfterValue == null && string.IsNullOrEmpty(Value)) { - + } - else if (_formattedValue != null) + else { - @((MarkupString)_formattedValue) - } - @ContentAfterValue + @ContentBeforeValue + if (EnableHighlighting && !string.IsNullOrEmpty(HighlightText)) + { + + } + else if (_formattedValue != null) + { + @((MarkupString)_formattedValue) + } + @ContentAfterValue + } } @@ -49,18 +56,17 @@ - + @PreCopyToolTip - + diff --git a/src/Aspire.Dashboard/Components/Controls/PropertyGrid.razor b/src/Aspire.Dashboard/Components/Controls/PropertyGrid.razor index 2630925740..8a23561b2c 100644 --- a/src/Aspire.Dashboard/Components/Controls/PropertyGrid.razor +++ b/src/Aspire.Dashboard/Components/Controls/PropertyGrid.razor @@ -23,7 +23,7 @@ where TItem : IPropertyGridItem [Parameter] public GenerateHeaderOption GenerateHeader { get; set; } = GenerateHeaderOption.Sticky; + // Return null if empty so GridValue knows there is no template. + private RenderFragment? GetContentAfterValue(TItem context) => ContentAfterValue == s_emptyChildContent + ? null + : ContentAfterValue(context); + private async Task OnIsValueMaskedChanged(TItem item, bool isValueMasked) { item.IsValueMasked = isValueMasked; From 0836cf6896201de583121c95bcb597bd415c9546 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sun, 3 Nov 2024 21:39:29 +0800 Subject: [PATCH 26/44] Change URL detection regex to exclude trailing content (#6572) --- .../Stress.ApiService/ConsoleStresser.cs | 5 ++++ src/Aspire.Dashboard/ConsoleLogs/UrlParser.cs | 17 ++++--------- .../ConsoleLogsTests/UrlParserTests.cs | 24 ++++++++++++++++++- 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/playground/Stress/Stress.ApiService/ConsoleStresser.cs b/playground/Stress/Stress.ApiService/ConsoleStresser.cs index 03ab3e8a48..a084050f66 100644 --- a/playground/Stress/Stress.ApiService/ConsoleStresser.cs +++ b/playground/Stress/Stress.ApiService/ConsoleStresser.cs @@ -78,6 +78,11 @@ public static void Stress() Console.WriteLine("https://www.example.com/path/with/percent%25encoded"); Console.WriteLine("https://www.example.com/path/with/dollar$sign"); Console.WriteLine("https://www.example.com/path/with/exclamation!mark"); + Console.WriteLine("https://www.example.com/;path/"); + Console.WriteLine("https://www.example.com/path/?query;string"); + Console.WriteLine("https://;www.example.com/"); + Console.WriteLine("https://www;.example.com/"); + Console.WriteLine("https://www.exa;mple.com/"); Console.Write("\x1b[0m"); // reset color diff --git a/src/Aspire.Dashboard/ConsoleLogs/UrlParser.cs b/src/Aspire.Dashboard/ConsoleLogs/UrlParser.cs index 0b19a200ba..e5e14f5d2f 100644 --- a/src/Aspire.Dashboard/ConsoleLogs/UrlParser.cs +++ b/src/Aspire.Dashboard/ConsoleLogs/UrlParser.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Net; using System.Text; using System.Text.RegularExpressions; @@ -34,7 +35,7 @@ public static bool TryParse(string? text, Func? nonMatchFragment nextCharIndex = urlMatch.Index + urlMatch.Length; var url = text[urlStart..nextCharIndex]; - builder.Append(CultureInfo.InvariantCulture, $"{url}"); + builder.Append(CultureInfo.InvariantCulture, $"{WebUtility.HtmlEncode(url)}"); urlMatch = urlMatch.NextMatch(); } @@ -65,17 +66,9 @@ static void AppendNonMatchFragment(StringBuilder stringBuilder, Funchttp://bing.com/")] [InlineData("http://bing.com/dir", "http://bing.com/dir")] [InlineData("http://bing.com/index.aspx", "http://bing.com/index.aspx")] - [InlineData("http://bing", "http://bing")] + [InlineData("http://localhost", "http://localhost")] public void TryParse_SupportedUrlFormats(string input, string? expectedOutput) { var result = UrlParser.TryParse(input, WebUtility.HtmlEncode, out var modifiedText); @@ -71,6 +71,15 @@ public void TryParse_ExcludeInvalidTrailingChars(string input, string? expectedO Assert.Equal(expectedOutput, modifiedText); } + [Fact] + public void TryParse_QueryString() + { + var result = UrlParser.TryParse("https://www.example.com?query=string¶m=value", WebUtility.HtmlEncode, out var modifiedText); + Assert.True(result); + + Assert.Equal("https://www.example.com?query=string&param=value", modifiedText); + } + [Theory] [InlineData("http://www.localhost:8080")] [InlineData("HTTP://WWW.LOCALHOST:8080")] @@ -83,4 +92,17 @@ public void GenerateUrlRegEx_MatchUrlAfterContent(string content) var match = regex.Match(content); Assert.Equal("http://www.localhost:8080", match.Value.ToLowerInvariant()); } + + [Theory] + [InlineData("http://www.localhost:8080!", "http://www.localhost:8080!")] + [InlineData("http://www.localhost:8080/path!", "http://www.localhost:8080/path!")] + [InlineData("http://www.localhost:8080/path;", "http://www.localhost:8080/path")] + [InlineData("http://www.localhost:8080;", "http://www.localhost:8080")] + [InlineData("http://www.local;host:8080;", "http://www.local")] + public void GenerateUrlRegEx_MatchUrlBeforeContent(string content, string expected) + { + var regex = UrlParser.GenerateUrlRegEx(); + var match = regex.Match(content); + Assert.Equal(expected, match.Value.ToLowerInvariant()); + } } From ba893fa61a371f2d2a5d4b790a747f1b226ead3d Mon Sep 17 00:00:00 2001 From: ChinoUkaegbu <77782533+ChinoUkaegbu@users.noreply.github.com> Date: Mon, 4 Nov 2024 17:03:26 +0100 Subject: [PATCH 27/44] fix typo in WithEndpoint doc (#6602) --- src/Aspire.Hosting/ResourceBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index 4c6e44572f..288e01adb4 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -455,7 +455,7 @@ private static void ApplyEndpoints(this IResourceBuilder builder, IResourc } /// - /// Changes an existing creates a new endpoint if it doesn't exist and invokes callback to modify the defaults. + /// Changes an existing endpoint or creates a new endpoint if it doesn't exist and invokes callback to modify the defaults. /// /// Resource builder for resource with endpoints. /// Name of endpoint to change. From 11c51e7a34d068eb1881ee69e2dd3668511718d2 Mon Sep 17 00:00:00 2001 From: Alireza Baloochi Date: Tue, 5 Nov 2024 00:30:54 +0330 Subject: [PATCH 28/44] Add WithDataVolume to Redis Insights (#6432) ## Description This pull request introduces new features to the `Aspire.Hosting.Redis` project, including methods for adding data volumes and bind mounts to Redis Insight resources, along with corresponding tests to ensure data persistence between usages. The most important changes are detailed below: ### New Features: * Added `WithDataVolume` and `WithDataBindMount` methods to `RedisBuilderExtensions` to support adding named volumes and bind mounts for data folders in Redis Insight container resources. ### Testing Enhancements: * Introduced a new test `RedisInsightWithDataShouldPersistStateBetweenUsages` in `RedisFunctionalTests` to verify that Redis Insight data persists between container restarts, using either volumes or bind mounts. Fixes #6299 --- .../PublicAPI.Unshipped.txt | 2 + .../RedisBuilderExtensions.cs | 28 +++ .../RedisFunctionalTests.cs | 166 ++++++++++++++++++ 3 files changed, 196 insertions(+) diff --git a/src/Aspire.Hosting.Redis/PublicAPI.Unshipped.txt b/src/Aspire.Hosting.Redis/PublicAPI.Unshipped.txt index adfcd6bf0f..f716f33240 100644 --- a/src/Aspire.Hosting.Redis/PublicAPI.Unshipped.txt +++ b/src/Aspire.Hosting.Redis/PublicAPI.Unshipped.txt @@ -2,5 +2,7 @@ Aspire.Hosting.Redis.RedisInsightResource Aspire.Hosting.Redis.RedisInsightResource.PrimaryEndpoint.get -> Aspire.Hosting.ApplicationModel.EndpointReference! Aspire.Hosting.Redis.RedisInsightResource.RedisInsightResource(string! name) -> void +static Aspire.Hosting.RedisBuilderExtensions.WithDataBindMount(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! source) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.RedisBuilderExtensions.WithDataVolume(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string? name = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.RedisBuilderExtensions.WithHostPort(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, int? port) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.RedisBuilderExtensions.WithRedisInsight(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, System.Action!>? configureContainer = null, string? containerName = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! diff --git a/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs b/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs index f3e5ff521d..7f7d193200 100644 --- a/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs +++ b/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs @@ -438,4 +438,32 @@ public static IResourceBuilder WithPersistence(this IResourceBuil return Task.CompletedTask; }), ResourceAnnotationMutationBehavior.Replace); } + + /// + /// Adds a named volume for the data folder to a Redis Insight container resource. + /// + /// The resource builder. + /// The name of the volume. Defaults to an auto-generated name based on the application and resource names. + /// The . + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Each overload targets a different resource builder type, allowing for tailored functionality. Optional volume names enhance usability, enabling users to easily provide custom names while maintaining clear and distinct method signatures.")] + public static IResourceBuilder WithDataVolume(this IResourceBuilder builder, string? name = null) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), "/data"); + } + + /// + /// Adds a bind mount for the data folder to a Redis Insight container resource. + /// + /// The resource builder. + /// The source directory on the host to mount into the container. + /// The . + public static IResourceBuilder WithDataBindMount(this IResourceBuilder builder, string source) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(source); + + return builder.WithBindMount(source, "/data"); + } } diff --git a/tests/Aspire.Hosting.Redis.Tests/RedisFunctionalTests.cs b/tests/Aspire.Hosting.Redis.Tests/RedisFunctionalTests.cs index e04687f238..5958ceef30 100644 --- a/tests/Aspire.Hosting.Redis.Tests/RedisFunctionalTests.cs +++ b/tests/Aspire.Hosting.Redis.Tests/RedisFunctionalTests.cs @@ -16,6 +16,7 @@ using Xunit; using Xunit.Abstractions; using Aspire.Hosting.Tests.Dcp; +using System.Text.Json.Nodes; namespace Aspire.Hosting.Redis.Tests; @@ -534,6 +535,171 @@ public async Task PersistenceIsDisabledByDefault() } } + [Theory] + [InlineData(false)] + [InlineData(true)] + [RequiresDocker] + public async Task RedisInsightWithDataShouldPersistStateBetweenUsages(bool useVolume) + { + var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10)); + + string? volumeName = null; + string? bindMountPath = null; + + try + { + using var builder1 = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(testOutputHelper); + IResourceBuilder? redisInsightBuilder1 = null; + var redis1 = builder1.AddRedis("redis") + .WithRedisInsight(c => { redisInsightBuilder1 = c; }); + Assert.NotNull(redisInsightBuilder1); + + if (useVolume) + { + // Use a deterministic volume name to prevent them from exhausting the machines if deletion fails + volumeName = VolumeNameGenerator.Generate(redisInsightBuilder1, nameof(RedisInsightWithDataShouldPersistStateBetweenUsages)); + + // if the volume already exists (because of a crashing previous run), delete it + DockerUtils.AttemptDeleteDockerVolume(volumeName, throwOnFailure: true); + redisInsightBuilder1.WithDataVolume(volumeName); + } + else + { + bindMountPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + redisInsightBuilder1.WithDataBindMount(bindMountPath); + } + + using (var app = builder1.Build()) + { + 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); + + try + { + var httpClient = app.CreateHttpClient(redisInsightBuilder1.Resource.Name, "http"); + await AcceptRedisInsightEula(httpClient, cts.Token); + } + finally + { + // Stops the container, or the Volume would still be in use + await app.StopAsync(); + } + } + + using var builder2 = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(testOutputHelper); + + IResourceBuilder? redisInsightBuilder2 = null; + var redis2 = builder2.AddRedis("redis") + .WithRedisInsight(c => { redisInsightBuilder2 = c; }); + Assert.NotNull(redisInsightBuilder2); + + if (useVolume) + { + redisInsightBuilder2.WithDataVolume(volumeName); + } + else + { + redisInsightBuilder2.WithDataBindMount(bindMountPath!); + } + + using (var app = builder2.Build()) + { + 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); + + try + { + var httpClient = app.CreateHttpClient(redisInsightBuilder2.Resource.Name, "http"); + await EnsureRedisInsightEulaAccepted(httpClient, cts.Token); + } + finally + { + // Stops the container, or the Volume would still be in use + await app.StopAsync(); + } + } + } + finally + { + if (volumeName is not null) + { + DockerUtils.AttemptDeleteDockerVolume(volumeName); + } + + if (bindMountPath is not null) + { + try + { + Directory.Delete(bindMountPath, recursive: true); + } + catch + { + // Don't fail test if we can't clean the temporary folder + } + } + } + } + + private static async Task EnsureRedisInsightEulaAccepted(HttpClient httpClient, CancellationToken ct) + { + var response = await httpClient.GetAsync("/api/settings", ct); + response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadAsStringAsync(ct); + + var jo = JsonObject.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()); + } + + static async Task AcceptRedisInsightEula(HttpClient client, CancellationToken ct) + { + var jsonContent = JsonContent.Create(new + { + agreements = new + { + eula = true, + analytics = false, + notifications = false, + encryption = false, + } + }); + + var apiUrl = $"/api/settings"; + + var response = await client.PatchAsync(apiUrl, jsonContent, ct); + + response.EnsureSuccessStatusCode(); + + await EnsureRedisInsightEulaAccepted(client, ct); + } + internal sealed class RedisInsightDatabaseModel { public string? Id { get; set; } From 98d51ca4ac05cdb16aedfdaa3c165e6ecbf4e760 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Tue, 5 Nov 2024 15:37:41 +1100 Subject: [PATCH 29/44] Remove Dapr init for stabillity reasons (#6608) --- .devcontainer/devcontainer.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 021ae50c2a..2166b0b0dd 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -87,10 +87,9 @@ } }, "onCreateCommand": "dotnet restore", - "postCreateCommand": "dapr init", "postStartCommand": "dotnet dev-certs https --trust" // Configure tool-specific properties. // "customizations": {}, // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. // "remoteUser": "root" -} \ No newline at end of file +} From 3a35141c2fa5e8441e10a4aa235fa93d27b8c8d9 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Tue, 5 Nov 2024 13:05:40 -0800 Subject: [PATCH 30/44] Update dependencies from https://github.com/microsoft/usvc-apiserver build 0.9.0 (#6612) Microsoft.DeveloperControlPlane.darwin-amd64 , Microsoft.DeveloperControlPlane.darwin-arm64 , Microsoft.DeveloperControlPlane.linux-amd64 , Microsoft.DeveloperControlPlane.linux-arm64 , Microsoft.DeveloperControlPlane.windows-386 , Microsoft.DeveloperControlPlane.windows-amd64 , Microsoft.DeveloperControlPlane.windows-arm64 From Version 0.8.13 -> To Version 0.9.0 Co-authored-by: dotnet-maestro[bot] --- eng/Version.Details.xml | 28 ++++++++++++++-------------- eng/Versions.props | 14 +++++++------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index b469d18fd0..3341f1f4e0 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,33 +1,33 @@ - + https://github.com/microsoft/usvc-apiserver - 9582c787e7963a2acf11111e5bc5a4938e7fe3ab + f9530dc2f2c52fac27d9c525f142d5d663500158 - + https://github.com/microsoft/usvc-apiserver - 9582c787e7963a2acf11111e5bc5a4938e7fe3ab + f9530dc2f2c52fac27d9c525f142d5d663500158 - + https://github.com/microsoft/usvc-apiserver - 9582c787e7963a2acf11111e5bc5a4938e7fe3ab + f9530dc2f2c52fac27d9c525f142d5d663500158 - + https://github.com/microsoft/usvc-apiserver - 9582c787e7963a2acf11111e5bc5a4938e7fe3ab + f9530dc2f2c52fac27d9c525f142d5d663500158 - + https://github.com/microsoft/usvc-apiserver - 9582c787e7963a2acf11111e5bc5a4938e7fe3ab + f9530dc2f2c52fac27d9c525f142d5d663500158 - + https://github.com/microsoft/usvc-apiserver - 9582c787e7963a2acf11111e5bc5a4938e7fe3ab + f9530dc2f2c52fac27d9c525f142d5d663500158 - + https://github.com/microsoft/usvc-apiserver - 9582c787e7963a2acf11111e5bc5a4938e7fe3ab + f9530dc2f2c52fac27d9c525f142d5d663500158 https://github.com/dotnet/extensions diff --git a/eng/Versions.props b/eng/Versions.props index 9985929f8e..b45945db7f 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -27,13 +27,13 @@ 8.0.100-rtm.23512.16 - 0.8.13 - 0.8.13 - 0.8.13 - 0.8.13 - 0.8.13 - 0.8.13 - 0.8.13 + 0.9.0 + 0.9.0 + 0.9.0 + 0.9.0 + 0.9.0 + 0.9.0 + 0.9.0 9.0.0-beta.24516.2 9.0.0-beta.24516.2 From 68436e703a0727d45a8fef2eade72a5da798a0ff Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Tue, 5 Nov 2024 20:31:23 -0600 Subject: [PATCH 31/44] Remove Swashbuckle.AspNetCore (#6614) * Remove Swashbuckle.AspNetCore These apps aren't really using the Swagger UI and this resolves an internal component governance warning. * Remove Microsoft.AspNetCore.OpenAPI from Directory.Packages.props since it is now unused. --- Directory.Packages.props | 2 -- eng/Version.Details.xml | 4 ---- eng/Versions.props | 1 - .../TestShop/CatalogService/CatalogService.csproj | 2 -- playground/TestShop/CatalogService/Program.cs | 12 +----------- playground/dapr/ServiceA/DaprServiceA.csproj | 4 +--- playground/dapr/ServiceA/Program.cs | 13 +------------ playground/dapr/ServiceB/DaprServiceB.csproj | 2 -- playground/dapr/ServiceB/Program.cs | 12 +----------- .../mysql/MySql.ApiService/MySql.ApiService.csproj | 1 - playground/mysql/MySql.ApiService/Program.cs | 9 --------- .../nats/Nats.ApiService/Nats.ApiService.csproj | 4 ---- playground/nats/Nats.ApiService/Program.cs | 10 ---------- .../TestingAppHost1.MyWebApp/Program.cs | 11 +---------- .../TestingAppHost1.MyWebApp.csproj | 5 ----- 15 files changed, 5 insertions(+), 87 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index c0a687d553..7c3f9e217e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -63,7 +63,6 @@ - @@ -165,7 +164,6 @@ - diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 3341f1f4e0..7bc7189aa6 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -81,10 +81,6 @@ https://github.com/dotnet/runtime b83186a969946f61c71ad0da034936fac9947752 - - https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - c2a442982e736e17ae6bcadbfd8ccba278ee1be6 - https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore c2a442982e736e17ae6bcadbfd8ccba278ee1be6 diff --git a/eng/Versions.props b/eng/Versions.props index b45945db7f..cfe0127b1b 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -53,7 +53,6 @@ 8.0.10 8.0.10 8.0.10 - 8.0.10 8.0.10 8.0.10 8.0.10 diff --git a/playground/TestShop/CatalogService/CatalogService.csproj b/playground/TestShop/CatalogService/CatalogService.csproj index 84675d79bb..544e4e8b05 100644 --- a/playground/TestShop/CatalogService/CatalogService.csproj +++ b/playground/TestShop/CatalogService/CatalogService.csproj @@ -14,8 +14,6 @@ - - diff --git a/playground/TestShop/CatalogService/Program.cs b/playground/TestShop/CatalogService/Program.cs index a6e98091b4..8d2edffc0f 100644 --- a/playground/TestShop/CatalogService/Program.cs +++ b/playground/TestShop/CatalogService/Program.cs @@ -6,21 +6,11 @@ builder.AddServiceDefaults(); builder.AddNpgsqlDbContext("catalogdb"); -builder.Services.AddEndpointsApiExplorer(); builder.Services.AddProblemDetails(); -builder.Services.AddSwaggerGen(); var app = builder.Build(); -if (app.Environment.IsDevelopment()) -{ - app.UseSwagger(); - app.UseSwaggerUI(); -} -else -{ - app.UseExceptionHandler(); -} +app.UseExceptionHandler(); app.MapCatalogApi(); app.MapDefaultEndpoints(); diff --git a/playground/dapr/ServiceA/DaprServiceA.csproj b/playground/dapr/ServiceA/DaprServiceA.csproj index 32dd62bcae..790366ee1f 100644 --- a/playground/dapr/ServiceA/DaprServiceA.csproj +++ b/playground/dapr/ServiceA/DaprServiceA.csproj @@ -1,4 +1,4 @@ - + $(DefaultTargetFramework) @@ -9,8 +9,6 @@ - - diff --git a/playground/dapr/ServiceA/Program.cs b/playground/dapr/ServiceA/Program.cs index 8b3e929443..65c8cce2a3 100644 --- a/playground/dapr/ServiceA/Program.cs +++ b/playground/dapr/ServiceA/Program.cs @@ -9,21 +9,11 @@ builder.AddServiceDefaults(); // Add services to the container. -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); - builder.Services.AddDaprClient(); var app = builder.Build(); // Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) -{ - app.UseSwagger(); - app.UseSwaggerUI(); -} - app.UseCloudEvents(); app.MapSubscribeHandler(); @@ -42,8 +32,7 @@ return forecasts; }) -.WithName("GetWeatherForecast") -.WithOpenApi(); +.WithName("GetWeatherForecast"); app.MapPost("/subscriptions/weather", [Topic("pubsub", "weather")] (ILogger logger, WeatherForecastMessage message) => { diff --git a/playground/dapr/ServiceB/DaprServiceB.csproj b/playground/dapr/ServiceB/DaprServiceB.csproj index 32dd62bcae..4f4415e450 100644 --- a/playground/dapr/ServiceB/DaprServiceB.csproj +++ b/playground/dapr/ServiceB/DaprServiceB.csproj @@ -9,8 +9,6 @@ - - diff --git a/playground/dapr/ServiceB/Program.cs b/playground/dapr/ServiceB/Program.cs index 90d696d450..06f3f399be 100644 --- a/playground/dapr/ServiceB/Program.cs +++ b/playground/dapr/ServiceB/Program.cs @@ -8,20 +8,11 @@ builder.AddServiceDefaults(); // Add services to the container. -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); - builder.Services.AddDaprClient(); var app = builder.Build(); // Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) -{ - app.UseSwagger(); - app.UseSwaggerUI(); -} var summaries = new[] { @@ -42,8 +33,7 @@ .ToArray(); return forecast; }) -.WithName("GetWeatherForecast") -.WithOpenApi(); +.WithName("GetWeatherForecast"); app.MapDefaultEndpoints(); diff --git a/playground/mysql/MySql.ApiService/MySql.ApiService.csproj b/playground/mysql/MySql.ApiService/MySql.ApiService.csproj index de9b36245c..06d6111470 100644 --- a/playground/mysql/MySql.ApiService/MySql.ApiService.csproj +++ b/playground/mysql/MySql.ApiService/MySql.ApiService.csproj @@ -10,7 +10,6 @@ - diff --git a/playground/mysql/MySql.ApiService/Program.cs b/playground/mysql/MySql.ApiService/Program.cs index 76493b2eb9..e5ffd06539 100644 --- a/playground/mysql/MySql.ApiService/Program.cs +++ b/playground/mysql/MySql.ApiService/Program.cs @@ -7,9 +7,6 @@ var builder = WebApplication.CreateBuilder(args); // Add services to the container. -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); builder.AddServiceDefaults(); @@ -19,12 +16,6 @@ var app = builder.Build(); // Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) -{ - app.UseSwagger(); - app.UseSwaggerUI(); -} - app.MapDefaultEndpoints(); app.MapGet("/catalog", async (MySqlConnection db) => { diff --git a/playground/nats/Nats.ApiService/Nats.ApiService.csproj b/playground/nats/Nats.ApiService/Nats.ApiService.csproj index 775a48f402..e0497905e6 100644 --- a/playground/nats/Nats.ApiService/Nats.ApiService.csproj +++ b/playground/nats/Nats.ApiService/Nats.ApiService.csproj @@ -7,10 +7,6 @@ true - - - - diff --git a/playground/nats/Nats.ApiService/Program.cs b/playground/nats/Nats.ApiService/Program.cs index 503be50065..97c214b38a 100644 --- a/playground/nats/Nats.ApiService/Program.cs +++ b/playground/nats/Nats.ApiService/Program.cs @@ -6,10 +6,6 @@ var builder = WebApplication.CreateBuilder(args); // Add services to the container. -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); - builder.AddServiceDefaults(); builder.AddNatsClient("nats", configureOptions: opts => @@ -23,12 +19,6 @@ var app = builder.Build(); // Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) -{ - app.UseSwagger(); - app.UseSwaggerUI(); -} - app.MapDefaultEndpoints(); app.MapGet("/ping", async (INatsConnection nats) => { diff --git a/tests/TestingAppHost1/TestingAppHost1.MyWebApp/Program.cs b/tests/TestingAppHost1/TestingAppHost1.MyWebApp/Program.cs index bb4aa11f4b..6b6d8ee9a8 100644 --- a/tests/TestingAppHost1/TestingAppHost1.MyWebApp/Program.cs +++ b/tests/TestingAppHost1/TestingAppHost1.MyWebApp/Program.cs @@ -6,20 +6,12 @@ builder.AddServiceDefaults(); // Add services to the container. -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); var app = builder.Build(); app.MapDefaultEndpoints(); // Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) -{ - app.UseSwagger(); - app.UseSwaggerUI(); -} app.UseHttpsRedirection(); @@ -40,8 +32,7 @@ .ToArray(); return forecast; }) -.WithName("GetWeatherForecast") -.WithOpenApi(); +.WithName("GetWeatherForecast"); app.Run(); diff --git a/tests/TestingAppHost1/TestingAppHost1.MyWebApp/TestingAppHost1.MyWebApp.csproj b/tests/TestingAppHost1/TestingAppHost1.MyWebApp/TestingAppHost1.MyWebApp.csproj index 9266eddb45..c6392c97f2 100644 --- a/tests/TestingAppHost1/TestingAppHost1.MyWebApp/TestingAppHost1.MyWebApp.csproj +++ b/tests/TestingAppHost1/TestingAppHost1.MyWebApp/TestingAppHost1.MyWebApp.csproj @@ -6,11 +6,6 @@ enable - - - - - From 1f45eda5ed2285ff275941b9c5ffdb04e9cbc31c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Nov 2024 04:01:10 +0000 Subject: [PATCH 32/44] Bump Microsoft.Playwright from 1.47.0 to 1.48.0 (#6528) Bumps [Microsoft.Playwright](https://github.com/microsoft/playwright-dotnet) from 1.47.0 to 1.48.0. - [Release notes](https://github.com/microsoft/playwright-dotnet/releases) - [Commits](https://github.com/microsoft/playwright-dotnet/compare/v1.47.0...v1.48.0) --- updated-dependencies: - dependency-name: Microsoft.Playwright dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 7c3f9e217e..b39ffe5440 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -163,7 +163,7 @@ - + From ff671b12b0b09e3ba2231b2aa55656ee6893d530 Mon Sep 17 00:00:00 2001 From: Alireza Baloochi Date: Thu, 7 Nov 2024 01:38:47 +0330 Subject: [PATCH 33/44] Add health check support for Qdrant client (#6057) This PR Adds health check support for Aspire.Qdrant.Client. We need to import Testcontainers for ConformanceTests. Fix #5768 * Add health check support for Qdrant client * Try to use qdrant-client health check * Use Grpc connection string for Grpc client * Apply suggestions from code review --------- Co-authored-by: Eric Erhardt --- Directory.Packages.props | 1 + .../Aspire.Hosting.Qdrant.csproj | 4 + .../QdrantBuilderExtensions.cs | 20 +-- .../Aspire.Qdrant.Client.csproj | 1 + .../AspireQdrantExtensions.cs | 17 +++ .../ConfigurationSchema.json | 10 ++ .../PublicAPI.Unshipped.txt | 5 +- .../QdrantClientSettings.cs | 16 +++ .../Aspire.Qdrant.Client/QdrantHealthCheck.cs | 9 +- .../Aspire.Qdrant.Client.Tests.csproj | 1 + .../AspireQdrantClientExtensionsTest.cs | 121 ++++++++++++++++++ .../AspireQdrantHelpers.cs | 37 ------ .../ConfigurationTests.cs | 8 ++ .../ConformanceTests.cs | 74 ++++++----- .../QdrantContainerFixture.cs | 49 +++++++ 15 files changed, 290 insertions(+), 83 deletions(-) create mode 100644 tests/Aspire.Qdrant.Client.Tests/AspireQdrantClientExtensionsTest.cs delete mode 100644 tests/Aspire.Qdrant.Client.Tests/AspireQdrantHelpers.cs create mode 100644 tests/Aspire.Qdrant.Client.Tests/QdrantContainerFixture.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index b39ffe5440..8f77046a07 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -174,6 +174,7 @@ + diff --git a/src/Aspire.Hosting.Qdrant/Aspire.Hosting.Qdrant.csproj b/src/Aspire.Hosting.Qdrant/Aspire.Hosting.Qdrant.csproj index 27699a9b97..4db984da75 100644 --- a/src/Aspire.Hosting.Qdrant/Aspire.Hosting.Qdrant.csproj +++ b/src/Aspire.Hosting.Qdrant/Aspire.Hosting.Qdrant.csproj @@ -20,4 +20,8 @@ + + + + diff --git a/src/Aspire.Hosting.Qdrant/QdrantBuilderExtensions.cs b/src/Aspire.Hosting.Qdrant/QdrantBuilderExtensions.cs index f92ca39021..52b7eaf55b 100644 --- a/src/Aspire.Hosting.Qdrant/QdrantBuilderExtensions.cs +++ b/src/Aspire.Hosting.Qdrant/QdrantBuilderExtensions.cs @@ -8,6 +8,7 @@ using Aspire.Qdrant.Client; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; +using Qdrant.Client; namespace Aspire.Hosting; @@ -47,20 +48,21 @@ public static IResourceBuilder AddQdrant(this IDistributed ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-Key", special: false); var qdrant = new QdrantServerResource(name, apiKeyParameter); - HttpClient? httpClient = null; + QdrantClient? qdrantClient = null; builder.Eventing.Subscribe(qdrant, async (@event, ct) => { - var connectionString = await qdrant.HttpConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false) + var connectionString = await qdrant.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false) ?? throw new DistributedApplicationException($"ConnectionStringAvailableEvent was published for the '{qdrant.Name}' resource but the connection string was null."); - httpClient = CreateQdrantHttpClient(connectionString); + + qdrantClient = CreateQdrantClient(connectionString); }); var healthCheckKey = $"{name}_check"; builder.Services.AddHealthChecks() .Add(new HealthCheckRegistration( healthCheckKey, - sp => new QdrantHealthCheck(httpClient!), + sp => new QdrantHealthCheck(qdrantClient ?? throw new InvalidOperationException("Qdrant Client is unavailable")), failureStatus: default, tags: default, timeout: default)); @@ -142,7 +144,7 @@ public static IResourceBuilder WithReference(this IR return builder; } - private static HttpClient CreateQdrantHttpClient(string? connectionString) + private static QdrantClient CreateQdrantClient(string? connectionString) { if (connectionString is null) { @@ -174,12 +176,12 @@ private static HttpClient CreateQdrantHttpClient(string? connectionString) } } - var client = new HttpClient(); - client.BaseAddress = endpoint; - if (key is not null) + if (endpoint is null) { - client.DefaultRequestHeaders.Add("Api-Key", key); + throw new InvalidOperationException("Endpoint is unavailable"); } + + var client = new QdrantClient(endpoint, key); return client; } } diff --git a/src/Components/Aspire.Qdrant.Client/Aspire.Qdrant.Client.csproj b/src/Components/Aspire.Qdrant.Client/Aspire.Qdrant.Client.csproj index 8a10093630..5690fc0e77 100644 --- a/src/Components/Aspire.Qdrant.Client/Aspire.Qdrant.Client.csproj +++ b/src/Components/Aspire.Qdrant.Client/Aspire.Qdrant.Client.csproj @@ -14,6 +14,7 @@ + diff --git a/src/Components/Aspire.Qdrant.Client/AspireQdrantExtensions.cs b/src/Components/Aspire.Qdrant.Client/AspireQdrantExtensions.cs index 751c2fb389..358fd6a121 100644 --- a/src/Components/Aspire.Qdrant.Client/AspireQdrantExtensions.cs +++ b/src/Components/Aspire.Qdrant.Client/AspireQdrantExtensions.cs @@ -1,9 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Aspire; using Aspire.Qdrant.Client; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; using Qdrant.Client; @@ -80,6 +82,21 @@ private static void AddQdrant( builder.Services.AddKeyedSingleton(serviceKey, (sp, key) => ConfigureQdrant(sp)); } + if (!settings.DisableHealthChecks) + { + var healthCheckName = serviceKey is null ? "Qdrant.Client" : $"Qdrant.Client_{connectionName}"; + + builder.TryAddHealthCheck(new HealthCheckRegistration( + healthCheckName, + sp => new QdrantHealthCheck(serviceKey is null ? + sp.GetRequiredService() : + sp.GetRequiredKeyedService(serviceKey)), + failureStatus: null, + tags: null, + timeout: settings.HealthCheckTimeout + )); + } + QdrantClient ConfigureQdrant(IServiceProvider serviceProvider) { if (settings.Endpoint is not null) diff --git a/src/Components/Aspire.Qdrant.Client/ConfigurationSchema.json b/src/Components/Aspire.Qdrant.Client/ConfigurationSchema.json index 690d7f94a4..cd18ec2903 100644 --- a/src/Components/Aspire.Qdrant.Client/ConfigurationSchema.json +++ b/src/Components/Aspire.Qdrant.Client/ConfigurationSchema.json @@ -19,11 +19,21 @@ "Client": { "type": "object", "properties": { + "DisableHealthChecks": { + "type": "boolean", + "description": "Gets or sets a boolean value that indicates whether the Qdrant client health check is disabled or not.", + "default": false + }, "Endpoint": { "type": "string", "format": "uri", "description": "The endpoint URI string of the Qdrant server to connect to." }, + "HealthCheckTimeout": { + "type": "string", + "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$", + "description": "Gets or sets the timeout duration for the health check." + }, "Key": { "type": "string", "description": "The API Key of the Qdrant server to connect to." diff --git a/src/Components/Aspire.Qdrant.Client/PublicAPI.Unshipped.txt b/src/Components/Aspire.Qdrant.Client/PublicAPI.Unshipped.txt index 074c6ad103..f98b161e26 100644 --- a/src/Components/Aspire.Qdrant.Client/PublicAPI.Unshipped.txt +++ b/src/Components/Aspire.Qdrant.Client/PublicAPI.Unshipped.txt @@ -1,2 +1,5 @@ #nullable enable - +Aspire.Qdrant.Client.QdrantClientSettings.DisableHealthChecks.get -> bool +Aspire.Qdrant.Client.QdrantClientSettings.DisableHealthChecks.set -> void +Aspire.Qdrant.Client.QdrantClientSettings.HealthCheckTimeout.get -> System.TimeSpan? +Aspire.Qdrant.Client.QdrantClientSettings.HealthCheckTimeout.set -> void diff --git a/src/Components/Aspire.Qdrant.Client/QdrantClientSettings.cs b/src/Components/Aspire.Qdrant.Client/QdrantClientSettings.cs index 5db43f6549..1a64aea944 100644 --- a/src/Components/Aspire.Qdrant.Client/QdrantClientSettings.cs +++ b/src/Components/Aspire.Qdrant.Client/QdrantClientSettings.cs @@ -23,6 +23,22 @@ public sealed class QdrantClientSettings ///
public string? Key { get; set; } + /// + /// Gets or sets a boolean value that indicates whether the Qdrant client health check is disabled or not. + /// + /// + /// The default value is . + /// + public bool DisableHealthChecks { get; set; } + + /// + /// Gets or sets the timeout duration for the health check. + /// + /// + /// The default value is . + /// + public TimeSpan? HealthCheckTimeout { get; set; } + internal void ParseConnectionString(string? connectionString) { if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri)) diff --git a/src/Components/Aspire.Qdrant.Client/QdrantHealthCheck.cs b/src/Components/Aspire.Qdrant.Client/QdrantHealthCheck.cs index e9593b9b8c..d9c93e382e 100644 --- a/src/Components/Aspire.Qdrant.Client/QdrantHealthCheck.cs +++ b/src/Components/Aspire.Qdrant.Client/QdrantHealthCheck.cs @@ -2,13 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Diagnostics.HealthChecks; +using Qdrant.Client; namespace Aspire.Qdrant.Client; internal sealed class QdrantHealthCheck : IHealthCheck { - private readonly HttpClient _client; + private readonly QdrantClient _client; - public QdrantHealthCheck(HttpClient client) + public QdrantHealthCheck(QdrantClient client) { ArgumentNullException.ThrowIfNull(client, nameof(client)); _client = client; @@ -18,9 +19,9 @@ public async Task CheckHealthAsync(HealthCheckContext context { try { - var response = await _client.GetAsync("/readyz", cancellationToken).ConfigureAwait(false); + var response = await _client.HealthAsync(cancellationToken).ConfigureAwait(false); - return response.IsSuccessStatusCode + return response?.Title is not null ? HealthCheckResult.Healthy() : new HealthCheckResult(HealthStatus.Unhealthy); } diff --git a/tests/Aspire.Qdrant.Client.Tests/Aspire.Qdrant.Client.Tests.csproj b/tests/Aspire.Qdrant.Client.Tests/Aspire.Qdrant.Client.Tests.csproj index ad310b5c82..f1d6f5df21 100644 --- a/tests/Aspire.Qdrant.Client.Tests/Aspire.Qdrant.Client.Tests.csproj +++ b/tests/Aspire.Qdrant.Client.Tests/Aspire.Qdrant.Client.Tests.csproj @@ -7,6 +7,7 @@ + diff --git a/tests/Aspire.Qdrant.Client.Tests/AspireQdrantClientExtensionsTest.cs b/tests/Aspire.Qdrant.Client.Tests/AspireQdrantClientExtensionsTest.cs new file mode 100644 index 0000000000..022cb0e352 --- /dev/null +++ b/tests/Aspire.Qdrant.Client.Tests/AspireQdrantClientExtensionsTest.cs @@ -0,0 +1,121 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Components.Common.Tests; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Hosting; +using Qdrant.Client; +using Xunit; + +namespace Aspire.Qdrant.Client.Tests; + +public class AspireQdrantClientExtensionsTest : IClassFixture +{ + private const string DefaultConnectionName = "qdrant"; + + private readonly QdrantContainerFixture _containerFixture; + + public AspireQdrantClientExtensionsTest(QdrantContainerFixture containerFixture) + { + _containerFixture = containerFixture; + } + + private string DefaultConnectionString => + RequiresDockerAttribute.IsSupported ? _containerFixture.GetConnectionString() : "Endpoint=http://localhost1:6331;Key=pass"; + + [Theory] + [InlineData(true)] + [InlineData(false)] + [RequiresDocker] + public async Task AddQdrantClient_HealthCheckShouldBeRegisteredWhenEnabled(bool useKeyed) + { + var key = DefaultConnectionName; + + var builder = CreateBuilder(DefaultConnectionString); + + if (useKeyed) + { + builder.AddKeyedQdrantClient(key); + } + else + { + builder.AddQdrantClient(DefaultConnectionName); + } + + using var host = builder.Build(); + + var healthCheckService = host.Services.GetRequiredService(); + + var healthCheckReport = await healthCheckService.CheckHealthAsync(); + + var healthCheckName = useKeyed ? $"Qdrant.Client_{key}" : "Qdrant.Client"; + + Assert.Contains(healthCheckReport.Entries, x => x.Key == healthCheckName); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AddQdrant_HealthCheckShouldNotBeRegisteredWhenDisabled(bool useKeyed) + { + var builder = CreateBuilder(DefaultConnectionString); + + if (useKeyed) + { + builder.AddKeyedQdrantClient(DefaultConnectionName, settings => + { + settings.DisableHealthChecks = true; + }); + } + else + { + builder.AddQdrantClient(DefaultConnectionName, settings => + { + settings.DisableHealthChecks = true; + }); + } + + using var host = builder.Build(); + + var healthCheckService = host.Services.GetService(); + + Assert.Null(healthCheckService); + } + + [Fact] + public void CanAddMultipleKeyedServices() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + builder.Configuration.AddInMemoryCollection([ + new KeyValuePair("ConnectionStrings:qdrant1", "Endpoint=http://localhost1:6331;Key=pass"), + new KeyValuePair("ConnectionStrings:qdrant2", "Endpoint=http://localhost2:6332;Key=pass"), + new KeyValuePair("ConnectionStrings:qdrant3", "Endpoint=http://localhost3:6333;Key=pass"), + ]); + + builder.AddQdrantClient("qdrant1"); + builder.AddKeyedQdrantClient("qdrant2"); + builder.AddKeyedQdrantClient("qdrant3"); + + using var host = builder.Build(); + + var client1 = host.Services.GetRequiredService(); + var client2 = host.Services.GetRequiredKeyedService("qdrant2"); + var client3 = host.Services.GetRequiredKeyedService("qdrant3"); + + Assert.NotSame(client1, client2); + Assert.NotSame(client1, client3); + Assert.NotSame(client2, client3); + } + + private static HostApplicationBuilder CreateBuilder(string connectionString) + { + var builder = Host.CreateEmptyApplicationBuilder(null); + + builder.Configuration.AddInMemoryCollection([ + new KeyValuePair($"ConnectionStrings:{DefaultConnectionName}", connectionString) + ]); + return builder; + } +} diff --git a/tests/Aspire.Qdrant.Client.Tests/AspireQdrantHelpers.cs b/tests/Aspire.Qdrant.Client.Tests/AspireQdrantHelpers.cs deleted file mode 100644 index 40c3e18400..0000000000 --- a/tests/Aspire.Qdrant.Client.Tests/AspireQdrantHelpers.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.DotNet.XUnitExtensions; -using Qdrant.Client; - -namespace Aspire.Qdrant.Client.Tests; -public static class AspireQdrantHelpers -{ - public const string TestingEndpoint = "http://localhost:6334"; - - private static readonly Lazy s_canConnectToServer = new(GetCanConnect); - public static bool CanConnectToServer => s_canConnectToServer.Value; - - public static void SkipIfCanNotConnectToServer() - { - if (!CanConnectToServer) - { - throw new SkipTestException("Unable to connect to the server."); - } - } - - private static bool GetCanConnect() - { - try - { - var client = new QdrantClient(new Uri(TestingEndpoint)); - client.ListCollectionsAsync().Wait(); - return true; - } - catch (Exception ex) - { - Console.WriteLine(ex.Message); - return false; - } - } -} diff --git a/tests/Aspire.Qdrant.Client.Tests/ConfigurationTests.cs b/tests/Aspire.Qdrant.Client.Tests/ConfigurationTests.cs index 5761b2f0fb..833691c916 100644 --- a/tests/Aspire.Qdrant.Client.Tests/ConfigurationTests.cs +++ b/tests/Aspire.Qdrant.Client.Tests/ConfigurationTests.cs @@ -9,4 +9,12 @@ public class ConfigurationTests [Fact] public void EndpointIsNullByDefault() => Assert.Null(new QdrantClientSettings().Endpoint); + + [Fact] + public void HealthChecksEnabledByDefault() => + Assert.False(new QdrantClientSettings().DisableHealthChecks); + + [Fact] + public void HealthCheckTimeoutNullByDefault() => + Assert.Null(new QdrantClientSettings().HealthCheckTimeout); } diff --git a/tests/Aspire.Qdrant.Client.Tests/ConformanceTests.cs b/tests/Aspire.Qdrant.Client.Tests/ConformanceTests.cs index e45ce81a99..8d4409d131 100644 --- a/tests/Aspire.Qdrant.Client.Tests/ConformanceTests.cs +++ b/tests/Aspire.Qdrant.Client.Tests/ConformanceTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Aspire.Components.Common.Tests; using Aspire.Components.ConformanceTests; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -10,13 +11,17 @@ namespace Aspire.Qdrant.Client.Tests; -public class ConformanceTests : ConformanceTests +public class ConformanceTests : ConformanceTests, IClassFixture { + private readonly QdrantContainerFixture _containerFixture; + + private readonly string _connectionString; + protected override bool SupportsKeyedRegistrations => true; protected override bool CanCreateClientWithoutConnectingToServer => false; - protected override bool CanConnectToServer => AspireQdrantHelpers.CanConnectToServer; + protected override bool CanConnectToServer => RequiresDockerAttribute.IsSupported; protected override ServiceLifetime ServiceLifetime => ServiceLifetime.Singleton; @@ -24,6 +29,16 @@ public class ConformanceTests : ConformanceTests ""; + protected override string? ConfigurationSectionName => "Aspire:Qdrant:Client"; + + public ConformanceTests(QdrantContainerFixture containerFixture) + { + _containerFixture = containerFixture; + _connectionString = RequiresDockerAttribute.IsSupported ? + $"{_containerFixture.GetConnectionString()}" : + "Endpoint=http://localhost:6334;Key=pass"; + } + protected override void RegisterComponent(HostApplicationBuilder builder, Action? configure = null, string? key = null) { if (key is null) @@ -37,17 +52,37 @@ protected override void RegisterComponent(HostApplicationBuilder builder, Action } protected override void PopulateConfiguration(ConfigurationManager configuration, string? key = null) - => configuration.AddInMemoryCollection(new KeyValuePair[2] + { + configuration.AddInMemoryCollection( + [ + new KeyValuePair(CreateConfigKey("Aspire:Qdrant:Client", key, "Endpoint"), GetConnectionStringKeyValue(_connectionString,"Endpoint")), + new KeyValuePair(CreateConfigKey("Aspire:Qdrant:Client", key, "Key"), GetConnectionStringKeyValue(_connectionString,"Key")), + ]); + } + + internal static string GetConnectionStringKeyValue(string connectionString, string configKey) + { + // from the connection string, extract the key value of the configKey + var parts = connectionString.Split(';'); + foreach (var part in parts) { - new KeyValuePair(CreateConfigKey("Aspire:Qdrant:Client", key, "Endpoint"), "http://localhost:6334"), - new KeyValuePair($"ConnectionStrings:{key}","Endpoint=http://localhost:6334;Key=pass") - }); + var keyValue = part.Split('='); + if (keyValue.Length == 2 && keyValue[0].Equals(configKey, StringComparison.OrdinalIgnoreCase)) + { + return keyValue[1]; + } + } + return string.Empty; + } protected override void TriggerActivity(QdrantClient service) { } - protected override void SetHealthCheck(QdrantClientSettings options, bool enabled) => throw new NotImplementedException(); + protected override void SetHealthCheck(QdrantClientSettings options, bool enabled) + { + options.DisableHealthChecks = !enabled; + } protected override void SetTracing(QdrantClientSettings options, bool enabled) => throw new NotImplementedException(); @@ -70,29 +105,4 @@ protected override (string json, string error)[] InvalidJsonToErrorMessage => ne ("""{"Aspire": { "Qdrant":{ "Client": { "Endpoint": 3 }}}}""", "Value is \"integer\" but should be \"string\""), ("""{"Aspire": { "Qdrant":{ "Client": { "Endpoint": "hello" }}}}""", "Value does not match format \"uri\"") }; - - [Fact] - public void CanAddMultipleKeyedServices() - { - var builder = Host.CreateEmptyApplicationBuilder(null); - builder.Configuration.AddInMemoryCollection([ - new KeyValuePair("ConnectionStrings:qdrant1", "Endpoint=http://localhost1:6331;Key=pass"), - new KeyValuePair("ConnectionStrings:qdrant2", "Endpoint=http://localhost2:6332;Key=pass"), - new KeyValuePair("ConnectionStrings:qdrant3", "Endpoint=http://localhost3:6333;Key=pass"), - ]); - - builder.AddQdrantClient("qdrant1"); - builder.AddKeyedQdrantClient("qdrant2"); - builder.AddKeyedQdrantClient("qdrant3"); - - using var host = builder.Build(); - - var client1 = host.Services.GetRequiredService(); - var client2 = host.Services.GetRequiredKeyedService("qdrant2"); - var client3 = host.Services.GetRequiredKeyedService("qdrant3"); - - Assert.NotSame(client1, client2); - Assert.NotSame(client1, client3); - Assert.NotSame(client2, client3); - } } diff --git a/tests/Aspire.Qdrant.Client.Tests/QdrantContainerFixture.cs b/tests/Aspire.Qdrant.Client.Tests/QdrantContainerFixture.cs new file mode 100644 index 0000000000..693e37a60e --- /dev/null +++ b/tests/Aspire.Qdrant.Client.Tests/QdrantContainerFixture.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Components.Common.Tests; +using Aspire.Hosting.Qdrant; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; +using Xunit; + +namespace Aspire.Qdrant.Client.Tests; + +public sealed class QdrantContainerFixture : IAsyncLifetime +{ + public IContainer? Container { get; private set; } + + private const int GrpcPort = 6334; + + public string GetConnectionString() + { + if (Container is null) + { + throw new InvalidOperationException("The test container was not initialized."); + } + var endpoint = new UriBuilder("http", Container.Hostname, Container.GetMappedPublicPort(GrpcPort)).ToString(); + return $"Endpoint={endpoint}"; + } + + public async Task InitializeAsync() + { + if (RequiresDockerAttribute.IsSupported) + { + Container = new ContainerBuilder() + .WithImage($"{ComponentTestConstants.AspireTestContainerRegistry}/{QdrantContainerImageTags.Image}:{QdrantContainerImageTags.Tag}") + .WithPortBinding(GrpcPort, true) + .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(GrpcPort)) + .Build(); + + await Container.StartAsync(); + } + } + + public async Task DisposeAsync() + { + if (Container is not null) + { + await Container.DisposeAsync(); + } + } +} From 279456f2f752fd07e4b28f227fb3b1dbd644c8ed Mon Sep 17 00:00:00 2001 From: Mrxx99 <33566379+Mrxx99@users.noreply.github.com> Date: Thu, 7 Nov 2024 00:33:10 +0100 Subject: [PATCH 34/44] Add password protection to NATS (#6259) This adds parameter support to NATS. The postgres implementation was used as inspiration for the implementation. It uses the user info of the connection string URL to add user name and password, so the connections string looks like nats://nats:MGBKxjTAjX2stg2wzzjtVW@localhost:52166. Some NATS clients support this form natively but the .NET one unfortunately does not. It would be nice if this support would be added directly to NATS.Net then that code could be removed from here again. Contributes to: #6155 * Add password protection to NATS * fix redacting of password * restore name only NatsServerResource constructor to remove breaking change + added more tests * fix duplicated namespace * authentication tests use same wait mechanic as other tests * restore xml comment for resource name * Update src/Aspire.Hosting.Nats/NatsServerResource.cs Co-authored-by: Eric Erhardt * Update src/Aspire.Hosting.Nats/NatsServerResource.cs Co-authored-by: Eric Erhardt * Update tests/Aspire.Hosting.Nats.Tests/NatsFunctionalTests.cs Co-authored-by: Eric Erhardt * Update tests/Aspire.Hosting.Nats.Tests/AddNatsTests.cs Co-authored-by: Eric Erhardt * addressed some review comments * updated to Nats.NET 2.5.3, use authentication in connection string that is now built in * re-add original AddNats overload (without optional parameter) * Trigger build --------- Co-authored-by: Eric Erhardt --- Directory.Packages.props | 4 +- .../nats/Nats.AppHost/aspire-manifest.json | 22 +- .../NatsBuilderExtensions.cs | 48 +++- src/Aspire.Hosting.Nats/NatsServerResource.cs | 48 +++- src/Aspire.Hosting.Nats/PublicAPI.Shipped.txt | 2 +- .../PublicAPI.Unshipped.txt | 7 +- .../Aspire.Hosting.Nats.Tests/AddNatsTests.cs | 216 ++++++++++++++++-- .../NatsFunctionalTests.cs | 93 ++++++++ .../NatsPublicApiTests.cs | 36 ++- .../AspireNatsClientExtensionsTests.cs | 31 ++- .../Aspire.NATS.Net.Tests/ConformanceTests.cs | 2 +- 11 files changed, 468 insertions(+), 41 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 8f77046a07..687d56057a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -124,7 +124,7 @@ - + @@ -200,4 +200,4 @@
- + \ No newline at end of file diff --git a/playground/nats/Nats.AppHost/aspire-manifest.json b/playground/nats/Nats.AppHost/aspire-manifest.json index 54280f398b..0564155c2d 100644 --- a/playground/nats/Nats.AppHost/aspire-manifest.json +++ b/playground/nats/Nats.AppHost/aspire-manifest.json @@ -3,9 +3,13 @@ "resources": { "nats": { "type": "container.v0", - "connectionString": "nats://{nats.bindings.tcp.host}:{nats.bindings.tcp.port}", + "connectionString": "nats://nats:{nats-password.value}@{nats.bindings.tcp.host}:{nats.bindings.tcp.port}", "image": "docker.io/library/nats:2.10", "args": [ + "--user", + "nats", + "--pass", + "{nats-password.value}", "-js" ], "bindings": { @@ -66,6 +70,22 @@ "transport": "http" } } + }, + "nats-password": { + "type": "parameter.v0", + "value": "{nats-password.inputs.value}", + "inputs": { + "value": { + "type": "string", + "secret": true, + "default": { + "generate": { + "minLength": 22, + "special": false + } + } + } + } } } } \ No newline at end of file diff --git a/src/Aspire.Hosting.Nats/NatsBuilderExtensions.cs b/src/Aspire.Hosting.Nats/NatsBuilderExtensions.cs index 145ba29202..9bb481147f 100644 --- a/src/Aspire.Hosting.Nats/NatsBuilderExtensions.cs +++ b/src/Aspire.Hosting.Nats/NatsBuilderExtensions.cs @@ -19,6 +19,7 @@ public static class NatsBuilderExtensions { /// /// Adds a NATS server resource to the application model. A container is used for local development. + /// This configures a default user name and password for the NATS server. /// /// /// This version of the package defaults to the tag of the container image. @@ -27,12 +28,30 @@ public static class NatsBuilderExtensions /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. /// The host port for NATS server. /// The . - public static IResourceBuilder AddNats(this IDistributedApplicationBuilder builder, [ResourceName] string name, int? port = null) + public static IResourceBuilder AddNats(this IDistributedApplicationBuilder builder, [ResourceName] string name, int? port) + { + return AddNats(builder, name, port, null); + } + + /// + /// Adds a NATS server resource to the application model. A container is used for local development. + /// + /// The . + /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. + /// The host port for NATS server. + /// The parameter used to provide the user name for the PostgreSQL resource. If a default value will be used. + /// The parameter used to provide the administrator password for the PostgreSQL resource. If a random password will be generated. + /// The . + public static IResourceBuilder AddNats(this IDistributedApplicationBuilder builder, [ResourceName] string name, int? port = null, + IResourceBuilder? userName = null, + IResourceBuilder? password = null) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(name); - var nats = new NatsServerResource(name); + var passwordParameter = password?.Resource ?? ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-password", special: false); + + var nats = new NatsServerResource(name, userName?.Resource, passwordParameter); NatsConnection? natsConnection = null; @@ -46,7 +65,15 @@ public static IResourceBuilder AddNats(this IDistributedAppl LoggerFactory = @event.Services.GetRequiredService(), }; - options = options with { Url = connectionString }; + options = options with + { + Url = connectionString, + AuthOpts = new() + { + Username = await nats.UserNameReference.GetValueAsync(ct).ConfigureAwait(false), + Password = nats.PasswordParameter!.Value, + } + }; natsConnection = new NatsConnection(options); }); @@ -61,10 +88,17 @@ public static IResourceBuilder AddNats(this IDistributedAppl timeout: default)); return builder.AddResource(nats) - .WithEndpoint(targetPort: 4222, port: port, name: NatsServerResource.PrimaryEndpointName) - .WithImage(NatsContainerImageTags.Image, NatsContainerImageTags.Tag) - .WithImageRegistry(NatsContainerImageTags.Registry) - .WithHealthCheck(healthCheckKey); + .WithEndpoint(targetPort: 4222, port: port, name: NatsServerResource.PrimaryEndpointName) + .WithImage(NatsContainerImageTags.Image, NatsContainerImageTags.Tag) + .WithImageRegistry(NatsContainerImageTags.Registry) + .WithHealthCheck(healthCheckKey) + .WithArgs(context => + { + context.Args.Add("--user"); + context.Args.Add(nats.UserNameReference); + context.Args.Add("--pass"); + context.Args.Add(nats.PasswordParameter!); + }); } /// diff --git a/src/Aspire.Hosting.Nats/NatsServerResource.cs b/src/Aspire.Hosting.Nats/NatsServerResource.cs index a4d60962e6..117c628d0d 100644 --- a/src/Aspire.Hosting.Nats/NatsServerResource.cs +++ b/src/Aspire.Hosting.Nats/NatsServerResource.cs @@ -10,25 +10,65 @@ namespace Aspire.Hosting.ApplicationModel; /// A resource that represents a NATS server container. /// /// The name of the resource. - public class NatsServerResource(string name) : ContainerResource(ThrowIfNull(name)), IResourceWithConnectionString { internal const string PrimaryEndpointName = "tcp"; internal const string PrimaryNatsSchemeName = "nats"; + private const string DefaultUserName = "nats"; private EndpointReference? _primaryEndpoint; + /// + /// Initializes a new instance of the class. + /// + /// The name of the resource. + /// A parameter that contains the NATS server user name, or to use a default value. + /// A parameter that contains the NATS server password. + public NatsServerResource(string name, ParameterResource? userName, ParameterResource? password) : this(name) + { + UserNameParameter = userName; + PasswordParameter = password; + } + /// /// Gets the primary endpoint for the NATS server. /// public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName); + /// + /// Gets or sets the user name for the NATS server. + /// + public ParameterResource? UserNameParameter { get; set; } + + internal ReferenceExpression UserNameReference => + UserNameParameter is not null ? + ReferenceExpression.Create($"{UserNameParameter}") : + ReferenceExpression.Create($"{DefaultUserName}"); + + /// + /// Gets or sets the password for the NATS server. + /// + public ParameterResource? PasswordParameter { get; set; } + /// /// Gets the connection string expression for the NATS server. /// - public ReferenceExpression ConnectionStringExpression => - ReferenceExpression.Create( - $"{PrimaryNatsSchemeName}://{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}"); + public ReferenceExpression ConnectionStringExpression => BuildConnectionString(); + + internal ReferenceExpression BuildConnectionString() + { + var builder = new ReferenceExpressionBuilder(); + builder.AppendLiteral($"{PrimaryNatsSchemeName}://"); + + if (PasswordParameter is not null) + { + builder.Append($"{UserNameReference}:{PasswordParameter}@"); + } + + builder.Append($"{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}"); + + return builder.Build(); + } private static string ThrowIfNull([NotNull] string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) => argument ?? throw new ArgumentNullException(paramName); diff --git a/src/Aspire.Hosting.Nats/PublicAPI.Shipped.txt b/src/Aspire.Hosting.Nats/PublicAPI.Shipped.txt index cc5ad423ee..2c2a4415f4 100644 --- a/src/Aspire.Hosting.Nats/PublicAPI.Shipped.txt +++ b/src/Aspire.Hosting.Nats/PublicAPI.Shipped.txt @@ -4,7 +4,7 @@ Aspire.Hosting.ApplicationModel.NatsServerResource.ConnectionStringExpression.ge Aspire.Hosting.ApplicationModel.NatsServerResource.NatsServerResource(string! name) -> void Aspire.Hosting.ApplicationModel.NatsServerResource.PrimaryEndpoint.get -> Aspire.Hosting.ApplicationModel.EndpointReference! Aspire.Hosting.NatsBuilderExtensions -static Aspire.Hosting.NatsBuilderExtensions.AddNats(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, int? port = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.NatsBuilderExtensions.AddNats(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, int? port) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.NatsBuilderExtensions.WithDataBindMount(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! source, bool isReadOnly = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.NatsBuilderExtensions.WithDataVolume(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string? name = null, bool isReadOnly = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.NatsBuilderExtensions.WithJetStream(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! diff --git a/src/Aspire.Hosting.Nats/PublicAPI.Unshipped.txt b/src/Aspire.Hosting.Nats/PublicAPI.Unshipped.txt index 074c6ad103..68906927aa 100644 --- a/src/Aspire.Hosting.Nats/PublicAPI.Unshipped.txt +++ b/src/Aspire.Hosting.Nats/PublicAPI.Unshipped.txt @@ -1,2 +1,7 @@ #nullable enable - +Aspire.Hosting.ApplicationModel.NatsServerResource.NatsServerResource(string! name, Aspire.Hosting.ApplicationModel.ParameterResource? userName, Aspire.Hosting.ApplicationModel.ParameterResource? password) -> void +Aspire.Hosting.ApplicationModel.NatsServerResource.PasswordParameter.get -> Aspire.Hosting.ApplicationModel.ParameterResource? +Aspire.Hosting.ApplicationModel.NatsServerResource.PasswordParameter.set -> void +Aspire.Hosting.ApplicationModel.NatsServerResource.UserNameParameter.get -> Aspire.Hosting.ApplicationModel.ParameterResource? +Aspire.Hosting.ApplicationModel.NatsServerResource.UserNameParameter.set -> void +static Aspire.Hosting.NatsBuilderExtensions.AddNats(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, int? port = null, Aspire.Hosting.ApplicationModel.IResourceBuilder? userName = null, Aspire.Hosting.ApplicationModel.IResourceBuilder? password = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! diff --git a/tests/Aspire.Hosting.Nats.Tests/AddNatsTests.cs b/tests/Aspire.Hosting.Nats.Tests/AddNatsTests.cs index 3a9d71f0ba..5972e53922 100644 --- a/tests/Aspire.Hosting.Nats.Tests/AddNatsTests.cs +++ b/tests/Aspire.Hosting.Nats.Tests/AddNatsTests.cs @@ -1,18 +1,89 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Aspire.Hosting.Utils; using System.Net.Sockets; +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Tests.Utils; +using Aspire.Hosting.Utils; using Microsoft.Extensions.DependencyInjection; using Xunit; -using Aspire.Hosting.ApplicationModel; namespace Aspire.Hosting.Nats.Tests; public class AddNatsTests { [Fact] - public void AddNatsContainerWithDefaultsAddsAnnotationMetadata() + public void AddNatsAddsGeneratedPasswordParameterWithUserSecretsParameterDefaultInRunMode() + { + using var appBuilder = TestDistributedApplicationBuilder.Create(); + + var nats = appBuilder.AddNats("nats"); + Assert.Equal("Aspire.Hosting.ApplicationModel.UserSecretsParameterDefault", nats.Resource.PasswordParameter!.Default?.GetType().FullName); + } + + [Fact] + public void AddNatsDoesNotAddGeneratedPasswordParameterWithUserSecretsParameterDefaultInPublishMode() + { + using var appBuilder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var nats = appBuilder.AddNats("nats"); + + Assert.NotEqual("Aspire.Hosting.ApplicationModel.UserSecretsParameterDefault", nats.Resource.PasswordParameter!.Default?.GetType().FullName); + } + + [Fact] + public async Task AddNatsSetsDefaultUserNameAndPasswordAndIncludesThemInConnectionString() + { + var appBuilder = DistributedApplication.CreateBuilder(); + + var nats = appBuilder.AddNats("nats") + .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 4222)); + + Assert.NotNull(nats.Resource.PasswordParameter); + Assert.False(string.IsNullOrEmpty(nats.Resource.PasswordParameter!.Value)); + + using var app = appBuilder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var natsResource = Assert.Single(appModel.Resources.OfType()); + var connectionStringResource = natsResource as IResourceWithConnectionString; + Assert.NotNull(connectionStringResource); + var connectionString = await connectionStringResource.GetConnectionStringAsync(); + + Assert.Equal($"nats://nats:{natsResource.PasswordParameter?.Value}@localhost:4222", connectionString); + Assert.Equal("nats://nats:{nats-password.value}@{nats.bindings.tcp.host}:{nats.bindings.tcp.port}", connectionStringResource.ConnectionStringExpression.ValueExpression); + } + + [Fact] + public async Task AddNatsSetsUserNameAndPasswordAndIncludesThemInConnection() + { + var appBuilder = DistributedApplication.CreateBuilder(); + var userParameters = appBuilder.AddParameter("user", "usr"); + var passwordParameters = appBuilder.AddParameter("pass", "password"); + + var nats = appBuilder.AddNats("nats", userName: userParameters, password: passwordParameters) + .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 4222)); + + Assert.NotNull(nats.Resource.UserNameParameter); + Assert.NotNull(nats.Resource.PasswordParameter); + + Assert.Equal("usr", nats.Resource.UserNameParameter!.Value); + Assert.Equal("password", nats.Resource.PasswordParameter!.Value); + + using var app = appBuilder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var connectionStringResource = Assert.Single(appModel.Resources.OfType()) as IResourceWithConnectionString; + var connectionString = await connectionStringResource.GetConnectionStringAsync(); + + Assert.Equal("nats://usr:password@localhost:4222", connectionString); + Assert.Equal("nats://{user.value}:{pass.value}@{nats.bindings.tcp.host}:{nats.bindings.tcp.port}", connectionStringResource.ConnectionStringExpression.ValueExpression); + } + + [Fact] + public async Task AddNatsContainerWithDefaultsAddsAnnotationMetadata() { var appBuilder = DistributedApplication.CreateBuilder(); @@ -38,15 +109,27 @@ public void AddNatsContainerWithDefaultsAddsAnnotationMetadata() Assert.Equal(NatsContainerImageTags.Tag, containerAnnotation.Tag); Assert.Equal(NatsContainerImageTags.Image, containerAnnotation.Image); Assert.Equal(NatsContainerImageTags.Registry, containerAnnotation.Registry); + + var args = await ArgumentEvaluator.GetArgumentListAsync(containerResource); + + Assert.Collection(args, + arg => Assert.Equal("--user", arg), + arg => Assert.Equal("nats", arg), + arg => Assert.Equal("--pass", arg), + arg => Assert.False(string.IsNullOrEmpty(arg)) + ); } [Fact] - public void AddNatsContainerAddsAnnotationMetadata() + public async Task AddNatsContainerAddsAnnotationMetadata() { var path = OperatingSystem.IsWindows() ? @"C:\tmp\dev-data" : "/tmp/dev-data"; var appBuilder = DistributedApplication.CreateBuilder(); - appBuilder.AddNats("nats", 1234).WithJetStream().WithDataBindMount(path); + var user = appBuilder.AddParameter("user", "usr"); + var pass = appBuilder.AddParameter("pass", "pass"); + + appBuilder.AddNats("nats", 1234, user, pass).WithJetStream().WithDataBindMount(path); using var app = appBuilder.Build(); @@ -59,17 +142,6 @@ public void AddNatsContainerAddsAnnotationMetadata() Assert.Equal(path, mountAnnotation.Source); Assert.Equal("/var/lib/nats", mountAnnotation.Target); - var argsAnnotations = containerResource.Annotations.OfType(); - - var args = new List(); - foreach (var argsAnnotation in argsAnnotations) - { - Assert.NotNull(argsAnnotation.Callback); - argsAnnotation.Callback(new CommandLineArgsCallbackContext(args)); - - } - Assert.Equal("-js -sd /var/lib/nats".Split(' '), args); - var endpoint = Assert.Single(containerResource.Annotations.OfType()); Assert.Equal(4222, endpoint.TargetPort); Assert.False(endpoint.IsExternal); @@ -83,6 +155,18 @@ public void AddNatsContainerAddsAnnotationMetadata() Assert.Equal(NatsContainerImageTags.Tag, containerAnnotation.Tag); Assert.Equal(NatsContainerImageTags.Image, containerAnnotation.Image); Assert.Equal(NatsContainerImageTags.Registry, containerAnnotation.Registry); + + var args = await ArgumentEvaluator.GetArgumentListAsync(containerResource); + + Assert.Collection(args, + arg => Assert.Equal("--user", arg), + arg => Assert.Equal("usr", arg), + arg => Assert.Equal("--pass", arg), + arg => Assert.Equal("pass", arg), + arg => Assert.Equal("-js", arg), + arg => Assert.Equal("-sd", arg), + arg => Assert.Equal("/var/lib/nats", arg) + ); } [Fact] @@ -106,8 +190,106 @@ public async Task VerifyManifest() var expectedManifest = $$""" { "type": "container.v0", - "connectionString": "nats://{nats.bindings.tcp.host}:{nats.bindings.tcp.port}", + "connectionString": "nats://nats:{nats-password.value}@{nats.bindings.tcp.host}:{nats.bindings.tcp.port}", + "image": "{{NatsContainerImageTags.Registry}}/{{NatsContainerImageTags.Image}}:{{NatsContainerImageTags.Tag}}", + "args": [ + "--user", + "nats", + "--pass", + "{nats-password.value}" + ], + "bindings": { + "tcp": { + "scheme": "tcp", + "protocol": "tcp", + "transport": "tcp", + "targetPort": 4222 + } + } + } + """; + + Assert.Equal(expectedManifest, manifest.ToString()); + } + + [Fact] + public async Task VerifyManifestWihtParameters() + { + using var builder = TestDistributedApplicationBuilder.Create(); + var userNameParameter = builder.AddParameter("user"); + var passwordParameter = builder.AddParameter("pass"); + + var nats = builder.AddNats("nats", userName: userNameParameter, password: passwordParameter) + .WithJetStream(); + + var manifest = await ManifestUtils.GetManifest(nats.Resource); + + var expectedManifest = $$""" + { + "type": "container.v0", + "connectionString": "nats://{user.value}:{pass.value}@{nats.bindings.tcp.host}:{nats.bindings.tcp.port}", + "image": "{{NatsContainerImageTags.Registry}}/{{NatsContainerImageTags.Image}}:{{NatsContainerImageTags.Tag}}", + "args": [ + "--user", + "{user.value}", + "--pass", + "{pass.value}", + "-js" + ], + "bindings": { + "tcp": { + "scheme": "tcp", + "protocol": "tcp", + "transport": "tcp", + "targetPort": 4222 + } + } + } + """; + Assert.Equal(expectedManifest, manifest.ToString()); + + nats = builder.AddNats("nats2", userName: userNameParameter); + + manifest = await ManifestUtils.GetManifest(nats.Resource); + + expectedManifest = $$""" + { + "type": "container.v0", + "connectionString": "nats://{user.value}:{nats2-password.value}@{nats2.bindings.tcp.host}:{nats2.bindings.tcp.port}", + "image": "{{NatsContainerImageTags.Registry}}/{{NatsContainerImageTags.Image}}:{{NatsContainerImageTags.Tag}}", + "args": [ + "--user", + "{user.value}", + "--pass", + "{nats2-password.value}" + ], + "bindings": { + "tcp": { + "scheme": "tcp", + "protocol": "tcp", + "transport": "tcp", + "targetPort": 4222 + } + } + } + """; + Assert.Equal(expectedManifest, manifest.ToString()); + + nats = builder.AddNats("nats3", password: passwordParameter); + + manifest = await ManifestUtils.GetManifest(nats.Resource); + + expectedManifest = $$""" + { + "type": "container.v0", + "connectionString": "nats://nats:{pass.value}@{nats3.bindings.tcp.host}:{nats3.bindings.tcp.port}", "image": "{{NatsContainerImageTags.Registry}}/{{NatsContainerImageTags.Image}}:{{NatsContainerImageTags.Tag}}", + "args": [ + "--user", + "nats", + "--pass", + "{pass.value}" + ], "bindings": { "tcp": { "scheme": "tcp", diff --git a/tests/Aspire.Hosting.Nats.Tests/NatsFunctionalTests.cs b/tests/Aspire.Hosting.Nats.Tests/NatsFunctionalTests.cs index 2be2da97be..2eae679906 100644 --- a/tests/Aspire.Hosting.Nats.Tests/NatsFunctionalTests.cs +++ b/tests/Aspire.Hosting.Nats.Tests/NatsFunctionalTests.cs @@ -13,6 +13,7 @@ using Aspire.Hosting.ApplicationModel; using Microsoft.Extensions.Diagnostics.HealthChecks; using Aspire.Hosting.Tests.Utils; + namespace Aspire.Hosting.Nats.Tests; public class NatsFunctionalTests(ITestOutputHelper testOutputHelper) @@ -57,6 +58,98 @@ public async Task VerifyNatsResource() await ConsumeTestData(jetStream, default); } + [Theory] + [RequiresDocker] + [InlineData(null, null)] + [InlineData("nats", null)] + [InlineData(null, "password")] + [InlineData("nats", "password")] + public async Task AuthenticationShouldWork(string? user, string? password) + { + var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5)); + + using var builder = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(testOutputHelper); + + var usernameParameter = user is null ? null : builder.AddParameter("user", user); + var passwordParameter = password is null ? null : builder.AddParameter("pass", password); + + var nats = builder.AddNats("nats", userName: usernameParameter, password: passwordParameter); + + using var app = builder.Build(); + + await app.StartAsync(); + + await app.WaitForTextAsync("Listening for client connections", nats.Resource.Name); + + var hb = Host.CreateApplicationBuilder(); + + var connectionString = await nats.Resource.ConnectionStringExpression.GetValueAsync(default); + hb.Configuration[$"ConnectionStrings:{nats.Resource.Name}"] = connectionString; + + hb.AddNatsClient("nats", configureOptions: opts => + { + var jsonRegistry = new NatsJsonContextSerializerRegistry(AppJsonContext.Default); + return opts with { SerializerRegistry = jsonRegistry }; + }); + + using var host = hb.Build(); + + await host.StartAsync(); + + var natsConnection = host.Services.GetRequiredService(); + await natsConnection.ConnectAsync(); + Assert.Equal(NatsConnectionState.Open, natsConnection.ConnectionState); + } + + [Theory] + [RequiresDocker] + [InlineData("user", "wrong-password")] + [InlineData("wrong-user", "password")] + [InlineData(null, null)] + public async Task AuthenticationShouldFailOnWrongOrMissingCredentials(string? user, string? password) + { + var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5)); + + using var builder = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(testOutputHelper); + builder.Configuration["Parameters:user"] = "user"; + builder.Configuration["Parameters:pass"] = "password"; + + var usernameParameter = builder.AddParameter("user"); + var passwordParameter = builder.AddParameter("pass"); + + var nats = builder.AddNats("nats", userName: usernameParameter, password: passwordParameter); + + using var app = builder.Build(); + + await app.StartAsync(); + + await app.WaitForTextAsync("Listening for client connections", nats.Resource.Name); + + var hb = Host.CreateApplicationBuilder(); + + var connectionString = await nats.Resource.ConnectionStringExpression.GetValueAsync(default); + var modifiedConnectionString = user is null + ? connectionString!.Replace(new Uri(connectionString).UserInfo, null) + : connectionString!.Replace("user", user).Replace("password", password); + + hb.Configuration[$"ConnectionStrings:{nats.Resource.Name}"] = modifiedConnectionString; + + hb.AddNatsClient("nats", configureOptions: opts => + { + var jsonRegistry = new NatsJsonContextSerializerRegistry(AppJsonContext.Default); + return opts with { SerializerRegistry = jsonRegistry }; + }); + + using var host = hb.Build(); + + await host.StartAsync(); + + var natsConnection = host.Services.GetRequiredService(); + + var exception = await Assert.ThrowsAsync(async () => await natsConnection.ConnectAsync()); + Assert.IsType(exception.InnerException); + } + [Theory] [InlineData(true)] [InlineData(false)] diff --git a/tests/Aspire.Hosting.Nats.Tests/NatsPublicApiTests.cs b/tests/Aspire.Hosting.Nats.Tests/NatsPublicApiTests.cs index e26f63b27c..736b67d01f 100644 --- a/tests/Aspire.Hosting.Nats.Tests/NatsPublicApiTests.cs +++ b/tests/Aspire.Hosting.Nats.Tests/NatsPublicApiTests.cs @@ -9,25 +9,29 @@ namespace Aspire.Hosting.Nats.Tests; public class NatsPublicApiTests { - [Fact] - public void AddNatsContainerShouldThrowWhenBuilderIsNull() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AddNatsContainerShouldThrowWhenBuilderIsNull(bool includePort) { IDistributedApplicationBuilder builder = null!; const string name = "Nats"; - var action = () => builder.AddNats(name); + var action = () => includePort ? builder.AddNats(name, 4222) : builder.AddNats(name); var exception = Assert.Throws(action); Assert.Equal(nameof(builder), exception.ParamName); } - [Fact] - public void AddNatsContainerShouldThrowWhenNameIsNull() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AddNatsContainerShouldThrowWhenNameIsNull(bool includePort) { var builder = TestDistributedApplicationBuilder.Create(); string name = null!; - var action = () => builder.AddNats(name); + var action = () => includePort ? builder.AddNats(name, 4222) : builder.AddNats(name); var exception = Assert.Throws(action); Assert.Equal(nameof(name), exception.ParamName); @@ -90,4 +94,24 @@ public void CtorNatsServerResourceShouldThrowWhenNameIsNull() var exception = Assert.Throws(action); Assert.Equal(nameof(name), exception.ParamName); } + + [Fact] + public void CtorNatsServerResourceWithParametersShouldThrowWhenNameIsNull() + { + string name = null!; + var builder = TestDistributedApplicationBuilder.Create(); + var user = builder.AddParameter("user"); + var password = builder.AddParameter("password"); + + var action = () => new NatsServerResource(name, user.Resource, password.Resource); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(name), exception.ParamName); + } + + [Fact] + public void CtorNatsServerResourceWithParametersShouldAcceptNullParameters() + { + new NatsServerResource("nats", userName: null, password: null); + } } diff --git a/tests/Aspire.NATS.Net.Tests/AspireNatsClientExtensionsTests.cs b/tests/Aspire.NATS.Net.Tests/AspireNatsClientExtensionsTests.cs index 2de664a719..def4cc3934 100644 --- a/tests/Aspire.NATS.Net.Tests/AspireNatsClientExtensionsTests.cs +++ b/tests/Aspire.NATS.Net.Tests/AspireNatsClientExtensionsTests.cs @@ -25,7 +25,7 @@ public AspireNatsClientExtensionsTests(NatsContainerFixture containerFixture) _containerFixture = containerFixture; _connectionString = RequiresDockerAttribute.IsSupported ? _containerFixture.GetConnectionString() - : "nats://apire-host:4222"; + : "nats://aspire-host:4222"; } [Theory] @@ -55,6 +55,35 @@ public void ReadsFromConnectionStringsCorrectly(bool useKeyed) Assert.Equal(_connectionString, connection.Opts.Url); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConnectionStringCanContainUserAndPassword(bool useKeyed) + { + var builder = Host.CreateEmptyApplicationBuilder(null); + builder.Configuration.AddInMemoryCollection([ + new KeyValuePair("ConnectionStrings:nats", "nats://nats:password@aspire-host:4222") + ]); + + if (useKeyed) + { + builder.AddKeyedNatsClient("nats"); + } + else + { + builder.AddNatsClient("nats"); + } + + using var host = builder.Build(); + var connection = useKeyed ? + host.Services.GetRequiredKeyedService("nats") : + host.Services.GetRequiredService(); + + Assert.Equal("nats://nats:password@aspire-host:4222", connection.Opts.Url); + Assert.Equal("nats", connection.Opts.AuthOpts.Username); + Assert.Equal("password", connection.Opts.AuthOpts.Password); + } + [Theory] [InlineData(true)] [InlineData(false)] diff --git a/tests/Aspire.NATS.Net.Tests/ConformanceTests.cs b/tests/Aspire.NATS.Net.Tests/ConformanceTests.cs index be238ca003..aa723c52b5 100644 --- a/tests/Aspire.NATS.Net.Tests/ConformanceTests.cs +++ b/tests/Aspire.NATS.Net.Tests/ConformanceTests.cs @@ -20,7 +20,7 @@ public ConformanceTests(NatsContainerFixture containerFixture) _containerFixture = containerFixture; _connectionString = RequiresDockerAttribute.IsSupported ? _containerFixture.GetConnectionString() - : "nats://apire-host:4222"; + : "nats://user:password@apire-host:4222"; } protected override bool SupportsKeyedRegistrations => true; From 3e8d46c21fbb0434094bb2c59e34f421a18712dc Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 7 Nov 2024 08:02:45 +0800 Subject: [PATCH 35/44] Fix source column displaying executable args (#6619) --- .../SourceColumnDisplay.razor | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Aspire.Dashboard/Components/ResourcesGridColumns/SourceColumnDisplay.razor b/src/Aspire.Dashboard/Components/ResourcesGridColumns/SourceColumnDisplay.razor index a8857ef357..da2539afd0 100644 --- a/src/Aspire.Dashboard/Components/ResourcesGridColumns/SourceColumnDisplay.razor +++ b/src/Aspire.Dashboard/Components/ResourcesGridColumns/SourceColumnDisplay.razor @@ -5,17 +5,18 @@ @inject IStringLocalizer ResourcesLoc + ValueToCopy="@ValueToCopy" + ValueToVisualize="@ValueToCopy" + ValueDescription="@ResourcesLoc[nameof(Resources.ResourcesSourceColumnHeader)]" + EnableHighlighting="true" + HighlightText="@FilterText" + PreCopyToolTip="@Loc[nameof(Columns.SourceColumnDisplayCopyCommandToClipboard)]" + ToolTip="@Tooltip" + StopClickPropagation="true"> @if (ContentAfterValue is not null) { - @ContentAfterValue +  @ContentAfterValue } From 7007cf6986d602ef57b1eef4f704ddd752ea5c99 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 7 Nov 2024 09:45:32 +0800 Subject: [PATCH 36/44] Make filter buttons consistent between pages (#6607) --- .../Components/Pages/Resources.razor | 31 +++++++++---------- .../Components/Pages/StructuredLogs.razor | 4 +-- .../Components/Pages/Traces.razor | 4 +-- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/Aspire.Dashboard/Components/Pages/Resources.razor b/src/Aspire.Dashboard/Components/Pages/Resources.razor index 78ab323fcc..35d8549cfa 100644 --- a/src/Aspire.Dashboard/Components/Pages/Resources.razor +++ b/src/Aspire.Dashboard/Components/Pages/Resources.razor @@ -23,11 +23,19 @@ + + @if (ViewportInformation.IsDesktop) { @@ -37,26 +45,17 @@
@Loc[nameof(Dashboard.Resources.Resources.ResourcesResourceTypesHeader)]
- +
} - -
- +
@Loc[nameof(Dashboard.Resources.Resources.ResourcesResourceTypesHeader)]
@if (ViewportInformation.IsDesktop) { - + } else { - @FilterLoc[nameof(StructuredFiltering.AddFilter)] + @FilterLoc[nameof(StructuredFiltering.AddFilter)] } diff --git a/src/Aspire.Dashboard/Components/Pages/Traces.razor b/src/Aspire.Dashboard/Components/Pages/Traces.razor index 320c8c213c..78efeed555 100644 --- a/src/Aspire.Dashboard/Components/Pages/Traces.razor +++ b/src/Aspire.Dashboard/Components/Pages/Traces.razor @@ -53,12 +53,12 @@ @if (ViewportInformation.IsDesktop) { - + } else { - @FilterLoc[nameof(StructuredFiltering.AddFilter)] + @FilterLoc[nameof(StructuredFiltering.AddFilter)] } From e4ee02e5e93bee6f01951b41be5bd4e90c3e9329 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 7 Nov 2024 09:45:41 +0800 Subject: [PATCH 37/44] Update FluentUI (#6228) --- Directory.Packages.props | 4 ++-- src/Aspire.Dashboard/Components/Layout/MainLayout.razor | 5 +---- src/Aspire.Dashboard/Components/Layout/MainLayout.razor.css | 4 ++-- .../Controls/TextVisualizerDialogTests.cs | 5 +++++ 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 687d56057a..c1cfc9fd3a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -117,8 +117,8 @@ - - + + diff --git a/src/Aspire.Dashboard/Components/Layout/MainLayout.razor b/src/Aspire.Dashboard/Components/Layout/MainLayout.razor index 81c4ab55f5..fe5dc78c97 100644 --- a/src/Aspire.Dashboard/Components/Layout/MainLayout.razor +++ b/src/Aspire.Dashboard/Components/Layout/MainLayout.razor @@ -72,10 +72,7 @@ - @* Temporary work around for https://github.com/microsoft/fluentui-blazor/issues/2736 *@ -
- -
+
@Loc[nameof(Layout.MainLayoutUnhandledErrorMessage)] @Loc[nameof(Layout.MainLayoutUnhandledErrorReload)] diff --git a/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.css b/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.css index 5116923bfa..a06b2cb2e6 100644 --- a/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.css +++ b/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.css @@ -53,7 +53,7 @@ grid-area: head; } - ::deep.layout > .nav-menu-container { + ::deep.layout > .fluent-appbar { grid-area: nav; overflow-y: auto; background: var(--neutral-layer-4); @@ -165,7 +165,7 @@ grid-area: head; } - ::deep.layout > .nav-menu-container { + ::deep.layout > .fluent-appbar { grid-area: nav; overflow-y: auto; background: var(--neutral-layer-4); diff --git a/tests/Aspire.Dashboard.Components.Tests/Controls/TextVisualizerDialogTests.cs b/tests/Aspire.Dashboard.Components.Tests/Controls/TextVisualizerDialogTests.cs index c4b9b3d0c7..5114a9334e 100644 --- a/tests/Aspire.Dashboard.Components.Tests/Controls/TextVisualizerDialogTests.cs +++ b/tests/Aspire.Dashboard.Components.Tests/Controls/TextVisualizerDialogTests.cs @@ -158,6 +158,11 @@ private IRenderedFragment SetUpDialog(out IDialogService dialogService, ThemeMan builder.CloseComponent(); }); + // Setting a provider ID on menu service is required to simulate on the page. + // This makes FluentMenu render without error. + var menuService = Services.GetRequiredService(); + menuService.ProviderId = "Test"; + dialogService = Services.GetRequiredService(); return cut; } From 2e60ee60398402a51f15e17d1115587ed523510e Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Thu, 7 Nov 2024 09:30:43 -0600 Subject: [PATCH 38/44] Remove AWS specific code. (#6624) This code has transferred to https://github.com/aws/integrations-on-dotnet-aspire-for-aws/pull/1. --- .github/dependabot.yml | 5 - Aspire.sln | 45 -- Directory.Packages.props | 13 +- playground/AWS/AWS.AppHost/AWS.AppHost.csproj | 23 - playground/AWS/AWS.AppHost/Program.cs | 34 -- .../Properties/launchSettings.json | 42 -- .../AWS/AWS.AppHost/app-resources.template | 59 -- playground/AWS/AWS.AppHost/appsettings.json | 9 - .../AWS/AWS.AppHost/aspire-manifest.json | 41 -- .../AWS.ServiceDefaults.csproj | 27 - .../AWS/AWS.ServiceDefaults/Extensions.cs | 113 ---- playground/AWS/AWSCDK.AppHost/.gitignore | 2 - .../AWS/AWSCDK.AppHost/AWSCDK.AppHost.csproj | 23 - playground/AWS/AWSCDK.AppHost/CustomStack.cs | 25 - playground/AWS/AWSCDK.AppHost/Program.cs | 25 - .../Properties/launchSettings.json | 41 -- playground/AWS/AWSCDK.AppHost/WebAppStack.cs | 26 - .../AWS/AWSCDK.AppHost/aspire-manifest.json | 50 -- playground/AWS/AWSCDK.AppHost/cdk.json | 63 --- playground/AWS/Frontend/Components/App.razor | 20 - .../Components/Layout/MainLayout.razor | 19 - .../Components/Layout/MainLayout.razor.css | 96 ---- .../Frontend/Components/Layout/NavMenu.razor | 30 -- .../Components/Layout/NavMenu.razor.css | 105 ---- .../Pages/AppHostConfiguration.razor | 34 -- .../AWS/Frontend/Components/Pages/Error.razor | 36 -- .../AWS/Frontend/Components/Pages/Home.razor | 11 - .../Components/Pages/MessagePublisher.razor | 69 --- .../AWS/Frontend/Components/Routes.razor | 6 - .../AWS/Frontend/Components/_Imports.razor | 10 - playground/AWS/Frontend/Frontend.csproj | 17 - playground/AWS/Frontend/Models/ChatMessage.cs | 11 - playground/AWS/Frontend/Program.cs | 45 -- .../Frontend/Properties/launchSettings.json | 38 -- .../AWS/Frontend/appsettings.Development.json | 8 - playground/AWS/Frontend/appsettings.json | 9 - playground/AWS/Frontend/wwwroot/app.css | 51 -- .../wwwroot/bootstrap/bootstrap.min.css | 7 - .../wwwroot/bootstrap/bootstrap.min.css.map | 1 - playground/AWS/Frontend/wwwroot/favicon.png | Bin 1148 -> 0 bytes src/Aspire.Hosting.AWS/AWSLifecycleHook.cs | 178 ------ src/Aspire.Hosting.AWS/AWSSDKConfig.cs | 15 - .../Aspire.Hosting.AWS.csproj | 30 -- src/Aspire.Hosting.AWS/CDK/CDKExtensions.cs | 238 -------- .../CDK/CloudAssemblyResourceAnnotation.cs | 18 - .../CDK/ConstructBuilderDelegate.cs | 12 - .../CDK/ConstructOutputAnnotation.cs | 31 -- .../CDK/ConstructOutputDelegate.cs | 11 - .../CDK/ConstructReferenceAnnotation.cs | 26 - .../CDK/ConstructResource.cs | 24 - .../CDK/IConstructModifierAnnotation.cs | 25 - .../CDK/IConstructOutputAnnotation.cs | 17 - .../CDK/IConstructResource.cs | 11 - .../CDK/IConstructResourceOfT.cs | 9 - .../CDK/IResourceWithConstruct.cs | 18 - .../CDK/IResourceWithConstructOfT.cs | 14 - src/Aspire.Hosting.AWS/CDK/IStackResource.cs | 18 - .../CDK/IStackResourceOfT.cs | 16 - .../Resources/CognitoResourceExtensions.cs | 54 -- .../Resources/DynamoDBResourceExtensions.cs | 65 --- .../Resources/KinesisResourceExtensions.cs | 44 -- .../CDK/Resources/S3ResourceExtensions.cs | 97 ---- .../CDK/Resources/SNSResourceExtensions.cs | 55 -- .../CDK/Resources/SQSResourceExtensions.cs | 43 -- src/Aspire.Hosting.AWS/CDK/StackResource.cs | 32 -- .../CDK/Utils/ConstructExtensions.cs | 37 -- .../CDK/Utils/ResourceExtensions.cs | 28 - .../CloudFormationExtensions.cs | 194 ------- .../CloudFormationReferenceAnnotation.cs | 20 - .../CloudFormation/CloudFormationResource.cs | 29 - .../CloudFormationStackResource.cs | 29 - .../CloudFormationTemplateResource.cs | 52 -- .../CloudFormation/ICloudFormationResource.cs | 29 - .../ICloudFormationStackResource.cs | 9 - .../ICloudFormationTemplateResource.cs | 48 -- .../CloudFormation/StackOutputReference.cs | 62 --- src/Aspire.Hosting.AWS/Constants.cs | 27 - src/Aspire.Hosting.AWS/IAWSResource.cs | 22 - src/Aspire.Hosting.AWS/IAWSSDKConfig.cs | 40 -- .../Provisioning/AWSProvisionerExtensions.cs | 38 -- .../Provisioning/AWSProvisioningException.cs | 13 - .../Provisioning/AWSResourceProvisionerOfT.cs | 19 - .../CDKStackResourceProvisioner.cs | 53 -- .../CloudFormationResourceProvisioner.cs | 117 ---- .../CloudFormationStackExecutionContext.cs | 23 - .../CloudFormationStackExecutor.cs | 507 ------------------ .../CloudFormationStackResourceProvisioner.cs | 50 -- ...oudFormationTemplateResourceProvisioner.cs | 85 --- src/Aspire.Hosting.AWS/PublicAPI.Shipped.txt | 1 - .../PublicAPI.Unshipped.txt | 99 ---- src/Aspire.Hosting.AWS/README.md | 162 ------ .../SDKResourceExtensions.cs | 71 --- src/Aspire.Hosting.AWS/SdkUtilities.cs | 70 --- .../Utils/ResourceExtensions.cs | 39 -- .../Utils/StringExtensions.cs | 41 -- .../AWSCDKResourceTests.cs | 138 ----- .../AWSCloudFormationResourceTests.cs | 122 ----- .../AWSSchemaTests.cs | 39 -- .../Aspire.Hosting.AWS.Tests.csproj | 12 - .../CloudFormationAWSConsoleUrlTests.cs | 70 --- .../StackOutputReferenceTests.cs | 78 --- .../Directory.Packages.Helix.props | 1 - 102 files changed, 1 insertion(+), 4863 deletions(-) delete mode 100644 playground/AWS/AWS.AppHost/AWS.AppHost.csproj delete mode 100644 playground/AWS/AWS.AppHost/Program.cs delete mode 100644 playground/AWS/AWS.AppHost/Properties/launchSettings.json delete mode 100644 playground/AWS/AWS.AppHost/app-resources.template delete mode 100644 playground/AWS/AWS.AppHost/appsettings.json delete mode 100644 playground/AWS/AWS.AppHost/aspire-manifest.json delete mode 100644 playground/AWS/AWS.ServiceDefaults/AWS.ServiceDefaults.csproj delete mode 100644 playground/AWS/AWS.ServiceDefaults/Extensions.cs delete mode 100644 playground/AWS/AWSCDK.AppHost/.gitignore delete mode 100644 playground/AWS/AWSCDK.AppHost/AWSCDK.AppHost.csproj delete mode 100644 playground/AWS/AWSCDK.AppHost/CustomStack.cs delete mode 100644 playground/AWS/AWSCDK.AppHost/Program.cs delete mode 100644 playground/AWS/AWSCDK.AppHost/Properties/launchSettings.json delete mode 100644 playground/AWS/AWSCDK.AppHost/WebAppStack.cs delete mode 100644 playground/AWS/AWSCDK.AppHost/aspire-manifest.json delete mode 100644 playground/AWS/AWSCDK.AppHost/cdk.json delete mode 100644 playground/AWS/Frontend/Components/App.razor delete mode 100644 playground/AWS/Frontend/Components/Layout/MainLayout.razor delete mode 100644 playground/AWS/Frontend/Components/Layout/MainLayout.razor.css delete mode 100644 playground/AWS/Frontend/Components/Layout/NavMenu.razor delete mode 100644 playground/AWS/Frontend/Components/Layout/NavMenu.razor.css delete mode 100644 playground/AWS/Frontend/Components/Pages/AppHostConfiguration.razor delete mode 100644 playground/AWS/Frontend/Components/Pages/Error.razor delete mode 100644 playground/AWS/Frontend/Components/Pages/Home.razor delete mode 100644 playground/AWS/Frontend/Components/Pages/MessagePublisher.razor delete mode 100644 playground/AWS/Frontend/Components/Routes.razor delete mode 100644 playground/AWS/Frontend/Components/_Imports.razor delete mode 100644 playground/AWS/Frontend/Frontend.csproj delete mode 100644 playground/AWS/Frontend/Models/ChatMessage.cs delete mode 100644 playground/AWS/Frontend/Program.cs delete mode 100644 playground/AWS/Frontend/Properties/launchSettings.json delete mode 100644 playground/AWS/Frontend/appsettings.Development.json delete mode 100644 playground/AWS/Frontend/appsettings.json delete mode 100644 playground/AWS/Frontend/wwwroot/app.css delete mode 100644 playground/AWS/Frontend/wwwroot/bootstrap/bootstrap.min.css delete mode 100644 playground/AWS/Frontend/wwwroot/bootstrap/bootstrap.min.css.map delete mode 100644 playground/AWS/Frontend/wwwroot/favicon.png delete mode 100644 src/Aspire.Hosting.AWS/AWSLifecycleHook.cs delete mode 100644 src/Aspire.Hosting.AWS/AWSSDKConfig.cs delete mode 100644 src/Aspire.Hosting.AWS/Aspire.Hosting.AWS.csproj delete mode 100644 src/Aspire.Hosting.AWS/CDK/CDKExtensions.cs delete mode 100644 src/Aspire.Hosting.AWS/CDK/CloudAssemblyResourceAnnotation.cs delete mode 100644 src/Aspire.Hosting.AWS/CDK/ConstructBuilderDelegate.cs delete mode 100644 src/Aspire.Hosting.AWS/CDK/ConstructOutputAnnotation.cs delete mode 100644 src/Aspire.Hosting.AWS/CDK/ConstructOutputDelegate.cs delete mode 100644 src/Aspire.Hosting.AWS/CDK/ConstructReferenceAnnotation.cs delete mode 100644 src/Aspire.Hosting.AWS/CDK/ConstructResource.cs delete mode 100644 src/Aspire.Hosting.AWS/CDK/IConstructModifierAnnotation.cs delete mode 100644 src/Aspire.Hosting.AWS/CDK/IConstructOutputAnnotation.cs delete mode 100644 src/Aspire.Hosting.AWS/CDK/IConstructResource.cs delete mode 100644 src/Aspire.Hosting.AWS/CDK/IConstructResourceOfT.cs delete mode 100644 src/Aspire.Hosting.AWS/CDK/IResourceWithConstruct.cs delete mode 100644 src/Aspire.Hosting.AWS/CDK/IResourceWithConstructOfT.cs delete mode 100644 src/Aspire.Hosting.AWS/CDK/IStackResource.cs delete mode 100644 src/Aspire.Hosting.AWS/CDK/IStackResourceOfT.cs delete mode 100644 src/Aspire.Hosting.AWS/CDK/Resources/CognitoResourceExtensions.cs delete mode 100644 src/Aspire.Hosting.AWS/CDK/Resources/DynamoDBResourceExtensions.cs delete mode 100644 src/Aspire.Hosting.AWS/CDK/Resources/KinesisResourceExtensions.cs delete mode 100644 src/Aspire.Hosting.AWS/CDK/Resources/S3ResourceExtensions.cs delete mode 100644 src/Aspire.Hosting.AWS/CDK/Resources/SNSResourceExtensions.cs delete mode 100644 src/Aspire.Hosting.AWS/CDK/Resources/SQSResourceExtensions.cs delete mode 100644 src/Aspire.Hosting.AWS/CDK/StackResource.cs delete mode 100644 src/Aspire.Hosting.AWS/CDK/Utils/ConstructExtensions.cs delete mode 100644 src/Aspire.Hosting.AWS/CDK/Utils/ResourceExtensions.cs delete mode 100644 src/Aspire.Hosting.AWS/CloudFormation/CloudFormationExtensions.cs delete mode 100644 src/Aspire.Hosting.AWS/CloudFormation/CloudFormationReferenceAnnotation.cs delete mode 100644 src/Aspire.Hosting.AWS/CloudFormation/CloudFormationResource.cs delete mode 100644 src/Aspire.Hosting.AWS/CloudFormation/CloudFormationStackResource.cs delete mode 100644 src/Aspire.Hosting.AWS/CloudFormation/CloudFormationTemplateResource.cs delete mode 100644 src/Aspire.Hosting.AWS/CloudFormation/ICloudFormationResource.cs delete mode 100644 src/Aspire.Hosting.AWS/CloudFormation/ICloudFormationStackResource.cs delete mode 100644 src/Aspire.Hosting.AWS/CloudFormation/ICloudFormationTemplateResource.cs delete mode 100644 src/Aspire.Hosting.AWS/CloudFormation/StackOutputReference.cs delete mode 100644 src/Aspire.Hosting.AWS/Constants.cs delete mode 100644 src/Aspire.Hosting.AWS/IAWSResource.cs delete mode 100644 src/Aspire.Hosting.AWS/IAWSSDKConfig.cs delete mode 100644 src/Aspire.Hosting.AWS/Provisioning/AWSProvisionerExtensions.cs delete mode 100644 src/Aspire.Hosting.AWS/Provisioning/AWSProvisioningException.cs delete mode 100644 src/Aspire.Hosting.AWS/Provisioning/AWSResourceProvisionerOfT.cs delete mode 100644 src/Aspire.Hosting.AWS/Provisioning/CDKStackResourceProvisioner.cs delete mode 100644 src/Aspire.Hosting.AWS/Provisioning/CloudFormationResourceProvisioner.cs delete mode 100644 src/Aspire.Hosting.AWS/Provisioning/CloudFormationStackExecutionContext.cs delete mode 100644 src/Aspire.Hosting.AWS/Provisioning/CloudFormationStackExecutor.cs delete mode 100644 src/Aspire.Hosting.AWS/Provisioning/CloudFormationStackResourceProvisioner.cs delete mode 100644 src/Aspire.Hosting.AWS/Provisioning/CloudFormationTemplateResourceProvisioner.cs delete mode 100644 src/Aspire.Hosting.AWS/PublicAPI.Shipped.txt delete mode 100644 src/Aspire.Hosting.AWS/PublicAPI.Unshipped.txt delete mode 100644 src/Aspire.Hosting.AWS/README.md delete mode 100644 src/Aspire.Hosting.AWS/SDKResourceExtensions.cs delete mode 100644 src/Aspire.Hosting.AWS/SdkUtilities.cs delete mode 100644 src/Aspire.Hosting.AWS/Utils/ResourceExtensions.cs delete mode 100644 src/Aspire.Hosting.AWS/Utils/StringExtensions.cs delete mode 100644 tests/Aspire.Hosting.AWS.Tests/AWSCDKResourceTests.cs delete mode 100644 tests/Aspire.Hosting.AWS.Tests/AWSCloudFormationResourceTests.cs delete mode 100644 tests/Aspire.Hosting.AWS.Tests/AWSSchemaTests.cs delete mode 100644 tests/Aspire.Hosting.AWS.Tests/Aspire.Hosting.AWS.Tests.csproj delete mode 100644 tests/Aspire.Hosting.AWS.Tests/CloudFormationAWSConsoleUrlTests.cs delete mode 100644 tests/Aspire.Hosting.AWS.Tests/StackOutputReferenceTests.cs diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2adb17b448..cf1a445ca4 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -31,11 +31,6 @@ updates: AspNetCoreHealthChecks: patterns: - "AspNetCore.HealthChecks.*" - AWS: - patterns: - - "AWS.*" - - "AWSSDK.*" - - "OpenTelemetry.*.AWS" EntityFrameworkCore: patterns: - "Microsoft.EntityFrameworkCore.*" diff --git a/Aspire.sln b/Aspire.sln index e3d7b7a579..52a3a75f81 100644 --- a/Aspire.sln +++ b/Aspire.sln @@ -171,16 +171,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.NATS.Net", "src\Comp EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{2136E31D-2CBB-41BB-8618-716FF8E46E9E}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AWS", "AWS", "{EF91843F-C4AB-47F8-909B-C494EABB2BA2}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AWS.AppHost", "playground\AWS\AWS.AppHost\AWS.AppHost.csproj", "{5FB59EA4-5D5D-4942-BF00-F034F57A146A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AWS.ServiceDefaults", "playground\AWS\AWS.ServiceDefaults\AWS.ServiceDefaults.csproj", "{2E54D01B-87AD-460E-A773-75C6DF50F30C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Frontend", "playground\AWS\Frontend\Frontend.csproj", "{A1662C12-DB8B-4C68-94EA-101860072BB1}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.AWS", "src\Aspire.Hosting.AWS\Aspire.Hosting.AWS.csproj", "{3112D63C-A9E3-44F8-92A5-8F3052627312}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.MongoDB.Driver", "src\Components\Aspire.MongoDB.Driver\Aspire.MongoDB.Driver.csproj", "{20A5A907-A135-4735-B4BF-E13514F360E3}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConfigurationSchemaGenerator", "src\Tools\ConfigurationSchemaGenerator\ConfigurationSchemaGenerator.csproj", "{39FA2A64-012F-4EB9-A14F-E8AC54C975F6}" @@ -583,8 +573,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BrowserTelemetry.Web", "pla EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Dapr.Tests", "tests\Aspire.Hosting.Dapr.Tests\Aspire.Hosting.Dapr.Tests.csproj", "{C60C5CFA-5B6D-4432-BFCD-54D1BEEC7DBE}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.AWS.Tests", "tests\Aspire.Hosting.AWS.Tests\Aspire.Hosting.AWS.Tests.csproj", "{6F71BC73-B703-4E64-98E0-801781302E7A}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AzureFunctionsEndToEnd", "AzureFunctionsEndToEnd", "{305D5B56-8782-493C-BD44-9860F8616D92}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureFunctionsEndToEnd.ApiService", "playground\AzureFunctionsEndToEnd\AzureFunctionsEndToEnd.ApiService\AzureFunctionsEndToEnd.ApiService.csproj", "{659AF918-57A4-4616-B3D0-24FCE38DF12A}" @@ -613,8 +601,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SqlServerEndToEnd.Common", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SqlServerEndToEnd.DbSetup", "playground\SqlServerEndToEnd\SqlServerEndToEnd.DbSetup\SqlServerEndToEnd.DbSetup.csproj", "{125C081D-7E5B-4F35-B5CD-E2B56140380F}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AWSCDK.AppHost", "playground\AWS\AWSCDK.AppHost\AWSCDK.AppHost.csproj", "{B5826732-7318-4179-9B85-870CB18AC533}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Redis", "Redis", "{874EA351-05EA-44F5-8B12-7A7F865ECEC5}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Redis.ApiService", "playground\Redis\Redis.ApiService\Redis.ApiService.csproj", "{B4CB2D9D-D3F5-4BB1-A7C0-7A6F13316A78}" @@ -907,22 +893,6 @@ Global {303E6308-740C-4673-96D2-0E81934C9E69}.Debug|Any CPU.Build.0 = Debug|Any CPU {303E6308-740C-4673-96D2-0E81934C9E69}.Release|Any CPU.ActiveCfg = Release|Any CPU {303E6308-740C-4673-96D2-0E81934C9E69}.Release|Any CPU.Build.0 = Release|Any CPU - {5FB59EA4-5D5D-4942-BF00-F034F57A146A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5FB59EA4-5D5D-4942-BF00-F034F57A146A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5FB59EA4-5D5D-4942-BF00-F034F57A146A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5FB59EA4-5D5D-4942-BF00-F034F57A146A}.Release|Any CPU.Build.0 = Release|Any CPU - {2E54D01B-87AD-460E-A773-75C6DF50F30C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2E54D01B-87AD-460E-A773-75C6DF50F30C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2E54D01B-87AD-460E-A773-75C6DF50F30C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2E54D01B-87AD-460E-A773-75C6DF50F30C}.Release|Any CPU.Build.0 = Release|Any CPU - {A1662C12-DB8B-4C68-94EA-101860072BB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A1662C12-DB8B-4C68-94EA-101860072BB1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A1662C12-DB8B-4C68-94EA-101860072BB1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A1662C12-DB8B-4C68-94EA-101860072BB1}.Release|Any CPU.Build.0 = Release|Any CPU - {3112D63C-A9E3-44F8-92A5-8F3052627312}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3112D63C-A9E3-44F8-92A5-8F3052627312}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3112D63C-A9E3-44F8-92A5-8F3052627312}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3112D63C-A9E3-44F8-92A5-8F3052627312}.Release|Any CPU.Build.0 = Release|Any CPU {20A5A907-A135-4735-B4BF-E13514F360E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {20A5A907-A135-4735-B4BF-E13514F360E3}.Debug|Any CPU.Build.0 = Debug|Any CPU {20A5A907-A135-4735-B4BF-E13514F360E3}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -1591,10 +1561,6 @@ Global {C60C5CFA-5B6D-4432-BFCD-54D1BEEC7DBE}.Debug|Any CPU.Build.0 = Debug|Any CPU {C60C5CFA-5B6D-4432-BFCD-54D1BEEC7DBE}.Release|Any CPU.ActiveCfg = Release|Any CPU {C60C5CFA-5B6D-4432-BFCD-54D1BEEC7DBE}.Release|Any CPU.Build.0 = Release|Any CPU - {6F71BC73-B703-4E64-98E0-801781302E7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6F71BC73-B703-4E64-98E0-801781302E7A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6F71BC73-B703-4E64-98E0-801781302E7A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6F71BC73-B703-4E64-98E0-801781302E7A}.Release|Any CPU.Build.0 = Release|Any CPU {659AF918-57A4-4616-B3D0-24FCE38DF12A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {659AF918-57A4-4616-B3D0-24FCE38DF12A}.Debug|Any CPU.Build.0 = Debug|Any CPU {659AF918-57A4-4616-B3D0-24FCE38DF12A}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -1643,10 +1609,6 @@ Global {125C081D-7E5B-4F35-B5CD-E2B56140380F}.Debug|Any CPU.Build.0 = Debug|Any CPU {125C081D-7E5B-4F35-B5CD-E2B56140380F}.Release|Any CPU.ActiveCfg = Release|Any CPU {125C081D-7E5B-4F35-B5CD-E2B56140380F}.Release|Any CPU.Build.0 = Release|Any CPU - {B5826732-7318-4179-9B85-870CB18AC533}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B5826732-7318-4179-9B85-870CB18AC533}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B5826732-7318-4179-9B85-870CB18AC533}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B5826732-7318-4179-9B85-870CB18AC533}.Release|Any CPU.Build.0 = Release|Any CPU {B4CB2D9D-D3F5-4BB1-A7C0-7A6F13316A78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B4CB2D9D-D3F5-4BB1-A7C0-7A6F13316A78}.Debug|Any CPU.Build.0 = Debug|Any CPU {B4CB2D9D-D3F5-4BB1-A7C0-7A6F13316A78}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -1765,11 +1727,6 @@ Global {74F89D52-645B-4283-98AF-D04DA794EE37} = {91F22EEA-EB23-425A-9B32-9438A0809F4B} {A0BB6F6B-9543-4525-81D6-95DC07BD3D02} = {91F22EEA-EB23-425A-9B32-9438A0809F4B} {303E6308-740C-4673-96D2-0E81934C9E69} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2} - {EF91843F-C4AB-47F8-909B-C494EABB2BA2} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0} - {5FB59EA4-5D5D-4942-BF00-F034F57A146A} = {EF91843F-C4AB-47F8-909B-C494EABB2BA2} - {2E54D01B-87AD-460E-A773-75C6DF50F30C} = {EF91843F-C4AB-47F8-909B-C494EABB2BA2} - {A1662C12-DB8B-4C68-94EA-101860072BB1} = {EF91843F-C4AB-47F8-909B-C494EABB2BA2} - {3112D63C-A9E3-44F8-92A5-8F3052627312} = {B80354C7-BE58-43F6-8928-9F3A74AB7F47} {20A5A907-A135-4735-B4BF-E13514F360E3} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2} {39FA2A64-012F-4EB9-A14F-E8AC54C975F6} = {2136E31D-2CBB-41BB-8618-716FF8E46E9E} {E592E447-BA3C-44FA-86C1-EBEDC864A644} = {C424395C-1235-41A4-BF55-07880A04368C} @@ -1970,7 +1927,6 @@ Global {29946391-F2E9-4DEA-92A2-6FAAC1508446} = {15966C27-17FA-4A46-A172-55985411540A} {091EA540-355B-4763-9980-5F83F0BB6F11} = {15966C27-17FA-4A46-A172-55985411540A} {C60C5CFA-5B6D-4432-BFCD-54D1BEEC7DBE} = {830A89EC-4029-4753-B25A-068BAE37DEC7} - {6F71BC73-B703-4E64-98E0-801781302E7A} = {830A89EC-4029-4753-B25A-068BAE37DEC7} {305D5B56-8782-493C-BD44-9860F8616D92} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0} {659AF918-57A4-4616-B3D0-24FCE38DF12A} = {305D5B56-8782-493C-BD44-9860F8616D92} {13025E2D-2E2B-4319-8754-0B12F324283E} = {305D5B56-8782-493C-BD44-9860F8616D92} @@ -1985,7 +1941,6 @@ Global {C7B77A8D-719E-479E-A063-08A3859B713E} = {C424395C-1235-41A4-BF55-07880A04368C} {1997067D-8EF2-43B3-AB13-9B2E12B52709} = {2CA6AB88-21EF-4488-BB1B-3A5BAD5FE2AD} {125C081D-7E5B-4F35-B5CD-E2B56140380F} = {2CA6AB88-21EF-4488-BB1B-3A5BAD5FE2AD} - {B5826732-7318-4179-9B85-870CB18AC533} = {EF91843F-C4AB-47F8-909B-C494EABB2BA2} {874EA351-05EA-44F5-8B12-7A7F865ECEC5} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0} {B4CB2D9D-D3F5-4BB1-A7C0-7A6F13316A78} = {874EA351-05EA-44F5-8B12-7A7F865ECEC5} {6249A193-3BF4-4FFB-AB81-6590A8318889} = {874EA351-05EA-44F5-8B12-7A7F865ECEC5} diff --git a/Directory.Packages.props b/Directory.Packages.props index c1cfc9fd3a..0cb800b5a6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,18 +10,6 @@ 1.0.0 - - - - - - - - - - - - @@ -81,6 +69,7 @@ + diff --git a/playground/AWS/AWS.AppHost/AWS.AppHost.csproj b/playground/AWS/AWS.AppHost/AWS.AppHost.csproj deleted file mode 100644 index 722ea3617b..0000000000 --- a/playground/AWS/AWS.AppHost/AWS.AppHost.csproj +++ /dev/null @@ -1,23 +0,0 @@ - - - - Exe - $(DefaultTargetFramework) - enable - enable - true - $(NoWarn);CS8002 - - - - - - - - - - - - - - diff --git a/playground/AWS/AWS.AppHost/Program.cs b/playground/AWS/AWS.AppHost/Program.cs deleted file mode 100644 index 3a1c5736e4..0000000000 --- a/playground/AWS/AWS.AppHost/Program.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -using Amazon; - -var builder = DistributedApplication.CreateBuilder(args); - -// Setup a configuration for the AWS .NET SDK. -var awsConfig = builder.AddAWSSDKConfig() - .WithProfile("default") - .WithRegion(RegionEndpoint.USWest2); - -// Provision application level resources like SQS queues and SNS topics defined in the CloudFormation template file app-resources.template. -var awsResources = builder.AddAWSCloudFormationTemplate("AspireSampleDevResources", "app-resources.template") - .WithParameter("DefaultVisibilityTimeout", "30") - // Add the SDK configuration so the AppHost knows what account/region to provision the resources. - .WithReference(awsConfig); - -// To add outputs of a CloudFormation stack that was created outside of AppHost use the AddAWSCloudFormationStack method. -// then attach the CloudFormation resource to a project using the WithReference method. -//var awsExistingResource = builder.AddAWSCloudFormationStack("ExistingStackName") -// .WithReference(awsConfig); - -// The AWS SDK Config reference is inferred from the CloudFormation resource associated to the project. If the -// project doesn't have a CloudFormation resource the AWS SDK Config reference can be assigned using the -// WithReference method. -builder.AddProject("Frontend") - .WithExternalHttpEndpoints() - // Demonstrating binding all of the output variables to a section in IConfiguration. By default they are bound to the AWS::Resources prefix. - // The prefix is configurable by the optional configSection parameter. - .WithReference(awsResources) - // Demonstrating binding a single output variable to environment variable in the project. - .WithEnvironment("ChatTopicArnEnv", awsResources.GetOutput("ChatTopicArn")); - -builder.Build().Run(); diff --git a/playground/AWS/AWS.AppHost/Properties/launchSettings.json b/playground/AWS/AWS.AppHost/Properties/launchSettings.json deleted file mode 100644 index 0b4ade4882..0000000000 --- a/playground/AWS/AWS.AppHost/Properties/launchSettings.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "profiles": { - "https": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "https://localhost:15068;http://localhost:15069", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "DOTNET_ENVIRONMENT": "Development", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:16216", - "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:17037" - } - }, - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "http://localhost:15069", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "DOTNET_ENVIRONMENT": "Development", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16216", - "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:17038", - "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true" - } - }, - "generate-manifest": { - "commandName": "Project", - "commandLineArgs": "--publisher manifest --output-path ./aspire-manifest.json", - "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "http://localhost:15069", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "DOTNET_ENVIRONMENT": "Development", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16216" - } - } - } -} diff --git a/playground/AWS/AWS.AppHost/app-resources.template b/playground/AWS/AWS.AppHost/app-resources.template deleted file mode 100644 index dfea808518..0000000000 --- a/playground/AWS/AWS.AppHost/app-resources.template +++ /dev/null @@ -1,59 +0,0 @@ -{ - "AWSTemplateFormatVersion" : "2010-09-09", - "Parameters" : { - "DefaultVisibilityTimeout" : { - "Type" : "Number", - "Description" : "The default visibility timeout for messages in SQS queue." - } - }, - "Resources" : { - "ChatMessagesQueue" : { - "Type" : "AWS::SQS::Queue", - "Properties" : { - "VisibilityTimeout" : { "Ref" : "DefaultVisibilityTimeout" } - } - }, - "ChatTopic" : { - "Type" : "AWS::SNS::Topic", - "Properties" : { - "Subscription" : [ - {"Protocol" : "sqs", "Endpoint" : {"Fn::GetAtt" : [ "ChatMessagesQueue", "Arn"]}} - ] - } - }, - "ChatMessagesQueuePolicy": { - "Type": "AWS::SQS::QueuePolicy", - "Properties": { - "Queues": [ - { "Ref": "ChatMessagesQueue" } - ], - "PolicyDocument": { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": "sqs:SendMessage", - "Principal": { - "Service": "sns.amazonaws.com" - }, - "Resource": { "Fn::GetAtt": [ "ChatMessagesQueue", "Arn" ] }, - "Condition": { - "ArnEquals": { - "aws:SourceArn": { "Ref": "ChatTopic" } - } - } - } - ] - } - } - } - }, - "Outputs" : { - "ChatMessagesQueueUrl" : { - "Value" : { "Ref" : "ChatMessagesQueue" } - }, - "ChatTopicArn" : { - "Value" : { "Ref" : "ChatTopic" } - } - } -} diff --git a/playground/AWS/AWS.AppHost/appsettings.json b/playground/AWS/AWS.AppHost/appsettings.json deleted file mode 100644 index 31c092aa45..0000000000 --- a/playground/AWS/AWS.AppHost/appsettings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning", - "Aspire.Hosting.Dcp": "Warning" - } - } -} diff --git a/playground/AWS/AWS.AppHost/aspire-manifest.json b/playground/AWS/AWS.AppHost/aspire-manifest.json deleted file mode 100644 index 004953b398..0000000000 --- a/playground/AWS/AWS.AppHost/aspire-manifest.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/aspire-8.0.json", - "resources": { - "AspireSampleDevResources": { - "type": "aws.cloudformation.template.v0", - "stack-name": "AspireSampleDevResources", - "template-path": "app-resources.template", - "references": [ - { - "target-resource": "Frontend" - } - ] - }, - "Frontend": { - "type": "project.v0", - "path": "../Frontend/Frontend.csproj", - "env": { - "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true", - "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true", - "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory", - "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true", - "HTTP_PORTS": "{Frontend.bindings.http.targetPort}", - "ChatTopicArnEnv": "{AspireSampleDevResources.output.ChatTopicArn}" - }, - "bindings": { - "http": { - "scheme": "http", - "protocol": "tcp", - "transport": "http", - "external": true - }, - "https": { - "scheme": "https", - "protocol": "tcp", - "transport": "http", - "external": true - } - } - } - } -} \ No newline at end of file diff --git a/playground/AWS/AWS.ServiceDefaults/AWS.ServiceDefaults.csproj b/playground/AWS/AWS.ServiceDefaults/AWS.ServiceDefaults.csproj deleted file mode 100644 index 8569877c1b..0000000000 --- a/playground/AWS/AWS.ServiceDefaults/AWS.ServiceDefaults.csproj +++ /dev/null @@ -1,27 +0,0 @@ - - - - Library - $(DefaultTargetFramework) - - - - - - - - - - - - - - - - - - - - - - diff --git a/playground/AWS/AWS.ServiceDefaults/Extensions.cs b/playground/AWS/AWS.ServiceDefaults/Extensions.cs deleted file mode 100644 index f36f076e54..0000000000 --- a/playground/AWS/AWS.ServiceDefaults/Extensions.cs +++ /dev/null @@ -1,113 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Diagnostics.HealthChecks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Diagnostics.HealthChecks; -using Microsoft.Extensions.Logging; -using OpenTelemetry; -using OpenTelemetry.Metrics; -using OpenTelemetry.Trace; - -namespace Microsoft.Extensions.Hosting; - -// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. -// This project should be referenced by each service project in your solution. -// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults -public static class Extensions -{ - public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder - { - builder.ConfigureOpenTelemetry(); - - builder.AddDefaultHealthChecks(); - - builder.Services.AddServiceDiscovery(); - - builder.Services.ConfigureHttpClientDefaults(http => - { - // Turn on resilience by default - http.AddStandardResilienceHandler(); - - // Turn on service discovery by default - http.AddServiceDiscovery(); - }); - - return builder; - } - - public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder - { - builder.Logging.AddOpenTelemetry(logging => - { - logging.IncludeFormattedMessage = true; - logging.IncludeScopes = true; - }); - - builder.Services.AddOpenTelemetry() - .WithMetrics(metrics => - { - metrics.AddAspNetCoreInstrumentation() - .AddHttpClientInstrumentation() - .AddRuntimeInstrumentation(); - }) - .WithTracing(tracing => - { - tracing.AddAspNetCoreInstrumentation() - // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) - //.AddGrpcClientInstrumentation() - .AddHttpClientInstrumentation() - // Add instrumentation for the AWS .NET SDK. - .AddAWSInstrumentation(); - }); - - builder.AddOpenTelemetryExporters(); - - return builder; - } - - private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder - { - var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); - - if (useOtlpExporter) - { - builder.Services.AddOpenTelemetry().UseOtlpExporter(); - } - - // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) - //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) - //{ - // builder.Services.AddOpenTelemetry() - // .UseAzureMonitor(); - //} - - return builder; - } - - public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder - { - builder.Services.AddHealthChecks() - // Add a default liveness check to ensure app is responsive - .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); - - return builder; - } - - public static WebApplication MapDefaultEndpoints(this WebApplication app) - { - // Adding health checks endpoints to applications in non-development environments has security implications. - // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. - if (app.Environment.IsDevelopment()) - { - // All health checks must pass for app to be considered ready to accept traffic after starting - app.MapHealthChecks("/health"); - - // Only health checks tagged with the "live" tag must pass for app to be considered alive - app.MapHealthChecks("/alive", new HealthCheckOptions - { - Predicate = r => r.Tags.Contains("live") - }); - } - - return app; - } -} diff --git a/playground/AWS/AWSCDK.AppHost/.gitignore b/playground/AWS/AWSCDK.AppHost/.gitignore deleted file mode 100644 index 7c884c3824..0000000000 --- a/playground/AWS/AWSCDK.AppHost/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# AWS CDK -cdk.out \ No newline at end of file diff --git a/playground/AWS/AWSCDK.AppHost/AWSCDK.AppHost.csproj b/playground/AWS/AWSCDK.AppHost/AWSCDK.AppHost.csproj deleted file mode 100644 index c8d0246d7a..0000000000 --- a/playground/AWS/AWSCDK.AppHost/AWSCDK.AppHost.csproj +++ /dev/null @@ -1,23 +0,0 @@ - - - - Exe - net8.0 - enable - enable - true - $(NoWarn);CS8002 - - - - - - - - - - - - - - diff --git a/playground/AWS/AWSCDK.AppHost/CustomStack.cs b/playground/AWS/AWSCDK.AppHost/CustomStack.cs deleted file mode 100644 index efaa9ae15f..0000000000 --- a/playground/AWS/AWSCDK.AppHost/CustomStack.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Amazon.CDK; -using Amazon.CDK.AWS.S3; -using Amazon.CDK.AWS.SQS; -using Constructs; - -namespace AWSCDK.AppHost; - -public class CustomStack : Stack -{ - - public IBucket Bucket { get; } - - public IQueue Queue { get; } - - public CustomStack(Construct scope, string id) - : base(scope, id) - { - Bucket = new Bucket(this, "Bucket"); - Queue = new Queue(this, "Queue"); - } - -} diff --git a/playground/AWS/AWSCDK.AppHost/Program.cs b/playground/AWS/AWSCDK.AppHost/Program.cs deleted file mode 100644 index 8b1950328d..0000000000 --- a/playground/AWS/AWSCDK.AppHost/Program.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Amazon; -using AWSCDK.AppHost; - -var builder = DistributedApplication.CreateBuilder(args); - -// Setup a configuration for the AWS .NET SDK. -var awsConfig = builder.AddAWSSDKConfig() - .WithProfile("default") - .WithRegion(RegionEndpoint.EUWest1); - -var stack = builder.AddAWSCDKStack("stack", "Aspire-stack").WithReference(awsConfig); -var customStack = builder.AddAWSCDKStack("custom", scope => new CustomStack(scope, "Aspire-custom")); -customStack.AddOutput("BucketName", stack => stack.Bucket.BucketName).WithReference(awsConfig); - -var topic = stack.AddSNSTopic("topic"); -var queue = stack.AddSQSQueue("queue"); -topic.AddSubscription(queue); - -builder.AddProject("frontend") - //.WithReference(stack) // Reference all outputs of a construct - .WithEnvironment("AWS__Resources__BucketName", customStack.GetOutput("BucketName")) // Reference a construct/stack output - .WithEnvironment("AWS__Resources__ChatTopicArn", topic, t => t.TopicArn) - .WithReference(customStack, s => s.Queue.QueueUrl, "QueueUrl", "AWS:Resources:Queue"); - -builder.Build().Run(); diff --git a/playground/AWS/AWSCDK.AppHost/Properties/launchSettings.json b/playground/AWS/AWSCDK.AppHost/Properties/launchSettings.json deleted file mode 100644 index a9449b4c09..0000000000 --- a/playground/AWS/AWSCDK.AppHost/Properties/launchSettings.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "profiles": { - "https": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "https://localhost:15887;http://localhost:15888", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "DOTNET_ENVIRONMENT": "Development", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16175", - "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:17037" - } - }, - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "http://localhost:15888", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "DOTNET_ENVIRONMENT": "Development", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16175", - "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:17038", - "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true" - } - }, - "generate-manifest": { - "commandName": "Project", - "commandLineArgs": "--publisher manifest --output-path ./aspire-manifest.json", - "dotnetRunMessages": true, - "applicationUrl": "http://localhost:15888", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "DOTNET_ENVIRONMENT": "Development", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16175" - } - } - } -} diff --git a/playground/AWS/AWSCDK.AppHost/WebAppStack.cs b/playground/AWS/AWSCDK.AppHost/WebAppStack.cs deleted file mode 100644 index 2c1b4e404c..0000000000 --- a/playground/AWS/AWSCDK.AppHost/WebAppStack.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Amazon.CDK; -using Amazon.CDK.AWS.DynamoDB; -using Constructs; -using Attribute = Amazon.CDK.AWS.DynamoDB.Attribute; - -namespace AWSCDK.AppHost; - -public class WebAppStackProps : StackProps; - -public class WebAppStack : Stack -{ - public ITable Table { get; } - - public WebAppStack(Construct scope, string id, WebAppStackProps props) - : base(scope, id, props) - { - Table = new Table(this, "Table", new TableProps - { - PartitionKey = new Attribute { Name = "id", Type = AttributeType.STRING }, - BillingMode = BillingMode.PAY_PER_REQUEST - }); - } -} diff --git a/playground/AWS/AWSCDK.AppHost/aspire-manifest.json b/playground/AWS/AWSCDK.AppHost/aspire-manifest.json deleted file mode 100644 index c3cf57f174..0000000000 --- a/playground/AWS/AWSCDK.AppHost/aspire-manifest.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/aspire-8.0.json", - "resources": { - "stack": { - "type": "aws.cloudformation.template.v0", - "stack-name": "Aspire-stack", - "template-path": "cdk.out/Aspire-stack.template.json", - "references": [ - { - "target-resource": "frontend" - } - ] - }, - "custom": { - "type": "aws.cloudformation.template.v0", - "stack-name": "Aspire-custom", - "template-path": "cdk.out/Aspire-custom.template.json", - "references": [ - { - "target-resource": "frontend" - } - ] - }, - "frontend": { - "type": "project.v0", - "path": "../Frontend/Frontend.csproj", - "env": { - "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true", - "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true", - "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory", - "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true", - "HTTP_PORTS": "{frontend.bindings.http.targetPort}", - "AWS__Resources__BucketName": "{custom.output.BucketName}", - "AWS__Resources__ChatTopicArn": "{stack.output.topic8C050C71AWSResourcesChatTopicArn}" - }, - "bindings": { - "http": { - "scheme": "http", - "protocol": "tcp", - "transport": "http" - }, - "https": { - "scheme": "https", - "protocol": "tcp", - "transport": "http" - } - } - } - } -} \ No newline at end of file diff --git a/playground/AWS/AWSCDK.AppHost/cdk.json b/playground/AWS/AWSCDK.AppHost/cdk.json deleted file mode 100644 index 6baff08c68..0000000000 --- a/playground/AWS/AWSCDK.AppHost/cdk.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "app": "dotnet run -- --publisher manifest --output-path ./aspire-manifest.json", - "watch": { - "include": [ - "**" - ], - "exclude": [ - "README.md", - "cdk*.json", - "src/*/obj", - "src/*/bin", - "src/*.sln", - "src/*/GlobalSuppressions.cs", - "src/*/*.csproj" - ] - }, - "context": { - "@aws-cdk/aws-lambda:recognizeLayerVersion": true, - "@aws-cdk/core:checkSecretUsage": true, - "@aws-cdk/core:target-partitions": [ - "aws", - "aws-cn" - ], - "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, - "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, - "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, - "@aws-cdk/aws-iam:minimizePolicies": true, - "@aws-cdk/core:validateSnapshotRemovalPolicy": true, - "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, - "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, - "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, - "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, - "@aws-cdk/core:enablePartitionLiterals": true, - "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, - "@aws-cdk/aws-iam:standardizedServicePrincipals": true, - "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, - "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, - "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, - "@aws-cdk/aws-route53-patters:useCertificate": true, - "@aws-cdk/customresources:installLatestAwsSdkDefault": false, - "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, - "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, - "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, - "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, - "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, - "@aws-cdk/aws-redshift:columnId": true, - "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, - "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, - "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, - "@aws-cdk/aws-kms:aliasNameRef": true, - "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, - "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, - "@aws-cdk/aws-efs:denyAnonymousAccess": true, - "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, - "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, - "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, - "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, - "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, - "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, - "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, - "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true - } -} diff --git a/playground/AWS/Frontend/Components/App.razor b/playground/AWS/Frontend/Components/App.razor deleted file mode 100644 index 4ef6bf5dfc..0000000000 --- a/playground/AWS/Frontend/Components/App.razor +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/playground/AWS/Frontend/Components/Layout/MainLayout.razor b/playground/AWS/Frontend/Components/Layout/MainLayout.razor deleted file mode 100644 index e2bcd7acdb..0000000000 --- a/playground/AWS/Frontend/Components/Layout/MainLayout.razor +++ /dev/null @@ -1,19 +0,0 @@ -@inherits LayoutComponentBase - -
- - -
-
- @Body -
-
-
- -
- An unhandled error has occurred. - Reload - 🗙 -
diff --git a/playground/AWS/Frontend/Components/Layout/MainLayout.razor.css b/playground/AWS/Frontend/Components/Layout/MainLayout.razor.css deleted file mode 100644 index a673115105..0000000000 --- a/playground/AWS/Frontend/Components/Layout/MainLayout.razor.css +++ /dev/null @@ -1,96 +0,0 @@ -.page { - position: relative; - display: flex; - flex-direction: column; -} - -main { - flex: 1; -} - -.sidebar { - background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); -} - -.top-row { - background-color: #f7f7f7; - border-bottom: 1px solid #d6d5d5; - justify-content: flex-end; - height: 3.5rem; - display: flex; - align-items: center; -} - - .top-row ::deep a, .top-row ::deep .btn-link { - white-space: nowrap; - margin-left: 1.5rem; - text-decoration: none; - } - - .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { - text-decoration: underline; - } - - .top-row ::deep a:first-child { - overflow: hidden; - text-overflow: ellipsis; - } - -@media (max-width: 640.98px) { - .top-row { - justify-content: space-between; - } - - .top-row ::deep a, .top-row ::deep .btn-link { - margin-left: 0; - } -} - -@media (min-width: 641px) { - .page { - flex-direction: row; - } - - .sidebar { - width: 250px; - height: 100vh; - position: sticky; - top: 0; - } - - .top-row { - position: sticky; - top: 0; - z-index: 1; - } - - .top-row.auth ::deep a:first-child { - flex: 1; - text-align: right; - width: 0; - } - - .top-row, article { - padding-left: 2rem !important; - padding-right: 1.5rem !important; - } -} - -#blazor-error-ui { - background: lightyellow; - bottom: 0; - box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); - display: none; - left: 0; - padding: 0.6rem 1.25rem 0.7rem 1.25rem; - position: fixed; - width: 100%; - z-index: 1000; -} - - #blazor-error-ui .dismiss { - cursor: pointer; - position: absolute; - right: 0.75rem; - top: 0.5rem; - } diff --git a/playground/AWS/Frontend/Components/Layout/NavMenu.razor b/playground/AWS/Frontend/Components/Layout/NavMenu.razor deleted file mode 100644 index 9b51cd0550..0000000000 --- a/playground/AWS/Frontend/Components/Layout/NavMenu.razor +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - diff --git a/playground/AWS/Frontend/Components/Layout/NavMenu.razor.css b/playground/AWS/Frontend/Components/Layout/NavMenu.razor.css deleted file mode 100644 index 4e15395e09..0000000000 --- a/playground/AWS/Frontend/Components/Layout/NavMenu.razor.css +++ /dev/null @@ -1,105 +0,0 @@ -.navbar-toggler { - appearance: none; - cursor: pointer; - width: 3.5rem; - height: 2.5rem; - color: white; - position: absolute; - top: 0.5rem; - right: 1rem; - border: 1px solid rgba(255, 255, 255, 0.1); - background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1); -} - -.navbar-toggler:checked { - background-color: rgba(255, 255, 255, 0.5); -} - -.top-row { - height: 3.5rem; - background-color: rgba(0,0,0,0.4); -} - -.navbar-brand { - font-size: 1.1rem; -} - -.bi { - display: inline-block; - position: relative; - width: 1.25rem; - height: 1.25rem; - margin-right: 0.75rem; - top: -1px; - background-size: cover; -} - -.bi-house-door-fill-nav-menu { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); -} - -.bi-plus-square-fill-nav-menu { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E"); -} - -.bi-list-nested-nav-menu { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); -} - -.nav-item { - font-size: 0.9rem; - padding-bottom: 0.5rem; -} - - .nav-item:first-of-type { - padding-top: 1rem; - } - - .nav-item:last-of-type { - padding-bottom: 1rem; - } - - .nav-item ::deep .nav-link { - color: #d7d7d7; - background: none; - border: none; - border-radius: 4px; - height: 3rem; - display: flex; - align-items: center; - line-height: 3rem; - width: 100%; - } - -.nav-item ::deep a.active { - background-color: rgba(255,255,255,0.37); - color: white; -} - -.nav-item ::deep .nav-link:hover { - background-color: rgba(255,255,255,0.1); - color: white; -} - -.nav-scrollable { - display: none; -} - -.navbar-toggler:checked ~ .nav-scrollable { - display: block; -} - -@media (min-width: 641px) { - .navbar-toggler { - display: none; - } - - .nav-scrollable { - /* Never collapse the sidebar for wide screens */ - display: block; - - /* Allow sidebar to scroll for tall menus */ - height: calc(100vh - 3.5rem); - overflow-y: auto; - } -} diff --git a/playground/AWS/Frontend/Components/Pages/AppHostConfiguration.razor b/playground/AWS/Frontend/Components/Pages/AppHostConfiguration.razor deleted file mode 100644 index ff52d4a46e..0000000000 --- a/playground/AWS/Frontend/Components/Pages/AppHostConfiguration.razor +++ /dev/null @@ -1,34 +0,0 @@ -@page "/apphost-configuration" -@using Amazon.SQS; -@using Amazon.SQS.Model; -@using Microsoft.Extensions.Options; - -@inject IConfiguration configuration; - -@inject IAmazonSQS sqsClient; - -SQS Send Message Example - -

App Host Configuration

- -

-The list of configuration applied from the CloudFormation stack created in AppHost and assigned to project. -

-
    - @foreach(var item in configuration.GetSection("AWS:Resources").AsEnumerable()) - { - @if(item.Value != null) - { -
  • @item.Key: @item.Value
  • - } - } -
- - -

-Configuration applied from AppHost to AWS service clients: -

-
    -
  • Profile: @Environment.GetEnvironmentVariable("AWS_PROFILE")
  • -
  • Region: @sqsClient.Config.RegionEndpoint.SystemName
  • -
diff --git a/playground/AWS/Frontend/Components/Pages/Error.razor b/playground/AWS/Frontend/Components/Pages/Error.razor deleted file mode 100644 index 576cc2d2f4..0000000000 --- a/playground/AWS/Frontend/Components/Pages/Error.razor +++ /dev/null @@ -1,36 +0,0 @@ -@page "/Error" -@using System.Diagnostics - -Error - -

Error.

-

An error occurred while processing your request.

- -@if (ShowRequestId) -{ -

- Request ID: @RequestId -

-} - -

Development Mode

-

- Swapping to Development environment will display more detailed information about the error that occurred. -

-

- The Development environment shouldn't be enabled for deployed applications. - It can result in displaying sensitive information from exceptions to end users. - For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development - and restarting the app. -

- -@code{ - [CascadingParameter] - private HttpContext? HttpContext { get; set; } - - private string? RequestId { get; set; } - private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); - - protected override void OnInitialized() => - RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; -} diff --git a/playground/AWS/Frontend/Components/Pages/Home.razor b/playground/AWS/Frontend/Components/Pages/Home.razor deleted file mode 100644 index b7a7bcd0b7..0000000000 --- a/playground/AWS/Frontend/Components/Pages/Home.razor +++ /dev/null @@ -1,11 +0,0 @@ -@page "/" -@using Amazon.SQS; -@using Amazon.SQS.Model; -@using Microsoft.Extensions.Options; - - -AWS Aspire - -

AWS and Aspire

- -This application demonstrates the usages of AWS with Aspire. diff --git a/playground/AWS/Frontend/Components/Pages/MessagePublisher.razor b/playground/AWS/Frontend/Components/Pages/MessagePublisher.razor deleted file mode 100644 index 9f8b16055c..0000000000 --- a/playground/AWS/Frontend/Components/Pages/MessagePublisher.razor +++ /dev/null @@ -1,69 +0,0 @@ -@page "/message-publisher" -@using Frontend.Models -@using AWS.Messaging - - -@inject IMessagePublisher publisher - -

Message Publisher Example

- -

-The page publishes a message to the SNS topic provisioning in the AppHost and assigned to this project. To publish the messages the AWS Messaging -package is used which uses services clients that were configured in the AppHost. -

- - -

- -

-

- -

-

- -

- -
    -@foreach(var message in PublishStatuses) -{ -
  • @message
  • -} -
- - -@code { - - public string? Message { get; set; } - - public string? Recipient { get; set; } - - - public List PublishStatuses { get; } = new List(); - - - public async Task SendMessageAsync() - { - if (string.IsNullOrEmpty(this.Message)) - { - return; - } - - var chatMessage = new ChatMessage { Message = this.Message, Recipient = this.Recipient }; - - try - { - await publisher.PublishAsync(chatMessage); - PublishStatuses.Add($"{DateTime.Now}: Message publish successfully"); - } - catch(Exception e) - { - PublishStatuses.Add($"{DateTime.Now}: Message failed to publish: {e.Message}"); - } - } -} diff --git a/playground/AWS/Frontend/Components/Routes.razor b/playground/AWS/Frontend/Components/Routes.razor deleted file mode 100644 index d0df781615..0000000000 --- a/playground/AWS/Frontend/Components/Routes.razor +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/playground/AWS/Frontend/Components/_Imports.razor b/playground/AWS/Frontend/Components/_Imports.razor deleted file mode 100644 index 73b95e26dd..0000000000 --- a/playground/AWS/Frontend/Components/_Imports.razor +++ /dev/null @@ -1,10 +0,0 @@ -@using System.Net.Http -@using System.Net.Http.Json -@using Microsoft.AspNetCore.Components.Forms -@using Microsoft.AspNetCore.Components.Routing -@using Microsoft.AspNetCore.Components.Web -@using static Microsoft.AspNetCore.Components.Web.RenderMode -@using Microsoft.AspNetCore.Components.Web.Virtualization -@using Microsoft.JSInterop -@using Frontend -@using Frontend.Components diff --git a/playground/AWS/Frontend/Frontend.csproj b/playground/AWS/Frontend/Frontend.csproj deleted file mode 100644 index 40bd2ba654..0000000000 --- a/playground/AWS/Frontend/Frontend.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - $(DefaultTargetFramework) - enable - enable - - - - - - - - - - - diff --git a/playground/AWS/Frontend/Models/ChatMessage.cs b/playground/AWS/Frontend/Models/ChatMessage.cs deleted file mode 100644 index 21e224ff5c..0000000000 --- a/playground/AWS/Frontend/Models/ChatMessage.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Frontend.Models; - -public class ChatMessage -{ - public string? Message { get; set; } - - public string? Recipient { get; set; } -} diff --git a/playground/AWS/Frontend/Program.cs b/playground/AWS/Frontend/Program.cs deleted file mode 100644 index 489b8351e5..0000000000 --- a/playground/AWS/Frontend/Program.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Amazon.SimpleNotificationService; -using Amazon.SQS; -using Frontend.Components; - -var builder = WebApplication.CreateBuilder(args); - -builder.AddServiceDefaults(); - -builder.Services.AddAWSService(); -builder.Services.AddAWSService(); - -// Configuring messaging using the AWS.Messaging library. -builder.Services.AddAWSMessageBus(messageBuilder => -{ - // Get the SQS queue URL that was created from AppHost and assigned to the project. - var chatTopicArn = builder.Configuration["AWS:Resources:ChatTopicArn"]; - if (chatTopicArn != null) - { - messageBuilder.AddSNSPublisher(chatTopicArn); - } -}); - -// Add services to the container. -builder.Services.AddRazorComponents() - .AddInteractiveServerComponents(); - -var app = builder.Build(); - -app.MapDefaultEndpoints(); - -if (!app.Environment.IsDevelopment()) -{ - app.UseExceptionHandler("/Error", createScopeForErrors: true); -} - -app.UseStaticFiles(); -app.UseAntiforgery(); - -app.MapRazorComponents() - .AddInteractiveServerRenderMode(); - -app.Run(); diff --git a/playground/AWS/Frontend/Properties/launchSettings.json b/playground/AWS/Frontend/Properties/launchSettings.json deleted file mode 100644 index 49c05fcfec..0000000000 --- a/playground/AWS/Frontend/Properties/launchSettings.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:14466", - "sslPort": 44341 - } - }, - "profiles": { - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "http://localhost:5166", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "https": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "https://localhost:7230;http://localhost:5166", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } - } diff --git a/playground/AWS/Frontend/appsettings.Development.json b/playground/AWS/Frontend/appsettings.Development.json deleted file mode 100644 index 0c208ae918..0000000000 --- a/playground/AWS/Frontend/appsettings.Development.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - } -} diff --git a/playground/AWS/Frontend/appsettings.json b/playground/AWS/Frontend/appsettings.json deleted file mode 100644 index 10f68b8c8b..0000000000 --- a/playground/AWS/Frontend/appsettings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" -} diff --git a/playground/AWS/Frontend/wwwroot/app.css b/playground/AWS/Frontend/wwwroot/app.css deleted file mode 100644 index 2bd9b7896d..0000000000 --- a/playground/AWS/Frontend/wwwroot/app.css +++ /dev/null @@ -1,51 +0,0 @@ -html, body { - font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; -} - -a, .btn-link { - color: #006bb7; -} - -.btn-primary { - color: #fff; - background-color: #1b6ec2; - border-color: #1861ac; -} - -.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { - box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; -} - -.content { - padding-top: 1.1rem; -} - -h1:focus { - outline: none; -} - -.valid.modified:not([type=checkbox]) { - outline: 1px solid #26b050; -} - -.invalid { - outline: 1px solid #e50000; -} - -.validation-message { - color: #e50000; -} - -.blazor-error-boundary { - background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; - padding: 1rem 1rem 1rem 3.7rem; - color: white; -} - - .blazor-error-boundary::after { - content: "An error has occurred." - } - -.darker-border-checkbox.form-check-input { - border-color: #929292; -} diff --git a/playground/AWS/Frontend/wwwroot/bootstrap/bootstrap.min.css b/playground/AWS/Frontend/wwwroot/bootstrap/bootstrap.min.css deleted file mode 100644 index 02ae65b5fe..0000000000 --- a/playground/AWS/Frontend/wwwroot/bootstrap/bootstrap.min.css +++ /dev/null @@ -1,7 +0,0 @@ -@charset "UTF-8";/*! - * Bootstrap v5.1.0 (https://getbootstrap.com/) - * Copyright 2011-2021 The Bootstrap Authors - * Copyright 2011-2021 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-rgb:33,37,41;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#6c757d}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-right:var(--bs-gutter-x,.75rem);padding-left:var(--bs-gutter-x,.75rem);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(var(--bs-gutter-y) * -1);margin-right:calc(var(--bs-gutter-x) * -.5);margin-left:calc(var(--bs-gutter-x) * -.5)}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-bg:transparent;--bs-table-accent-bg:transparent;--bs-table-striped-color:#212529;--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:#212529;--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:#212529;--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:#212529;vertical-align:top;border-color:#dee2e6}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table>:not(:last-child)>:last-child>*{border-bottom-color:currentColor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-striped>tbody>tr:nth-of-type(odd){--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-bg:#cfe2ff;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:#000;border-color:#bacbe6}.table-secondary{--bs-table-bg:#e2e3e5;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:#000;border-color:#cbccce}.table-success{--bs-table-bg:#d1e7dd;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:#000;border-color:#bcd0c7}.table-info{--bs-table-bg:#cff4fc;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:#000;border-color:#badce3}.table-warning{--bs-table-bg:#fff3cd;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:#000;border-color:#e6dbb9}.table-danger{--bs-table-bg:#f8d7da;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:#000;border-color:#dfc2c4}.table-light{--bs-table-bg:#f8f9fa;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:#000;border-color:#dfe0e1}.table-dark{--bs-table-bg:#212529;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:#fff;border-color:#373b3e}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:#6c757d}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + 2px)}textarea.form-control-sm{min-height:calc(1.5em + .5rem + 2px)}textarea.form-control-lg{min-height:calc(1.5em + 1rem + 2px)}.form-control-color{width:3rem;height:auto;padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{height:1.5em;border-radius:.25rem}.form-control-color::-webkit-color-swatch{height:1.5em;border-radius:.25rem}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;-moz-padding-start:calc(0.75rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #212529}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-select{height:calc(3.5rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;height:100%;padding:1rem .75rem;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control{padding:1rem .75rem}.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus{z-index:3}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:3}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#198754}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(25,135,84,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#198754;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#198754}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#198754}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#198754}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#198754}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group .form-control.is-valid,.input-group .form-select.is-valid,.was-validated .input-group .form-control:valid,.was-validated .input-group .form-select:valid{z-index:1}.input-group .form-control.is-valid:focus,.input-group .form-select.is-valid:focus,.was-validated .input-group .form-control:valid:focus,.was-validated .input-group .form-select:valid:focus{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#dc3545}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#dc3545}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#dc3545}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group .form-control.is-invalid,.input-group .form-select.is-invalid,.was-validated .input-group .form-control:invalid,.was-validated .input-group .form-select:invalid{z-index:2}.input-group .form-control.is-invalid:focus,.input-group .form-select.is-invalid:focus,.was-validated .input-group .form-control:invalid:focus,.was-validated .input-group .form-select:invalid:focus{z-index:3}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#212529;text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529}.btn-check:focus+.btn,.btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{pointer-events:none;opacity:.65}.btn-primary{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-primary:hover{color:#fff;background-color:#0b5ed7;border-color:#0a58ca}.btn-check:focus+.btn-primary,.btn-primary:focus{color:#fff;background-color:#0b5ed7;border-color:#0a58ca;box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-check:active+.btn-primary,.btn-check:checked+.btn-primary,.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0a58ca;border-color:#0a53be}.btn-check:active+.btn-primary:focus,.btn-check:checked+.btn-primary:focus,.btn-primary.active:focus,.btn-primary:active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5c636a;border-color:#565e64}.btn-check:focus+.btn-secondary,.btn-secondary:focus{color:#fff;background-color:#5c636a;border-color:#565e64;box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-check:active+.btn-secondary,.btn-check:checked+.btn-secondary,.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#565e64;border-color:#51585e}.btn-check:active+.btn-secondary:focus,.btn-check:checked+.btn-secondary:focus,.btn-secondary.active:focus,.btn-secondary:active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-success{color:#fff;background-color:#198754;border-color:#198754}.btn-success:hover{color:#fff;background-color:#157347;border-color:#146c43}.btn-check:focus+.btn-success,.btn-success:focus{color:#fff;background-color:#157347;border-color:#146c43;box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-check:active+.btn-success,.btn-check:checked+.btn-success,.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#146c43;border-color:#13653f}.btn-check:active+.btn-success:focus,.btn-check:checked+.btn-success:focus,.btn-success.active:focus,.btn-success:active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#198754;border-color:#198754}.btn-info{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-info:hover{color:#000;background-color:#31d2f2;border-color:#25cff2}.btn-check:focus+.btn-info,.btn-info:focus{color:#000;background-color:#31d2f2;border-color:#25cff2;box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-check:active+.btn-info,.btn-check:checked+.btn-info,.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#000;background-color:#3dd5f3;border-color:#25cff2}.btn-check:active+.btn-info:focus,.btn-check:checked+.btn-info:focus,.btn-info.active:focus,.btn-info:active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-info.disabled,.btn-info:disabled{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-warning{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#000;background-color:#ffca2c;border-color:#ffc720}.btn-check:focus+.btn-warning,.btn-warning:focus{color:#000;background-color:#ffca2c;border-color:#ffc720;box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-check:active+.btn-warning,.btn-check:checked+.btn-warning,.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#000;background-color:#ffcd39;border-color:#ffc720}.btn-check:active+.btn-warning:focus,.btn-check:checked+.btn-warning:focus,.btn-warning.active:focus,.btn-warning:active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#bb2d3b;border-color:#b02a37}.btn-check:focus+.btn-danger,.btn-danger:focus{color:#fff;background-color:#bb2d3b;border-color:#b02a37;box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-check:active+.btn-danger,.btn-check:checked+.btn-danger,.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#b02a37;border-color:#a52834}.btn-check:active+.btn-danger:focus,.btn-check:checked+.btn-danger:focus,.btn-danger.active:focus,.btn-danger:active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-light{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:focus+.btn-light,.btn-light:focus{color:#000;background-color:#f9fafb;border-color:#f9fafb;box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-check:active+.btn-light,.btn-check:checked+.btn-light,.btn-light.active,.btn-light:active,.show>.btn-light.dropdown-toggle{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:active+.btn-light:focus,.btn-check:checked+.btn-light:focus,.btn-light.active:focus,.btn-light:active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-light.disabled,.btn-light:disabled{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-dark{color:#fff;background-color:#212529;border-color:#212529}.btn-dark:hover{color:#fff;background-color:#1c1f23;border-color:#1a1e21}.btn-check:focus+.btn-dark,.btn-dark:focus{color:#fff;background-color:#1c1f23;border-color:#1a1e21;box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-check:active+.btn-dark,.btn-check:checked+.btn-dark,.btn-dark.active,.btn-dark:active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1a1e21;border-color:#191c1f}.btn-check:active+.btn-dark:focus,.btn-check:checked+.btn-dark:focus,.btn-dark.active:focus,.btn-dark:active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#212529;border-color:#212529}.btn-outline-primary{color:#0d6efd;border-color:#0d6efd}.btn-outline-primary:hover{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:focus+.btn-outline-primary,.btn-outline-primary:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-check:active+.btn-outline-primary,.btn-check:checked+.btn-outline-primary,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show,.btn-outline-primary:active{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:active+.btn-outline-primary:focus,.btn-check:checked+.btn-outline-primary:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus,.btn-outline-primary:active:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0d6efd;background-color:transparent}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:focus+.btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-check:active+.btn-outline-secondary,.btn-check:checked+.btn-outline-secondary,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show,.btn-outline-secondary:active{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:active+.btn-outline-secondary:focus,.btn-check:checked+.btn-outline-secondary:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus,.btn-outline-secondary:active:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-success{color:#198754;border-color:#198754}.btn-outline-success:hover{color:#fff;background-color:#198754;border-color:#198754}.btn-check:focus+.btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-check:active+.btn-outline-success,.btn-check:checked+.btn-outline-success,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show,.btn-outline-success:active{color:#fff;background-color:#198754;border-color:#198754}.btn-check:active+.btn-outline-success:focus,.btn-check:checked+.btn-outline-success:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus,.btn-outline-success:active:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#198754;background-color:transparent}.btn-outline-info{color:#0dcaf0;border-color:#0dcaf0}.btn-outline-info:hover{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:focus+.btn-outline-info,.btn-outline-info:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-check:active+.btn-outline-info,.btn-check:checked+.btn-outline-info,.btn-outline-info.active,.btn-outline-info.dropdown-toggle.show,.btn-outline-info:active{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:active+.btn-outline-info:focus,.btn-check:checked+.btn-outline-info:focus,.btn-outline-info.active:focus,.btn-outline-info.dropdown-toggle.show:focus,.btn-outline-info:active:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#0dcaf0;background-color:transparent}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:focus+.btn-outline-warning,.btn-outline-warning:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-check:active+.btn-outline-warning,.btn-check:checked+.btn-outline-warning,.btn-outline-warning.active,.btn-outline-warning.dropdown-toggle.show,.btn-outline-warning:active{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:active+.btn-outline-warning:focus,.btn-check:checked+.btn-outline-warning:focus,.btn-outline-warning.active:focus,.btn-outline-warning.dropdown-toggle.show:focus,.btn-outline-warning:active:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:focus+.btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-check:active+.btn-outline-danger,.btn-check:checked+.btn-outline-danger,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show,.btn-outline-danger:active{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:active+.btn-outline-danger:focus,.btn-check:checked+.btn-outline-danger:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus,.btn-outline-danger:active:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:focus+.btn-outline-light,.btn-outline-light:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-check:active+.btn-outline-light,.btn-check:checked+.btn-outline-light,.btn-outline-light.active,.btn-outline-light.dropdown-toggle.show,.btn-outline-light:active{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:active+.btn-outline-light:focus,.btn-check:checked+.btn-outline-light:focus,.btn-outline-light.active:focus,.btn-outline-light.dropdown-toggle.show:focus,.btn-outline-light:active:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-dark{color:#212529;border-color:#212529}.btn-outline-dark:hover{color:#fff;background-color:#212529;border-color:#212529}.btn-check:focus+.btn-outline-dark,.btn-outline-dark:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-check:active+.btn-outline-dark,.btn-check:checked+.btn-outline-dark,.btn-outline-dark.active,.btn-outline-dark.dropdown-toggle.show,.btn-outline-dark:active{color:#fff;background-color:#212529;border-color:#212529}.btn-check:active+.btn-outline-dark:focus,.btn-check:checked+.btn-outline-dark:focus,.btn-outline-dark.active:focus,.btn-outline-dark.dropdown-toggle.show:focus,.btn-outline-dark:active:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#212529;background-color:transparent}.btn-link{font-weight:400;color:#0d6efd;text-decoration:underline}.btn-link:hover{color:#0a58ca}.btn-link.disabled,.btn-link:disabled{color:#6c757d}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropend,.dropstart,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;z-index:1000;display:none;min-width:10rem;padding:.5rem 0;margin:0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:.125rem}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid rgba(0,0,0,.15)}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#212529;text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1e2125;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0d6efd}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#212529}.dropdown-menu-dark{color:#dee2e6;background-color:#343a40;border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item{color:#dee2e6}.dropdown-menu-dark .dropdown-item:focus,.dropdown-menu-dark .dropdown-item:hover{color:#fff;background-color:rgba(255,255,255,.15)}.dropdown-menu-dark .dropdown-item.active,.dropdown-menu-dark .dropdown-item:active{color:#fff;background-color:#0d6efd}.dropdown-menu-dark .dropdown-item.disabled,.dropdown-menu-dark .dropdown-item:disabled{color:#adb5bd}.dropdown-menu-dark .dropdown-divider{border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item-text{color:#dee2e6}.dropdown-menu-dark .dropdown-header{color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;color:#0d6efd;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:#0a58ca}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;background:0 0;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6;isolation:isolate}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{background:0 0;border:0;border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#0d6efd}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding-top:.5rem;padding-bottom:.5rem}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;text-decoration:none;white-space:nowrap}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem;transition:box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 .25rem}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas-header{display:none}.navbar-expand-sm .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-sm .offcanvas-bottom,.navbar-expand-sm .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas-header{display:none}.navbar-expand-md .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-md .offcanvas-bottom,.navbar-expand-md .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas-header{display:none}.navbar-expand-lg .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-lg .offcanvas-bottom,.navbar-expand-lg .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas-header{display:none}.navbar-expand-xl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xl .offcanvas-bottom,.navbar-expand-xl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xxl .offcanvas-bottom,.navbar-expand-xxl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas-header{display:none}.navbar-expand .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand .offcanvas-bottom,.navbar-expand .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.55)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.55);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.55)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.55)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.55);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.55)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:1rem 1rem}.card-title{margin-bottom:.5rem}.card-subtitle{margin-top:-.25rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:1rem}.card-header{padding:.5rem 1rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.5rem 1rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.5rem;margin-bottom:-.5rem;margin-left:-.5rem;border-bottom:0}.card-header-pills{margin-right:-.5rem;margin-left:-.5rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-group>.card{margin-bottom:.75rem}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:1rem 1.25rem;font-size:1rem;color:#212529;text-align:left;background-color:#fff;border:0;border-radius:0;overflow-anchor:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,border-radius .15s ease}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:#0c63e4;background-color:#e7f1ff;box-shadow:inset 0 -1px 0 rgba(0,0,0,.125)}.accordion-button:not(.collapsed)::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");transform:rotate(-180deg)}.accordion-button::after{flex-shrink:0;width:1.25rem;height:1.25rem;margin-left:auto;content:"";background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-size:1.25rem;transition:transform .2s ease-in-out}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.accordion-header{margin-bottom:0}.accordion-item{background-color:#fff;border:1px solid rgba(0,0,0,.125)}.accordion-item:first-of-type{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.accordion-item:first-of-type .accordion-button{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-body{padding:1rem 1.25rem}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button{border-radius:0}.breadcrumb{display:flex;flex-wrap:wrap;padding:0 0;margin-bottom:1rem;list-style:none}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;color:#0d6efd;text-decoration:none;background-color:#fff;border:1px solid #dee2e6;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:#0a58ca;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;color:#0a58ca;background-color:#e9ecef;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item.active .page-link{z-index:3;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:#dee2e6}.page-link{padding:.375rem .75rem}.page-item:first-child .page-link{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{position:relative;padding:1rem 1rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{color:#084298;background-color:#cfe2ff;border-color:#b6d4fe}.alert-primary .alert-link{color:#06357a}.alert-secondary{color:#41464b;background-color:#e2e3e5;border-color:#d3d6d8}.alert-secondary .alert-link{color:#34383c}.alert-success{color:#0f5132;background-color:#d1e7dd;border-color:#badbcc}.alert-success .alert-link{color:#0c4128}.alert-info{color:#055160;background-color:#cff4fc;border-color:#b6effb}.alert-info .alert-link{color:#04414d}.alert-warning{color:#664d03;background-color:#fff3cd;border-color:#ffecb5}.alert-warning .alert-link{color:#523e02}.alert-danger{color:#842029;background-color:#f8d7da;border-color:#f5c2c7}.alert-danger .alert-link{color:#6a1a21}.alert-light{color:#636464;background-color:#fefefe;border-color:#fdfdfe}.alert-light .alert-link{color:#4f5050}.alert-dark{color:#141619;background-color:#d3d3d4;border-color:#bcbebf}.alert-dark .alert-link{color:#101214}@-webkit-keyframes progress-bar-stripes{0%{background-position-x:1rem}}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#0d6efd;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>li::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.5rem 1rem;color:#212529;text-decoration:none;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#084298;background-color:#cfe2ff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#084298;background-color:#bacbe6}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#084298;border-color:#084298}.list-group-item-secondary{color:#41464b;background-color:#e2e3e5}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#41464b;background-color:#cbccce}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#41464b;border-color:#41464b}.list-group-item-success{color:#0f5132;background-color:#d1e7dd}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#0f5132;background-color:#bcd0c7}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#0f5132;border-color:#0f5132}.list-group-item-info{color:#055160;background-color:#cff4fc}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#055160;background-color:#badce3}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#055160;border-color:#055160}.list-group-item-warning{color:#664d03;background-color:#fff3cd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#664d03;background-color:#e6dbb9}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#664d03;border-color:#664d03}.list-group-item-danger{color:#842029;background-color:#f8d7da}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#842029;background-color:#dfc2c4}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#842029;border-color:#842029}.list-group-item-light{color:#636464;background-color:#fefefe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#636464;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636464;border-color:#636464}.list-group-item-dark{color:#141619;background-color:#d3d3d4}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#141619;background-color:#bebebf}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#141619;border-color:#141619}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;border-radius:.25rem;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{width:350px;max-width:100%;font-size:.875rem;pointer-events:auto;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .5rem 1rem rgba(0,0,0,.15);border-radius:.25rem}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:.75rem}.toast-header{display:flex;align-items:center;padding:.5rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-header .btn-close{margin-right:-.375rem;margin-left:.75rem}.toast-body{padding:.75rem;word-wrap:break-word}.modal{position:fixed;top:0;left:0;z-index:1055;display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1050;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .btn-close{padding:.5rem .5rem;margin:-.5rem -.5rem -.5rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;flex-shrink:0;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{height:calc(100% - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}.modal-fullscreen .modal-footer{border-radius:0}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}.modal-fullscreen-sm-down .modal-footer{border-radius:0}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}.modal-fullscreen-md-down .modal-footer{border-radius:0}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}.modal-fullscreen-lg-down .modal-footer{border-radius:0}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}.modal-fullscreen-xl-down .modal-footer{border-radius:0}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}.modal-fullscreen-xxl-down .modal-footer{border-radius:0}}.tooltip{position:absolute;z-index:1080;display:block;margin:0;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .tooltip-arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[data-popper-placement^=right],.bs-tooltip-end{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[data-popper-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[data-popper-placement^=left],.bs-tooltip-start{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1070;display:block;max-width:276px;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .popover-arrow{position:absolute;display:block;width:1rem;height:.5rem}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f0f0f0}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem 1rem;margin-bottom:0;font-size:1rem;background-color:#f0f0f0;border-bottom:1px solid rgba(0,0,0,.2);border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:1rem 1rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}@-webkit-keyframes spinner-border{to{transform:rotate(360deg)}}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.offcanvas{position:fixed;bottom:0;z-index:1045;display:flex;flex-direction:column;max-width:100%;visibility:hidden;background-color:#fff;background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1rem}.offcanvas-header .btn-close{padding:.5rem .5rem;margin-top:-.5rem;margin-right:-.5rem;margin-bottom:-.5rem}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{flex-grow:1;padding:1rem 1rem;overflow-y:auto}.offcanvas-start{top:0;left:0;width:400px;border-right:1px solid rgba(0,0,0,.2);transform:translateX(-100%)}.offcanvas-end{top:0;right:0;width:400px;border-left:1px solid rgba(0,0,0,.2);transform:translateX(100%)}.offcanvas-top{top:0;right:0;left:0;height:30vh;max-height:100%;border-bottom:1px solid rgba(0,0,0,.2);transform:translateY(-100%)}.offcanvas-bottom{right:0;left:0;height:30vh;max-height:100%;border-top:1px solid rgba(0,0,0,.2);transform:translateY(100%)}.offcanvas.show{transform:none}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentColor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{-webkit-animation:placeholder-glow 2s ease-in-out infinite;animation:placeholder-glow 2s ease-in-out infinite}@-webkit-keyframes placeholder-glow{50%{opacity:.2}}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;-webkit-animation:placeholder-wave 2s linear infinite;animation:placeholder-wave 2s linear infinite}@-webkit-keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.link-primary{color:#0d6efd}.link-primary:focus,.link-primary:hover{color:#0a58ca}.link-secondary{color:#6c757d}.link-secondary:focus,.link-secondary:hover{color:#565e64}.link-success{color:#198754}.link-success:focus,.link-success:hover{color:#146c43}.link-info{color:#0dcaf0}.link-info:focus,.link-info:hover{color:#3dd5f3}.link-warning{color:#ffc107}.link-warning:focus,.link-warning:hover{color:#ffcd39}.link-danger{color:#dc3545}.link-danger:focus,.link-danger:hover{color:#b02a37}.link-light{color:#f8f9fa}.link-light:focus,.link-light:hover{color:#f9fafb}.link-dark{color:#212529}.link-dark:focus,.link-dark:hover{color:#1a1e21}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:calc(3 / 4 * 100%)}.ratio-16x9{--bs-aspect-ratio:calc(9 / 16 * 100%)}.ratio-21x9{--bs-aspect-ratio:calc(9 / 21 * 100%)}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:1px;min-height:1em;background-color:currentColor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:1px solid #dee2e6!important}.border-0{border:0!important}.border-top{border-top:1px solid #dee2e6!important}.border-top-0{border-top:0!important}.border-end{border-right:1px solid #dee2e6!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:1px solid #dee2e6!important}.border-start-0{border-left:0!important}.border-primary{border-color:#0d6efd!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#198754!important}.border-info{border-color:#0dcaf0!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#212529!important}.border-white{border-color:#fff!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:#6c757d!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:.25rem!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:.2rem!important}.rounded-2{border-radius:.25rem!important}.rounded-3{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-end{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-start{border-bottom-left-radius:.25rem!important;border-top-left-radius:.25rem!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} -/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/playground/AWS/Frontend/wwwroot/bootstrap/bootstrap.min.css.map b/playground/AWS/Frontend/wwwroot/bootstrap/bootstrap.min.css.map deleted file mode 100644 index afcd9e33e9..0000000000 --- a/playground/AWS/Frontend/wwwroot/bootstrap/bootstrap.min.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["../../scss/bootstrap.scss","../../scss/_root.scss","../../scss/_reboot.scss","dist/css/bootstrap.css","../../scss/vendor/_rfs.scss","../../scss/mixins/_border-radius.scss","../../scss/_type.scss","../../scss/mixins/_lists.scss","../../scss/_images.scss","../../scss/mixins/_image.scss","../../scss/_containers.scss","../../scss/mixins/_container.scss","../../scss/mixins/_breakpoints.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/_tables.scss","../../scss/mixins/_table-variants.scss","../../scss/forms/_labels.scss","../../scss/forms/_form-text.scss","../../scss/forms/_form-control.scss","../../scss/mixins/_transition.scss","../../scss/mixins/_gradients.scss","../../scss/forms/_form-select.scss","../../scss/forms/_form-check.scss","../../scss/forms/_form-range.scss","../../scss/forms/_floating-labels.scss","../../scss/forms/_input-group.scss","../../scss/mixins/_forms.scss","../../scss/_buttons.scss","../../scss/mixins/_buttons.scss","../../scss/_transitions.scss","../../scss/_dropdown.scss","../../scss/mixins/_caret.scss","../../scss/_button-group.scss","../../scss/_nav.scss","../../scss/_navbar.scss","../../scss/_card.scss","../../scss/_accordion.scss","../../scss/_breadcrumb.scss","../../scss/_pagination.scss","../../scss/mixins/_pagination.scss","../../scss/_badge.scss","../../scss/_alert.scss","../../scss/mixins/_alert.scss","../../scss/_progress.scss","../../scss/_list-group.scss","../../scss/mixins/_list-group.scss","../../scss/_close.scss","../../scss/_toasts.scss","../../scss/_modal.scss","../../scss/mixins/_backdrop.scss","../../scss/_tooltip.scss","../../scss/mixins/_reset-text.scss","../../scss/_popover.scss","../../scss/_carousel.scss","../../scss/mixins/_clearfix.scss","../../scss/_spinners.scss","../../scss/_offcanvas.scss","../../scss/_placeholders.scss","../../scss/helpers/_colored-links.scss","../../scss/helpers/_ratio.scss","../../scss/helpers/_position.scss","../../scss/helpers/_stacks.scss","../../scss/helpers/_visually-hidden.scss","../../scss/mixins/_visually-hidden.scss","../../scss/helpers/_stretched-link.scss","../../scss/helpers/_text-truncation.scss","../../scss/mixins/_text-truncate.scss","../../scss/helpers/_vr.scss","../../scss/mixins/_utilities.scss","../../scss/utilities/_api.scss"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Bootstrap v5.1.0 (https://getbootstrap.com/)\n * Copyright 2011-2021 The Bootstrap Authors\n * Copyright 2011-2021 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n\n// scss-docs-start import-stack\n// Configuration\n@import \"functions\";\n@import \"variables\";\n@import \"mixins\";\n@import \"utilities\";\n\n// Layout & components\n@import \"root\";\n@import \"reboot\";\n@import \"type\";\n@import \"images\";\n@import \"containers\";\n@import \"grid\";\n@import \"tables\";\n@import \"forms\";\n@import \"buttons\";\n@import \"transitions\";\n@import \"dropdown\";\n@import \"button-group\";\n@import \"nav\";\n@import \"navbar\";\n@import \"card\";\n@import \"accordion\";\n@import \"breadcrumb\";\n@import \"pagination\";\n@import \"badge\";\n@import \"alert\";\n@import \"progress\";\n@import \"list-group\";\n@import \"close\";\n@import \"toasts\";\n@import \"modal\";\n@import \"tooltip\";\n@import \"popover\";\n@import \"carousel\";\n@import \"spinners\";\n@import \"offcanvas\";\n@import \"placeholders\";\n\n// Helpers\n@import \"helpers\";\n\n// Utilities\n@import \"utilities/api\";\n// scss-docs-end import-stack\n",":root {\n // Note: Custom variable values only support SassScript inside `#{}`.\n\n // Colors\n //\n // Generate palettes for full colors, grays, and theme colors.\n\n @each $color, $value in $colors {\n --#{$variable-prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $grays {\n --#{$variable-prefix}gray-#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors {\n --#{$variable-prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors-rgb {\n --#{$variable-prefix}#{$color}-rgb: #{$value};\n }\n\n --#{$variable-prefix}white-rgb: #{to-rgb($white)};\n --#{$variable-prefix}black-rgb: #{to-rgb($black)};\n --#{$variable-prefix}body-rgb: #{to-rgb($body-color)};\n\n // Fonts\n\n // Note: Use `inspect` for lists so that quoted items keep the quotes.\n // See https://github.com/sass/sass/issues/2383#issuecomment-336349172\n --#{$variable-prefix}font-sans-serif: #{inspect($font-family-sans-serif)};\n --#{$variable-prefix}font-monospace: #{inspect($font-family-monospace)};\n --#{$variable-prefix}gradient: #{$gradient};\n\n // Root and body\n // stylelint-disable custom-property-empty-line-before\n // scss-docs-start root-body-variables\n @if $font-size-root != null {\n --#{$variable-prefix}root-font-size: #{$font-size-root};\n }\n --#{$variable-prefix}body-font-family: #{$font-family-base};\n --#{$variable-prefix}body-font-size: #{$font-size-base};\n --#{$variable-prefix}body-font-weight: #{$font-weight-base};\n --#{$variable-prefix}body-line-height: #{$line-height-base};\n --#{$variable-prefix}body-color: #{$body-color};\n @if $body-text-align != null {\n --#{$variable-prefix}body-text-align: #{$body-text-align};\n }\n --#{$variable-prefix}body-bg: #{$body-bg};\n // scss-docs-end root-body-variables\n // stylelint-enable custom-property-empty-line-before\n}\n","// stylelint-disable declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n\n// Root\n//\n// Ability to the value of the root font sizes, affecting the value of `rem`.\n// null by default, thus nothing is generated.\n\n:root {\n @if $font-size-root != null {\n font-size: var(--#{$variable-prefix}-root-font-size);\n }\n\n @if $enable-smooth-scroll {\n @media (prefers-reduced-motion: no-preference) {\n scroll-behavior: smooth;\n }\n }\n}\n\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Prevent adjustments of font size after orientation changes in iOS.\n// 4. Change the default tap highlight to be completely transparent in iOS.\n\n// scss-docs-start reboot-body-rules\nbody {\n margin: 0; // 1\n font-family: var(--#{$variable-prefix}body-font-family);\n @include font-size(var(--#{$variable-prefix}body-font-size));\n font-weight: var(--#{$variable-prefix}body-font-weight);\n line-height: var(--#{$variable-prefix}body-line-height);\n color: var(--#{$variable-prefix}body-color);\n text-align: var(--#{$variable-prefix}body-text-align);\n background-color: var(--#{$variable-prefix}body-bg); // 2\n -webkit-text-size-adjust: 100%; // 3\n -webkit-tap-highlight-color: rgba($black, 0); // 4\n}\n// scss-docs-end reboot-body-rules\n\n\n// Content grouping\n//\n// 1. Reset Firefox's gray color\n// 2. Set correct height and prevent the `size` attribute to make the `hr` look like an input field\n\nhr {\n margin: $hr-margin-y 0;\n color: $hr-color; // 1\n background-color: currentColor;\n border: 0;\n opacity: $hr-opacity;\n}\n\nhr:not([size]) {\n height: $hr-height; // 2\n}\n\n\n// Typography\n//\n// 1. Remove top margins from headings\n// By default, `

`-`

` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n\n%heading {\n margin-top: 0; // 1\n margin-bottom: $headings-margin-bottom;\n font-family: $headings-font-family;\n font-style: $headings-font-style;\n font-weight: $headings-font-weight;\n line-height: $headings-line-height;\n color: $headings-color;\n}\n\nh1 {\n @extend %heading;\n @include font-size($h1-font-size);\n}\n\nh2 {\n @extend %heading;\n @include font-size($h2-font-size);\n}\n\nh3 {\n @extend %heading;\n @include font-size($h3-font-size);\n}\n\nh4 {\n @extend %heading;\n @include font-size($h4-font-size);\n}\n\nh5 {\n @extend %heading;\n @include font-size($h5-font-size);\n}\n\nh6 {\n @extend %heading;\n @include font-size($h6-font-size);\n}\n\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\n\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-bs-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-bs-original-title] { // 1\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n text-decoration-skip-ink: none; // 4\n}\n\n\n// Address\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\n\n// Lists\n\nol,\nul {\n padding-left: 2rem;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\n// 1. Undo browser default\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // 1\n}\n\n\n// Blockquote\n\nblockquote {\n margin: 0 0 1rem;\n}\n\n\n// Strong\n//\n// Add the correct font weight in Chrome, Edge, and Safari\n\nb,\nstrong {\n font-weight: $font-weight-bolder;\n}\n\n\n// Small\n//\n// Add the correct font size in all browsers\n\nsmall {\n @include font-size($small-font-size);\n}\n\n\n// Mark\n\nmark {\n padding: $mark-padding;\n background-color: $mark-bg;\n}\n\n\n// Sub and Sup\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n\nsub,\nsup {\n position: relative;\n @include font-size($sub-sup-font-size);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n// Links\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n\n &:hover {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([class]) {\n &,\n &:hover {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n// Code\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-code;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n direction: ltr #{\"/* rtl:ignore */\"};\n unicode-bidi: bidi-override;\n}\n\n// 1. Remove browser default top margin\n// 2. Reset browser default of `1em` to use `rem`s\n// 3. Don't allow content to break outside\n\npre {\n display: block;\n margin-top: 0; // 1\n margin-bottom: 1rem; // 2\n overflow: auto; // 3\n @include font-size($code-font-size);\n color: $pre-color;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n @include font-size(inherit);\n color: inherit;\n word-break: normal;\n }\n}\n\ncode {\n @include font-size($code-font-size);\n color: $code-color;\n word-wrap: break-word;\n\n // Streamline the style when inside anchors to avoid broken underline and more\n a > & {\n color: inherit;\n }\n}\n\nkbd {\n padding: $kbd-padding-y $kbd-padding-x;\n @include font-size($kbd-font-size);\n color: $kbd-color;\n background-color: $kbd-bg;\n @include border-radius($border-radius-sm);\n\n kbd {\n padding: 0;\n @include font-size(1em);\n font-weight: $nested-kbd-font-weight;\n }\n}\n\n\n// Figures\n//\n// Apply a consistent margin strategy (matches our type styles).\n\nfigure {\n margin: 0 0 1rem;\n}\n\n\n// Images and content\n\nimg,\nsvg {\n vertical-align: middle;\n}\n\n\n// Tables\n//\n// Prevent double borders\n\ntable {\n caption-side: bottom;\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: $table-cell-padding-y;\n padding-bottom: $table-cell-padding-y;\n color: $table-caption-color;\n text-align: left;\n}\n\n// 1. Removes font-weight bold by inheriting\n// 2. Matches default `` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\n\n// Forms\n//\n// 1. Allow labels to use `margin` for spacing.\n\nlabel {\n display: inline-block; // 1\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n// See https://github.com/twbs/bootstrap/issues/24093\n\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Explicitly remove focus outline in Chromium when it shouldn't be\n// visible (e.g. as result of mouse click or touch tap). It already\n// should be doing this automatically, but seems to currently be\n// confused and applies its very visible two-tone outline anyway.\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\n// 1. Remove the margin in Firefox and Safari\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // 1\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\n// Remove the inheritance of text transform in Firefox\nbutton,\nselect {\n text-transform: none;\n}\n// Set the cursor for non-`