From 768b4de7f5136aa170c247c11542af04228eec16 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 7 Mar 2024 13:55:48 +0800 Subject: [PATCH 01/50] Improve resource subscription performance and safety (#2665) --- .../Components/Pages/ConsoleLogs.razor.cs | 25 +-- .../Components/Pages/Resources.razor.cs | 55 +++---- src/Aspire.Dashboard/Model/DashboardClient.cs | 65 ++++++-- .../Model/IDashboardClient.cs | 2 +- .../Model/ResourceOutgoingPeerResolver.cs | 19 ++- .../ApplicationModel/ResourceLoggerService.cs | 2 +- .../Dashboard/DockerContainerLogSource.cs | 2 +- src/Aspire.Hosting/Dashboard/FileLogSource.cs | 2 +- .../Dashboard/ResourceLogSource.cs | 2 +- .../Dashboard/ResourcePublisher.cs | 2 +- src/Shared/ChannelExtensions.cs | 18 +- .../ChannelExtensionsTests.cs | 154 ++++++++++++++++++ .../Model/DashboardClientTests.cs | 5 +- 13 files changed, 280 insertions(+), 73 deletions(-) create mode 100644 tests/Aspire.Dashboard.Tests/ChannelExtensionsTests.cs diff --git a/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.cs b/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.cs index 7186bfc0397..5fef37dfa60 100644 --- a/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.cs +++ b/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.cs @@ -105,18 +105,23 @@ async Task TrackResourceSnapshotsAsync() _resourceSubscriptionTask = Task.Run(async () => { - await foreach (var (changeType, resource) in subscription.WithCancellation(_resourceSubscriptionCancellation.Token)) + await foreach (var changes in subscription.WithCancellation(_resourceSubscriptionCancellation.Token)) { - await OnResourceChanged(changeType, resource); - - // the initial snapshot we obtain is [almost] never correct (it's always empty) - // we still want to select the user's initial queried resource on page load, - // so if there is no selected resource when we - // receive an added resource, and that added resource name == ResourceName, - // we should mark it as selected - if (ResourceName is not null && PageViewModel.SelectedResource is null && changeType == ResourceViewModelChangeType.Upsert && string.Equals(ResourceName, resource.Name)) + // TODO: This could be updated to be more efficent. + // It should apply on the resource changes in a batch and then update the UI. + foreach (var (changeType, resource) in changes) { - SetSelectedResourceOption(resource); + await OnResourceChanged(changeType, resource); + + // the initial snapshot we obtain is [almost] never correct (it's always empty) + // we still want to select the user's initial queried resource on page load, + // so if there is no selected resource when we + // receive an added resource, and that added resource name == ResourceName, + // we should mark it as selected + if (ResourceName is not null && PageViewModel.SelectedResource is null && changeType == ResourceViewModelChangeType.Upsert && string.Equals(ResourceName, resource.Name)) + { + SetSelectedResourceOption(resource); + } } } }); diff --git a/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs b/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs index a2d2d2fdfb3..c4632c1f702 100644 --- a/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs +++ b/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs @@ -13,7 +13,7 @@ namespace Aspire.Dashboard.Components.Pages; -public partial class Resources : ComponentBase, IDisposable +public partial class Resources : ComponentBase, IAsyncDisposable { private Subscription? _logsSubscription; private Dictionary? _applicationUnviewedErrorCounts; @@ -37,6 +37,7 @@ public partial class Resources : ComponentBase, IDisposable private readonly ConcurrentDictionary _visibleResourceTypes; private string _filter = ""; private bool _isTypeFilterVisible; + private Task? _resourceSubscriptionTask; public Resources() { @@ -139,21 +140,24 @@ async Task SubscribeResourcesAsync() } // Listen for updates and apply. - _ = Task.Run(async () => + _resourceSubscriptionTask = Task.Run(async () => { - await foreach (var (changeType, resource) in subscription.WithCancellation(_watchTaskCancellationTokenSource.Token)) + await foreach (var changes in subscription.WithCancellation(_watchTaskCancellationTokenSource.Token)) { - if (changeType == ResourceViewModelChangeType.Upsert) + foreach (var (changeType, resource) in changes) { - _resourceByName[resource.Name] = resource; - - _allResourceTypes[resource.ResourceType] = true; - _visibleResourceTypes[resource.ResourceType] = true; - } - else if (changeType == ResourceViewModelChangeType.Delete) - { - var removed = _resourceByName.TryRemove(resource.Name, out _); - Debug.Assert(removed, "Cannot remove unknown resource."); + if (changeType == ResourceViewModelChangeType.Upsert) + { + _resourceByName[resource.Name] = resource; + + _allResourceTypes[resource.ResourceType] = true; + _visibleResourceTypes[resource.ResourceType] = true; + } + else if (changeType == ResourceViewModelChangeType.Delete) + { + var removed = _resourceByName.TryRemove(resource.Name, out _); + Debug.Assert(removed, "Cannot remove unknown resource."); + } } await InvokeAsync(StateHasChanged); @@ -199,22 +203,6 @@ private bool HasMultipleReplicas(ResourceViewModel resource) return false; } - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - _watchTaskCancellationTokenSource.Cancel(); - _watchTaskCancellationTokenSource.Dispose(); - _logsSubscription?.Dispose(); - } - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - private string? GetRowClass(ResourceViewModel resource) => resource == SelectedResource ? "selected-row resource-row" : "resource-row"; @@ -251,4 +239,13 @@ private async Task ExecuteResourceCommandAsync(ResourceViewModel resource, Comma }); } } + + public async ValueTask DisposeAsync() + { + _watchTaskCancellationTokenSource.Cancel(); + _watchTaskCancellationTokenSource.Dispose(); + _logsSubscription?.Dispose(); + + await TaskHelpers.WaitIgnoreCancelAsync(_resourceSubscriptionTask); + } } diff --git a/src/Aspire.Dashboard/Model/DashboardClient.cs b/src/Aspire.Dashboard/Model/DashboardClient.cs index 505472f7bff..f8ec7826768 100644 --- a/src/Aspire.Dashboard/Model/DashboardClient.cs +++ b/src/Aspire.Dashboard/Model/DashboardClient.cs @@ -45,7 +45,7 @@ internal sealed class DashboardClient : IDashboardClient private readonly IConfiguration _configuration; private readonly ILogger _logger; - private ImmutableHashSet> _outgoingChannels = []; + private ImmutableHashSet>> _outgoingChannels = []; private string? _applicationName; private const int StateDisabled = -1; @@ -287,12 +287,8 @@ async Task WatchResourcesAsync() { foreach (var channel in _outgoingChannels) { - // TODO send batches over the channel instead of individual items? They are batched downstream however - foreach (var change in changes) - { - // Channel is unbound so TryWrite always succeeds. - channel.Writer.TryWrite(change); - } + // Channel is unbound so TryWrite always succeeds. + channel.Writer.TryWrite(changes); } } } @@ -332,7 +328,7 @@ async Task IDashboardClient.SubscribeResourcesAsy // There are two types of channel in this class. This is not a gRPC channel. // It's a producer-consumer queue channel, used to push updates to subscribers // without blocking the producer here. - var channel = Channel.CreateUnbounded( + var channel = Channel.CreateUnbounded>( new UnboundedChannelOptions { AllowSynchronousContinuations = false, SingleReader = true, SingleWriter = true }); lock (_lock) @@ -344,13 +340,20 @@ async Task IDashboardClient.SubscribeResourcesAsy Subscription: StreamUpdatesAsync(cts.Token)); } - async IAsyncEnumerable StreamUpdatesAsync([EnumeratorCancellation] CancellationToken enumeratorCancellationToken = default) + async IAsyncEnumerable> StreamUpdatesAsync([EnumeratorCancellation] CancellationToken enumeratorCancellationToken = default) { try { - await foreach (var batch in channel.Reader.ReadAllAsync(enumeratorCancellationToken).ConfigureAwait(false)) + await foreach (var batch in channel.GetBatchesAsync(minReadInterval: TimeSpan.FromMilliseconds(100), cancellationToken: enumeratorCancellationToken).ConfigureAwait(false)) { - yield return batch; + if (batch.Count == 1) + { + yield return batch[0]; + } + else + { + yield return batch.SelectMany(batch => batch).ToList(); + } } } finally @@ -371,17 +374,47 @@ async IAsyncEnumerable StreamUpdatesAsync([EnumeratorCa new WatchResourceConsoleLogsRequest() { ResourceName = resourceName }, cancellationToken: combinedTokens.Token); - await foreach (var response in call.ResponseStream.ReadAllAsync(cancellationToken: combinedTokens.Token)) + // Write incoming logs to a channel, and then read from that channel to yield the logs. + // We do this to batch logs together and enforce a minimum read interval. + var channel = Channel.CreateUnbounded>( + new UnboundedChannelOptions { AllowSynchronousContinuations = false, SingleReader = true, SingleWriter = true }); + + var readTask = Task.Run(async () => { - var logLines = new (string Content, bool IsErrorMessage)[response.LogLines.Count]; + try + { + await foreach (var response in call.ResponseStream.ReadAllAsync(cancellationToken: combinedTokens.Token)) + { + var logLines = new (string Content, bool IsErrorMessage)[response.LogLines.Count]; - for (var i = 0; i < logLines.Length; i++) + for (var i = 0; i < logLines.Length; i++) + { + logLines[i] = (response.LogLines[i].Text, response.LogLines[i].IsStdErr); + } + + // Channel is unbound so TryWrite always succeeds. + channel.Writer.TryWrite(logLines); + } + } + finally { - logLines[i] = (response.LogLines[i].Text, response.LogLines[i].IsStdErr); + channel.Writer.TryComplete(); } + }, combinedTokens.Token); - yield return logLines; + await foreach (var batch in channel.GetBatchesAsync(TimeSpan.FromMilliseconds(100), combinedTokens.Token)) + { + if (batch.Count == 1) + { + yield return batch[0]; + } + else + { + yield return batch.SelectMany(batch => batch).ToList(); + } } + + await readTask.ConfigureAwait(false); } public async Task ExecuteResourceCommandAsync(string resourceName, string resourceType, CommandViewModel command, CancellationToken cancellationToken) diff --git a/src/Aspire.Dashboard/Model/IDashboardClient.cs b/src/Aspire.Dashboard/Model/IDashboardClient.cs index fa8abc6f3e5..8118692dfb8 100644 --- a/src/Aspire.Dashboard/Model/IDashboardClient.cs +++ b/src/Aspire.Dashboard/Model/IDashboardClient.cs @@ -56,7 +56,7 @@ public interface IDashboardClient : IAsyncDisposable public sealed record ResourceViewModelSubscription( ImmutableArray InitialState, - IAsyncEnumerable Subscription); + IAsyncEnumerable> Subscription); public sealed record ResourceViewModelChange( ResourceViewModelChangeType ChangeType, diff --git a/src/Aspire.Dashboard/Model/ResourceOutgoingPeerResolver.cs b/src/Aspire.Dashboard/Model/ResourceOutgoingPeerResolver.cs index 2e71cbf769f..4b995db96c8 100644 --- a/src/Aspire.Dashboard/Model/ResourceOutgoingPeerResolver.cs +++ b/src/Aspire.Dashboard/Model/ResourceOutgoingPeerResolver.cs @@ -34,16 +34,19 @@ public ResourceOutgoingPeerResolver(IDashboardClient resourceService) Debug.Assert(added, "Should not receive duplicate resources in initial snapshot data."); } - await foreach (var (changeType, resource) in subscription.WithCancellation(_watchContainersTokenSource.Token)) + await foreach (var changes in subscription.WithCancellation(_watchContainersTokenSource.Token)) { - if (changeType == ResourceViewModelChangeType.Upsert) + foreach (var (changeType, resource) in changes) { - _resourceByName[resource.Name] = resource; - } - else if (changeType == ResourceViewModelChangeType.Delete) - { - var removed = _resourceByName.TryRemove(resource.Name, out _); - Debug.Assert(removed, "Cannot remove unknown resource."); + if (changeType == ResourceViewModelChangeType.Upsert) + { + _resourceByName[resource.Name] = resource; + } + else if (changeType == ResourceViewModelChangeType.Delete) + { + var removed = _resourceByName.TryRemove(resource.Name, out _); + Debug.Assert(removed, "Cannot remove unknown resource."); + } } await RaisePeerChangesAsync().ConfigureAwait(false); diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceLoggerService.cs b/src/Aspire.Hosting/ApplicationModel/ResourceLoggerService.cs index 009f33f53b0..13bc79e1e68 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceLoggerService.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceLoggerService.cs @@ -151,7 +151,7 @@ void Log((string Content, bool IsErrorMessage) log) try { - await foreach (var entry in channel.GetBatchesAsync(cancellationToken)) + await foreach (var entry in channel.GetBatchesAsync(cancellationToken: cancellationToken)) { yield return entry; } diff --git a/src/Aspire.Hosting/Dashboard/DockerContainerLogSource.cs b/src/Aspire.Hosting/Dashboard/DockerContainerLogSource.cs index f9f9541dbb6..cc456e35de3 100644 --- a/src/Aspire.Hosting/Dashboard/DockerContainerLogSource.cs +++ b/src/Aspire.Hosting/Dashboard/DockerContainerLogSource.cs @@ -45,7 +45,7 @@ internal sealed class DockerContainerLogSource(string containerId) : IAsyncEnume // Don't forward cancellationToken here, because it's handled internally in WaitForExit _ = Task.Run(() => WaitForExit(tcs, ctr), CancellationToken.None); - await foreach (var batch in channel.GetBatchesAsync(cancellationToken)) + await foreach (var batch in channel.GetBatchesAsync(cancellationToken: cancellationToken)) { yield return batch; } diff --git a/src/Aspire.Hosting/Dashboard/FileLogSource.cs b/src/Aspire.Hosting/Dashboard/FileLogSource.cs index c52e57524d7..ea7c7158687 100644 --- a/src/Aspire.Hosting/Dashboard/FileLogSource.cs +++ b/src/Aspire.Hosting/Dashboard/FileLogSource.cs @@ -27,7 +27,7 @@ internal sealed partial class FileLogSource(string stdOutPath, string stdErrPath var stdOut = Task.Run(() => WatchFileAsync(stdOutPath, isError: false), cancellationToken); var stdErr = Task.Run(() => WatchFileAsync(stdErrPath, isError: true), cancellationToken); - await foreach (var batch in channel.GetBatchesAsync(cancellationToken)) + await foreach (var batch in channel.GetBatchesAsync(cancellationToken: cancellationToken)) { yield return batch; } diff --git a/src/Aspire.Hosting/Dashboard/ResourceLogSource.cs b/src/Aspire.Hosting/Dashboard/ResourceLogSource.cs index 45bdbf3f712..8d9d069d9b2 100644 --- a/src/Aspire.Hosting/Dashboard/ResourceLogSource.cs +++ b/src/Aspire.Hosting/Dashboard/ResourceLogSource.cs @@ -47,7 +47,7 @@ public async IAsyncEnumerator GetAsyncEnumerator(CancellationToken TaskContinuationOptions.None, TaskScheduler.Default).ConfigureAwait(false); - await foreach (var batch in channel.GetBatchesAsync(cancellationToken)) + await foreach (var batch in channel.GetBatchesAsync(cancellationToken: cancellationToken)) { yield return batch; } diff --git a/src/Aspire.Hosting/Dashboard/ResourcePublisher.cs b/src/Aspire.Hosting/Dashboard/ResourcePublisher.cs index 67d2a20f634..479d5fd2193 100644 --- a/src/Aspire.Hosting/Dashboard/ResourcePublisher.cs +++ b/src/Aspire.Hosting/Dashboard/ResourcePublisher.cs @@ -49,7 +49,7 @@ async IAsyncEnumerable> StreamUpdates([Enu try { - await foreach (var batch in channel.GetBatchesAsync(linked.Token).ConfigureAwait(false)) + await foreach (var batch in channel.GetBatchesAsync(cancellationToken: linked.Token).ConfigureAwait(false)) { yield return batch; } diff --git a/src/Shared/ChannelExtensions.cs b/src/Shared/ChannelExtensions.cs index 231b94b1c40..81a0c1a25fa 100644 --- a/src/Shared/ChannelExtensions.cs +++ b/src/Shared/ChannelExtensions.cs @@ -3,6 +3,7 @@ using System.Runtime.CompilerServices; using System.Threading.Channels; +using Aspire.Dashboard.Otlp.Storage; namespace Aspire; @@ -19,19 +20,31 @@ internal static class ChannelExtensions /// /// The type of items in the channel and returned batch. /// The channel to read values from. + /// The minimum read interval. The enumerable will wait this long before returning the next available result. /// A token that signals a loss of interest in the operation. /// public static async IAsyncEnumerable> GetBatchesAsync( this Channel channel, - [EnumeratorCancellation] CancellationToken cancellationToken) + TimeSpan? minReadInterval = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) { + DateTime? lastRead = null; + while (!cancellationToken.IsCancellationRequested) { List? batch = null; - // Wait until there's something to read, or the channel closes. if (await channel.Reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false)) { + if (minReadInterval != null && lastRead != null) + { + var s = lastRead.Value.Add(minReadInterval.Value) - DateTime.UtcNow; + if (s > TimeSpan.Zero) + { + await Task.Delay(s, cancellationToken).ConfigureAwait(false); + } + } + // Read everything in the channel into a batch. while (!cancellationToken.IsCancellationRequested && channel.Reader.TryRead(out var log)) { @@ -41,6 +54,7 @@ public static async IAsyncEnumerable> GetBatchesAsync( if (!cancellationToken.IsCancellationRequested && batch is not null) { + lastRead = DateTime.UtcNow; yield return batch; } } diff --git a/tests/Aspire.Dashboard.Tests/ChannelExtensionsTests.cs b/tests/Aspire.Dashboard.Tests/ChannelExtensionsTests.cs new file mode 100644 index 00000000000..0142e878bd0 --- /dev/null +++ b/tests/Aspire.Dashboard.Tests/ChannelExtensionsTests.cs @@ -0,0 +1,154 @@ +// 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.Threading.Channels; +using Aspire.Dashboard.Utils; +using Xunit; + +namespace Aspire.Dashboard.Tests; + +public class ChannelExtensionsTests +{ + [Fact] + public async Task GetBatchesAsync_CancellationToken_Exits() + { + // Arrange + var cts = new CancellationTokenSource(); + var channel = Channel.CreateUnbounded>(); + + channel.Writer.TryWrite(["a", "b", "c"]); + + // Act + IReadOnlyList>? readBatch = null; + var readTask = Task.Run(async () => + { + await foreach (var batch in channel.GetBatchesAsync(cancellationToken: cts.Token)) + { + readBatch = batch; + cts.Cancel(); + } + }); + + // Assert + await TaskHelpers.WaitIgnoreCancelAsync(readTask); + } + + [Fact] + public async Task GetBatchesAsync_WithCancellation_Exits() + { + // Arrange + var cts = new CancellationTokenSource(); + var channel = Channel.CreateUnbounded>(); + + channel.Writer.TryWrite(["a", "b", "c"]); + + // Act + IReadOnlyList>? readBatch = null; + var readTask = Task.Run(async () => + { + await foreach (var batch in channel.GetBatchesAsync().WithCancellation(cts.Token)) + { + readBatch = batch; + cts.Cancel(); + } + }); + + // Assert + await TaskHelpers.WaitIgnoreCancelAsync(readTask); + } + + [Fact] + public async Task GetBatchesAsync_MinReadInterval_WaitForNextRead() + { + // Arrange + var cts = new CancellationTokenSource(); + var channel = Channel.CreateUnbounded>(); + var resultChannel = Channel.CreateUnbounded>>(); + var minReadInterval = TimeSpan.FromMilliseconds(500); + + channel.Writer.TryWrite(["a", "b", "c"]); + + // Act + var readTask = Task.Run(async () => + { + try + { + await foreach (var batch in channel.GetBatchesAsync(minReadInterval).WithCancellation(cts.Token)) + { + resultChannel.Writer.TryWrite(batch); + } + } + finally + { + resultChannel.Writer.Complete(); + } + }); + + // Assert + var stopwatch = Stopwatch.StartNew(); + var read1 = await resultChannel.Reader.ReadAsync(); + Assert.Equal(["a", "b", "c"], read1.Single()); + + channel.Writer.TryWrite(["d", "e", "f"]); + + var read2 = await resultChannel.Reader.ReadAsync(); + Assert.Equal(["d", "e", "f"], read2.Single()); + + var elapsed = stopwatch.Elapsed; + Assert.True(elapsed >= minReadInterval, $"Elapsed time {elapsed} should be greater than min read interval {minReadInterval} on read."); + + channel.Writer.Complete(); + await TaskHelpers.WaitIgnoreCancelAsync(readTask); + } + + [Fact] + public async Task GetBatchesAsync_MinReadInterval_WithCancellation_Exit() + { + // Arrange + var cts = new CancellationTokenSource(); + var channel = Channel.CreateUnbounded>(); + var resultChannel = Channel.CreateUnbounded>>(); + var minReadInterval = TimeSpan.FromMilliseconds(50000); + + channel.Writer.TryWrite(["a", "b", "c"]); + + // Act + var readTask = Task.Run(async () => + { + try + { + await foreach (var batch in channel.GetBatchesAsync(minReadInterval).WithCancellation(cts.Token)) + { + resultChannel.Writer.TryWrite(batch); + } + } + finally + { + resultChannel.Writer.Complete(); + } + }); + + // Assert + var stopwatch = Stopwatch.StartNew(); + var read1 = await resultChannel.Reader.ReadAsync(); + Assert.Equal(["a", "b", "c"], read1.Single()); + + channel.Writer.TryWrite(["d", "e", "f"]); + + var read2Task = resultChannel.Reader.ReadAsync(); + cts.Cancel(); + + await TaskHelpers.WaitIgnoreCancelAsync(readTask); + try + { + await read2Task; + } + catch (ChannelClosedException) + { + } + + var elapsed = stopwatch.Elapsed; + Assert.True(elapsed <= minReadInterval, $"Elapsed time {elapsed} should be less than min read interval {minReadInterval} on cancellation."); + } +} diff --git a/tests/Aspire.Dashboard.Tests/Model/DashboardClientTests.cs b/tests/Aspire.Dashboard.Tests/Model/DashboardClientTests.cs index 0e0ee7eeec8..236270182f9 100644 --- a/tests/Aspire.Dashboard.Tests/Model/DashboardClientTests.cs +++ b/tests/Aspire.Dashboard.Tests/Model/DashboardClientTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Dashboard.Model; +using Aspire.Dashboard.Utils; using Aspire.V1; using Google.Protobuf.WellKnownTypes; using Microsoft.Extensions.Configuration; @@ -46,7 +47,7 @@ public async Task SubscribeResources_OnCancel_ChannelRemoved() await cts.CancelAsync(); - await Assert.ThrowsAnyAsync(() => readTask).ConfigureAwait(false); + await TaskHelpers.WaitIgnoreCancelAsync(readTask); Assert.Equal(0, instance.OutgoingResourceSubscriberCount); } @@ -76,7 +77,7 @@ public async Task SubscribeResources_OnDispose_ChannelRemoved() Assert.Equal(0, instance.OutgoingResourceSubscriberCount); - await Assert.ThrowsAnyAsync(() => readTask).ConfigureAwait(false); + await TaskHelpers.WaitIgnoreCancelAsync(readTask); } [Fact] From e8440ab2b94ef2b0fb0784c09c56c3de5a01cb39 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 7 Mar 2024 14:24:51 +0800 Subject: [PATCH 02/50] Attempt to fix flaky test cert issue (#2696) --- .../Integration/IntegrationTestHelpers.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/Aspire.Dashboard.Tests/Integration/IntegrationTestHelpers.cs b/tests/Aspire.Dashboard.Tests/Integration/IntegrationTestHelpers.cs index 4933d46c720..4235a23368d 100644 --- a/tests/Aspire.Dashboard.Tests/Integration/IntegrationTestHelpers.cs +++ b/tests/Aspire.Dashboard.Tests/Integration/IntegrationTestHelpers.cs @@ -5,6 +5,7 @@ using Grpc.Core; using Grpc.Net.Client; using Grpc.Net.Client.Configuration; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -16,15 +17,15 @@ namespace Aspire.Dashboard.Tests.Integration; public static class IntegrationTestHelpers { + private static readonly X509Certificate2 s_testCertificate = TestCertificateLoader.GetTestCertificate(); + public static DashboardWebApplication CreateDashboardWebApplication(ITestOutputHelper testOutputHelper, ITestSink? testSink = null) { var config = new ConfigurationManager() .AddInMemoryCollection(new Dictionary { ["ASPNETCORE_URLS"] = "https://127.0.0.1:0", - ["DOTNET_DASHBOARD_OTLP_ENDPOINT_URL"] = "http://127.0.0.1:0", - ["Kestrel:Certificates:Default:Path"] = TestCertificateLoader.TestCertificatePath, - ["Kestrel:Certificates:Default:Password"] = "testPassword" + ["DOTNET_DASHBOARD_OTLP_ENDPOINT_URL"] = "http://127.0.0.1:0" }).Build(); var dashboardWebApplication = new DashboardWebApplication(builder => @@ -40,6 +41,13 @@ public static DashboardWebApplication CreateDashboardWebApplication(ITestOutputH { builder.Logging.AddProvider(new TestLoggerProvider(testSink)); } + builder.WebHost.ConfigureKestrel(serverOptions => + { + serverOptions.ConfigureHttpsDefaults(options => + { + options.ServerCertificate = s_testCertificate; + }); + }); }); return dashboardWebApplication; From 07a5044c1f512bf3034259cd03224e7179e964d6 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 7 Mar 2024 15:07:15 +0800 Subject: [PATCH 03/50] Only update resources UI from logs changed event if error counts change (#2697) --- .../Components/Pages/Resources.razor.cs | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs b/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs index c4632c1f702..091a9dca3a4 100644 --- a/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs +++ b/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs @@ -120,8 +120,14 @@ protected override async Task OnInitializedAsync() _logsSubscription = TelemetryRepository.OnNewLogs(null, SubscriptionType.Other, async () => { - _applicationUnviewedErrorCounts = TelemetryRepository.GetApplicationUnviewedErrorLogsCount(); - await InvokeAsync(StateHasChanged); + var newApplicationUnviewedErrorCounts = TelemetryRepository.GetApplicationUnviewedErrorLogsCount(); + + // Only update UI if the error counts have changed. + if (ApplicationErrorCountsChanged(newApplicationUnviewedErrorCounts)) + { + _applicationUnviewedErrorCounts = newApplicationUnviewedErrorCounts; + await InvokeAsync(StateHasChanged); + } }); async Task SubscribeResourcesAsync() @@ -166,6 +172,24 @@ async Task SubscribeResourcesAsync() } } + private bool ApplicationErrorCountsChanged(Dictionary newApplicationUnviewedErrorCounts) + { + if (_applicationUnviewedErrorCounts == null || _applicationUnviewedErrorCounts.Count != newApplicationUnviewedErrorCounts.Count) + { + return true; + } + + foreach (var (application, count) in newApplicationUnviewedErrorCounts) + { + if (!_applicationUnviewedErrorCounts.TryGetValue(application, out var oldCount) || oldCount != count) + { + return true; + } + } + + return false; + } + private void ShowResourceDetails(ResourceViewModel resource) { if (SelectedResource == resource) From e2a44b12c294e581fdd470f3ed9b98f7ba0115fe Mon Sep 17 00:00:00 2001 From: David Fowler Date: Wed, 6 Mar 2024 23:40:37 -0800 Subject: [PATCH 04/50] Don't wait for dashboard to be ready (#2699) - Remove the logic that waits for the dashboard to be ready --- src/Aspire.Hosting/Dcp/ApplicationExecutor.cs | 30 ++--------- .../Dcp/HttpPingDashboardAvailability.cs | 53 ------------------- .../Dcp/IDashboardAvailability.cs | 9 ---- .../DistributedApplicationBuilder.cs | 1 - .../Dcp/ApplicationExecutorTests.cs | 1 - .../Dcp/MockDashboardAvailability.cs | 14 ----- 6 files changed, 3 insertions(+), 105 deletions(-) delete mode 100644 src/Aspire.Hosting/Dcp/HttpPingDashboardAvailability.cs delete mode 100644 src/Aspire.Hosting/Dcp/IDashboardAvailability.cs delete mode 100644 tests/Aspire.Hosting.Tests/Dcp/MockDashboardAvailability.cs diff --git a/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs b/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs index 8a4157d737d..c6b69aa4f7a 100644 --- a/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs +++ b/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs @@ -59,7 +59,6 @@ internal sealed class ApplicationExecutor(ILogger logger, IConfiguration configuration, IOptions options, IDashboardEndpointProvider dashboardEndpointProvider, - IDashboardAvailability dashboardAvailability, DistributedApplicationExecutionContext executionContext, ResourceNotificationService notificationService, ResourceLoggerService loggerService) @@ -71,7 +70,6 @@ internal sealed class ApplicationExecutor(ILogger logger, private readonly IDistributedApplicationLifecycleHook[] _lifecycleHooks = lifecycleHooks.ToArray(); private readonly IOptions _options = options; private readonly IDashboardEndpointProvider _dashboardEndpointProvider = dashboardEndpointProvider; - private readonly IDashboardAvailability _dashboardAvailability = dashboardAvailability; private readonly DistributedApplicationExecutionContext _executionContext = executionContext; private readonly List _appResources = []; @@ -222,37 +220,15 @@ private async Task StartDashboardAsDcpExecutableAsync(CancellationToken cancella }; await kubernetesService.CreateAsync(dashboardExecutable, cancellationToken).ConfigureAwait(false); - await CheckDashboardAvailabilityAsync(dashboardUrls, cancellationToken).ConfigureAwait(false); + PrintDashboardUrls(dashboardUrls); } - private TimeSpan DashboardAvailabilityTimeoutDuration - { - get - { - if (configuration["DOTNET_ASPIRE_DASHBOARD_TIMEOUT_SECONDS"] is { } timeoutString && int.TryParse(timeoutString, out var timeoutInSeconds)) - { - return TimeSpan.FromSeconds(timeoutInSeconds); - } - else - { - return TimeSpan.FromSeconds(DefaultDashboardAvailabilityTimeoutDurationInSeconds); - } - } - } - - private const int DefaultDashboardAvailabilityTimeoutDurationInSeconds = 60; - - private async Task CheckDashboardAvailabilityAsync(string delimitedUrlList, CancellationToken cancellationToken) + private void PrintDashboardUrls(string delimitedUrlList) { if (StringUtils.TryGetUriFromDelimitedString(delimitedUrlList, ";", out var firstDashboardUrl)) { - await _dashboardAvailability.WaitForDashboardAvailabilityAsync(firstDashboardUrl, DashboardAvailabilityTimeoutDuration, cancellationToken).ConfigureAwait(false); distributedApplicationLogger.LogInformation("Now listening on: {DashboardUrl}", firstDashboardUrl.ToString().TrimEnd('/')); } - else - { - _logger.LogWarning("Skipping dashboard availability check because ASPNETCORE_URLS environment variable could not be parsed."); - } } private async Task CreateServicesAsync(CancellationToken cancellationToken = default) @@ -659,7 +635,7 @@ private async Task CreateExecutableAsync(AppResource er, ILogger resourceLogger, throw new DistributedApplicationException("Cannot check dashboard availability since ASPNETCORE_URLS environment variable not set."); } - await CheckDashboardAvailabilityAsync(dashboardUrls, cancellationToken).ConfigureAwait(false); + PrintDashboardUrls(dashboardUrls); } } diff --git a/src/Aspire.Hosting/Dcp/HttpPingDashboardAvailability.cs b/src/Aspire.Hosting/Dcp/HttpPingDashboardAvailability.cs deleted file mode 100644 index 11316bc9544..00000000000 --- a/src/Aspire.Hosting/Dcp/HttpPingDashboardAvailability.cs +++ /dev/null @@ -1,53 +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.Extensions.Logging; - -namespace Aspire.Hosting.Dcp; - -internal sealed class HttpPingDashboardAvailability : IDashboardAvailability -{ - private readonly ILogger _logger; - - public HttpPingDashboardAvailability(ILogger logger) - { - _logger = logger; - } - - public async Task WaitForDashboardAvailabilityAsync(Uri url, TimeSpan timeout, CancellationToken cancellationToken = default) - { - using var timeoutCts = new CancellationTokenSource(timeout); - using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); - - var client = new HttpClient(); - - try - { - while (true) - { - cancellationToken.ThrowIfCancellationRequested(); - - try - { - var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, linkedCts.Token).ConfigureAwait(false); - - if (response.IsSuccessStatusCode) - { - return; - } - } - catch (Exception ex) - { - _logger.LogDebug(ex, "Dashboard not ready yet."); - } - - await Task.Delay(TimeSpan.FromMilliseconds(50), linkedCts.Token).ConfigureAwait(false); - } - } - catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested) - { - // Only display this error if the timeout CTS was the one that was cancelled. - throw new DistributedApplicationException($"Timed out after {timeout} while waiting for the dashboard to be responsive."); - } - } -} diff --git a/src/Aspire.Hosting/Dcp/IDashboardAvailability.cs b/src/Aspire.Hosting/Dcp/IDashboardAvailability.cs deleted file mode 100644 index b932a18fc04..00000000000 --- a/src/Aspire.Hosting/Dcp/IDashboardAvailability.cs +++ /dev/null @@ -1,9 +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 Aspire.Hosting.Dcp; - -internal interface IDashboardAvailability -{ - Task WaitForDashboardAvailabilityAsync(Uri url, TimeSpan timeout, CancellationToken cancellationToken = default); -} diff --git a/src/Aspire.Hosting/DistributedApplicationBuilder.cs b/src/Aspire.Hosting/DistributedApplicationBuilder.cs index 124cba4f3b3..924b481d0b5 100644 --- a/src/Aspire.Hosting/DistributedApplicationBuilder.cs +++ b/src/Aspire.Hosting/DistributedApplicationBuilder.cs @@ -91,7 +91,6 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options) // DCP stuff _innerBuilder.Services.AddSingleton(); _innerBuilder.Services.AddSingleton(); - _innerBuilder.Services.AddSingleton(); _innerBuilder.Services.AddSingleton(); _innerBuilder.Services.AddHostedService(); diff --git a/tests/Aspire.Hosting.Tests/Dcp/ApplicationExecutorTests.cs b/tests/Aspire.Hosting.Tests/Dcp/ApplicationExecutorTests.cs index 2306434a1e9..836e6a42c51 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/ApplicationExecutorTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/ApplicationExecutorTests.cs @@ -71,7 +71,6 @@ private static ApplicationExecutor CreateAppExecutor( DashboardPath = "./dashboard" }), new MockDashboardEndpointProvider(), - new MockDashboardAvailability(), new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run), new ResourceNotificationService(), new ResourceLoggerService() diff --git a/tests/Aspire.Hosting.Tests/Dcp/MockDashboardAvailability.cs b/tests/Aspire.Hosting.Tests/Dcp/MockDashboardAvailability.cs deleted file mode 100644 index 3c94c8a69b2..00000000000 --- a/tests/Aspire.Hosting.Tests/Dcp/MockDashboardAvailability.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 Aspire.Hosting.Dcp; - -namespace Aspire.Hosting.Tests.Dcp; - -internal sealed class MockDashboardAvailability : IDashboardAvailability -{ - public Task WaitForDashboardAvailabilityAsync(Uri url, TimeSpan timeout, CancellationToken cancellationToken = default) - { - return Task.CompletedTask; - } -} From 3571424167e2c99005f4b0bee84c7f35e7488920 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 7 Mar 2024 15:53:17 +0800 Subject: [PATCH 05/50] Improve telemetry subscription performance and safety (#2672) --- .../Otlp/Storage/Subscription.cs | 60 +++++++++++++++-- .../Otlp/Storage/TelemetryRepository.cs | 18 ++--- .../TelemetryRepositoryTests/LogTests.cs | 65 +++++++++++++++++++ .../TelemetryRepositoryTests/TestHelpers.cs | 10 ++- 4 files changed, 139 insertions(+), 14 deletions(-) diff --git a/src/Aspire.Dashboard/Otlp/Storage/Subscription.cs b/src/Aspire.Dashboard/Otlp/Storage/Subscription.cs index 4048f71d138..35cd10a2ad6 100644 --- a/src/Aspire.Dashboard/Otlp/Storage/Subscription.cs +++ b/src/Aspire.Dashboard/Otlp/Storage/Subscription.cs @@ -7,20 +7,60 @@ public sealed class Subscription : IDisposable { private readonly Func _callback; private readonly ExecutionContext? _executionContext; - private readonly ILogger _logger; + private readonly TelemetryRepository _telemetryRepository; + private readonly CancellationTokenSource _cts; + private readonly CancellationToken _cancellationToken; private readonly Action _unsubscribe; + private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); + private ILogger Logger => _telemetryRepository._logger; + + private DateTime? _lastExecute; public string? ApplicationId { get; } public SubscriptionType SubscriptionType { get; } + public string Name { get; } - public Subscription(string? applicationId, SubscriptionType subscriptionType, Func callback, Action unsubscribe, ExecutionContext? executionContext, ILogger logger) + public Subscription(string name, string? applicationId, SubscriptionType subscriptionType, Func callback, Action unsubscribe, ExecutionContext? executionContext, TelemetryRepository telemetryRepository) { + Name = name; ApplicationId = applicationId; SubscriptionType = subscriptionType; _callback = callback; _unsubscribe = unsubscribe; _executionContext = executionContext; - _logger = logger; + _telemetryRepository = telemetryRepository; + _cts = new CancellationTokenSource(); + _cancellationToken = _cts.Token; + } + + private async Task TryQueueAsync(CancellationToken cancellationToken) + { + var success = _lock.Wait(0, cancellationToken); + if (!success) + { + Logger.LogDebug("Subscription '{Name}' update already queued.", Name); + return false; + } + + try + { + var lastExecute = _lastExecute; + if (lastExecute != null) + { + var s = lastExecute.Value.Add(_telemetryRepository._subscriptionMinExecuteInterval) - DateTime.UtcNow; + if (s > TimeSpan.Zero) + { + Logger.LogDebug("Subscription '{Name}' minimum execute interval hit. Waiting {DelayInterval}.", Name, s); + await Task.Delay(s, cancellationToken).ConfigureAwait(false); + } + } + + return true; + } + finally + { + _lock.Release(); + } } public void Execute() @@ -29,6 +69,13 @@ public void Execute() // The caller doesn't want to wait while the subscription is running or receive exceptions. _ = Task.Run(async () => { + // Try to queue the subscription callback. + // If another caller is already in the queue then exit without calling the callback. + if (!await TryQueueAsync(_cancellationToken).ConfigureAwait(false)) + { + return; + } + try { // Set the execution context to the one captured when the subscription was created. @@ -42,11 +89,13 @@ public void Execute() ExecutionContext.Restore(_executionContext); } + Logger.LogDebug("Subscription '{Name}' executing.", Name); await _callback().ConfigureAwait(false); + _lastExecute = DateTime.UtcNow; } catch (Exception ex) { - _logger.LogError(ex, "Error in subscription callback"); + Logger.LogError(ex, "Error in subscription callback"); } }); } @@ -54,5 +103,8 @@ public void Execute() public void Dispose() { _unsubscribe(); + _cts.Cancel(); + _cts.Dispose(); + _lock.Dispose(); } } diff --git a/src/Aspire.Dashboard/Otlp/Storage/TelemetryRepository.cs b/src/Aspire.Dashboard/Otlp/Storage/TelemetryRepository.cs index ff4e0f37a8e..96575b9baf6 100644 --- a/src/Aspire.Dashboard/Otlp/Storage/TelemetryRepository.cs +++ b/src/Aspire.Dashboard/Otlp/Storage/TelemetryRepository.cs @@ -40,7 +40,9 @@ public sealed class TelemetryRepository private const int DefaultSpanEventCountLimit = int.MaxValue; private readonly object _lock = new(); - private readonly ILogger _logger; + internal readonly ILogger _logger; + internal TimeSpan _subscriptionMinExecuteInterval = TimeSpan.FromMilliseconds(100); + private readonly TelemetryOptions _options; private readonly List _applicationSubscriptions = new(); private readonly List _logSubscriptions = new(); @@ -208,34 +210,34 @@ public OtlpApplication GetOrAddApplication(Resource resource) public Subscription OnNewApplications(Func callback) { - return AddSubscription(string.Empty, SubscriptionType.Read, callback, _applicationSubscriptions); + return AddSubscription(nameof(OnNewApplications), string.Empty, SubscriptionType.Read, callback, _applicationSubscriptions); } public Subscription OnNewLogs(string? applicationId, SubscriptionType subscriptionType, Func callback) { - return AddSubscription(applicationId, subscriptionType, callback, _logSubscriptions); + return AddSubscription(nameof(OnNewLogs), applicationId, subscriptionType, callback, _logSubscriptions); } public Subscription OnNewMetrics(string? applicationId, SubscriptionType subscriptionType, Func callback) { - return AddSubscription(applicationId, subscriptionType, callback, _metricsSubscriptions); + return AddSubscription(nameof(OnNewMetrics), applicationId, subscriptionType, callback, _metricsSubscriptions); } public Subscription OnNewTraces(string? applicationId, SubscriptionType subscriptionType, Func callback) { - return AddSubscription(applicationId, subscriptionType, callback, _tracesSubscriptions); + return AddSubscription(nameof(OnNewTraces), applicationId, subscriptionType, callback, _tracesSubscriptions); } - private Subscription AddSubscription(string? applicationId, SubscriptionType subscriptionType, Func callback, List subscriptions) + private Subscription AddSubscription(string name, string? applicationId, SubscriptionType subscriptionType, Func callback, List subscriptions) { Subscription? subscription = null; - subscription = new Subscription(applicationId, subscriptionType, callback, () => + subscription = new Subscription(name, applicationId, subscriptionType, callback, () => { lock (_lock) { subscriptions.Remove(subscription!); } - }, ExecutionContext.Capture(), _logger); + }, ExecutionContext.Capture(), this); lock (_lock) { diff --git a/tests/Aspire.Dashboard.Tests/TelemetryRepositoryTests/LogTests.cs b/tests/Aspire.Dashboard.Tests/TelemetryRepositoryTests/LogTests.cs index 6943c7f7388..9d6e093cf02 100644 --- a/tests/Aspire.Dashboard.Tests/TelemetryRepositoryTests/LogTests.cs +++ b/tests/Aspire.Dashboard.Tests/TelemetryRepositoryTests/LogTests.cs @@ -1,6 +1,8 @@ // 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.Threading.Channels; using Aspire.Dashboard.Otlp.Model; using Aspire.Dashboard.Otlp.Storage; using Google.Protobuf.Collections; @@ -643,4 +645,67 @@ public void AddLogs_AttributeLimits_LimitsApplied() }); }); } + + [Fact] + public async Task Subscription_MultipleUpdates_MinExecuteIntervalApplied() + { + // Arrange + var minExecuteInterval = TimeSpan.FromMilliseconds(500); + var repository = CreateRepository(subscriptionMinExecuteInterval: minExecuteInterval); + + var callCount = 0; + var resultChannel = Channel.CreateUnbounded(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var subscription = repository.OnNewLogs(applicationId: null, SubscriptionType.Read, () => + { + ++callCount; + resultChannel.Writer.TryWrite(callCount); + return Task.CompletedTask; + }); + + // Act + var addContext = new AddContext(); + repository.AddLogs(addContext, new RepeatedField() + { + new ResourceLogs + { + Resource = CreateResource(), + ScopeLogs = + { + new ScopeLogs + { + Scope = CreateScope("TestLogger"), + LogRecords = { CreateLogRecord() } + } + } + } + }); + + // Assert + var stopwatch = Stopwatch.StartNew(); + var read1 = await resultChannel.Reader.ReadAsync(); + Assert.Equal(1, read1); + + repository.AddLogs(addContext, new RepeatedField() + { + new ResourceLogs + { + Resource = CreateResource(), + ScopeLogs = + { + new ScopeLogs + { + Scope = CreateScope("TestLogger"), + LogRecords = { CreateLogRecord() } + } + } + } + }); + + var read2 = await resultChannel.Reader.ReadAsync(); + Assert.Equal(2, read2); + + var elapsed = stopwatch.Elapsed; + Assert.True(elapsed >= minExecuteInterval, $"Elapsed time {elapsed} should be greater than min execute interval {minExecuteInterval} on read."); + } } diff --git a/tests/Aspire.Dashboard.Tests/TelemetryRepositoryTests/TestHelpers.cs b/tests/Aspire.Dashboard.Tests/TelemetryRepositoryTests/TestHelpers.cs index 83945a6cd03..f6520674b08 100644 --- a/tests/Aspire.Dashboard.Tests/TelemetryRepositoryTests/TestHelpers.cs +++ b/tests/Aspire.Dashboard.Tests/TelemetryRepositoryTests/TestHelpers.cs @@ -192,7 +192,8 @@ public static TelemetryRepository CreateRepository( int? metricsCountLimit = null, int? attributeCountLimit = null, int? attributeLengthLimit = null, - int? spanEventCountLimit = null) + int? spanEventCountLimit = null, + TimeSpan? subscriptionMinExecuteInterval = null) { var inMemorySettings = new Dictionary(); if (metricsCountLimit != null) @@ -216,7 +217,12 @@ public static TelemetryRepository CreateRepository( .AddInMemoryCollection(inMemorySettings) .Build(); - return new TelemetryRepository(configuration, NullLoggerFactory.Instance); + var repository = new TelemetryRepository(configuration, NullLoggerFactory.Instance); + if (subscriptionMinExecuteInterval != null) + { + repository._subscriptionMinExecuteInterval = subscriptionMinExecuteInterval.Value; + } + return repository; } public static ulong DateTimeToUnixNanoseconds(DateTime dateTime) From 9bed9204c8ae7c0f6e89f37d6f8ee79deb991ccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mats=20Dyr=C3=B8y?= Date: Thu, 7 Mar 2024 09:02:06 +0100 Subject: [PATCH 06/50] Remove ellipsis from logs and details buttons (#2676) --- src/Aspire.Dashboard/Components/Pages/Resources.razor | 4 ++-- .../Components/Pages/StructuredLogs.razor | 2 +- src/Aspire.Dashboard/Components/Pages/TraceDetail.razor | 2 +- src/Aspire.Dashboard/Components/Pages/Traces.razor | 2 +- src/Aspire.Dashboard/wwwroot/css/app.css | 9 +++++++++ 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Aspire.Dashboard/Components/Pages/Resources.razor b/src/Aspire.Dashboard/Components/Pages/Resources.razor index a2ab249bdc3..aa86f381599 100644 --- a/src/Aspire.Dashboard/Components/Pages/Resources.razor +++ b/src/Aspire.Dashboard/Components/Pages/Resources.razor @@ -72,10 +72,10 @@ - + @ControlsStringsLoc[ControlsStrings.ViewAction] - + @ControlsStringsLoc[nameof(ControlsStrings.ViewAction)] diff --git a/src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor b/src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor index 5d69d5d1e3c..66217569b90 100644 --- a/src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor +++ b/src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor @@ -90,7 +90,7 @@ } - + @ControlsStringsLoc[nameof(ControlsStrings.ViewAction)] diff --git a/src/Aspire.Dashboard/Components/Pages/TraceDetail.razor b/src/Aspire.Dashboard/Components/Pages/TraceDetail.razor index b2d44a32eaf..672cba9f0cb 100644 --- a/src/Aspire.Dashboard/Components/Pages/TraceDetail.razor +++ b/src/Aspire.Dashboard/Components/Pages/TraceDetail.razor @@ -164,7 +164,7 @@ - + - + @ControlsStringsLoc[nameof(ControlsStrings.ViewAction)] diff --git a/src/Aspire.Dashboard/wwwroot/css/app.css b/src/Aspire.Dashboard/wwwroot/css/app.css index bfcbf4ae932..bb23049108f 100644 --- a/src/Aspire.Dashboard/wwwroot/css/app.css +++ b/src/Aspire.Dashboard/wwwroot/css/app.css @@ -356,3 +356,12 @@ fluent-switch.table-switch::part(label) { top: 50px; right: calc(var(--design-unit) * 1.5px); } + + +/* Some of the data-columns (e.g. logs and details in the resource page) + should not have ellipsis as this is unnecessary and a bit distracting. + !important is required to override the fluentui-blazor styling. +*/ +fluent-data-grid-cell.no-ellipsis { + text-overflow: unset !important; +} From 1d25695863c997ec0cc4c2b3676d9e51da2a1eb7 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Thu, 7 Mar 2024 06:21:44 -0800 Subject: [PATCH 07/50] Use EndpointReference instead of AllocatedEndpoints (#2700) * Use EndpointReference instead of AllocatedEndpoints - This unifies most of the callers way of property looking up named endpoints and fixed case sensivity issues. - Added a GetEndpoints method which returns all allocated endpoints on a resource. * Removed last public usage of TryGetAllocatedEndpoints - Fix a bug in Seq with manifest expression * Obsolete the API --- .../AzureCosmosDBResource.cs | 17 +++--- .../AzureStorageResource.cs | 48 ++++++++--------- .../Extensions/AzureStorageExtensions.cs | 7 ++- ...DaprDistributedApplicationLifecycleHook.cs | 10 ++-- .../DistributedApplicationExtensions.cs | 22 ++++---- .../ApplicationModel/EndpointReference.cs | 53 ++++++++++++++----- .../EndpointReferenceAnnotation.cs | 7 ++- .../Extensions/ResourceExtensions.cs | 16 ++++++ .../MySql/PhpMyAdminConfigWriterHook.cs | 8 +-- .../Postgres/PgAdminConfigWriterHook.cs | 4 +- .../Seq/SeqBuilderExtensions.cs | 4 +- src/Aspire.Hosting/Seq/SeqResource.cs | 18 ++++--- .../DistributedApplicationTests.cs | 5 +- ...locatedEndpointAnnotationTestExtensions.cs | 4 +- .../WithReferenceTests.cs | 8 +-- 15 files changed, 132 insertions(+), 99 deletions(-) diff --git a/src/Aspire.Hosting.Azure/AzureCosmosDBResource.cs b/src/Aspire.Hosting.Azure/AzureCosmosDBResource.cs index acbd726f821..77255452eac 100644 --- a/src/Aspire.Hosting.Azure/AzureCosmosDBResource.cs +++ b/src/Aspire.Hosting.Azure/AzureCosmosDBResource.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.Net.Sockets; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Azure; using Aspire.Hosting.Azure.Cosmos; @@ -13,10 +12,13 @@ namespace Aspire.Hosting; /// public class AzureCosmosDBResource(string name) : AzureBicepResource(name, templateResourceName: "Aspire.Hosting.Azure.Bicep.cosmosdb.bicep"), - IResourceWithConnectionString + IResourceWithConnectionString, + IResourceWithEndpoints { internal List Databases { get; } = []; + internal EndpointReference EmulatorEndpoint => new(this, "emulator"); + /// /// Gets the "connectionString" reference from the secret outputs of the Azure Cosmos DB resource. /// @@ -55,18 +57,11 @@ public class AzureCosmosDBResource(string name) : { if (IsEmulator) { - return AzureCosmosDBEmulatorConnectionString.Create(GetEmulatorPort("emulator")); + return AzureCosmosDBEmulatorConnectionString.Create(EmulatorEndpoint.Port); } return ConnectionString.Value; } - - private int GetEmulatorPort(string endpointName) => - Annotations - .OfType() - .FirstOrDefault(x => x.Name == endpointName) - ?.Port - ?? throw new DistributedApplicationException($"Azure Cosmos DB resource does not have endpoint annotation with name '{endpointName}'."); } /// @@ -103,7 +98,7 @@ public static IResourceBuilder AddAzureCosmosDB(this IDis /// public static IResourceBuilder RunAsEmulator(this IResourceBuilder builder, Action>? configureContainer = null) { - builder.WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, name: "emulator", containerPort: 8081)) + builder.WithEndpoint(name: "emulator", containerPort: 8081) .WithAnnotation(new ContainerImageAnnotation { Image = "mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator", Tag = "latest" }); if (configureContainer != null) diff --git a/src/Aspire.Hosting.Azure/AzureStorageResource.cs b/src/Aspire.Hosting.Azure/AzureStorageResource.cs index 8433374eec7..ca7ed18e13a 100644 --- a/src/Aspire.Hosting.Azure/AzureStorageResource.cs +++ b/src/Aspire.Hosting.Azure/AzureStorageResource.cs @@ -12,8 +12,14 @@ namespace Aspire.Hosting.Azure; /// /// /// -public class AzureStorageConstructResource(string name, Action configureConstruct) : AzureConstructResource(name, configureConstruct) +public class AzureStorageConstructResource(string name, Action configureConstruct) : + AzureConstructResource(name, configureConstruct), + IResourceWithEndpoints { + private EndpointReference EmulatorBlobEndpoint => new(this, "blob"); + private EndpointReference EmulatorQueueEndpoint => new(this, "queue"); + private EndpointReference EmulatorTableEndpoint => new(this, "table"); + /// /// Gets the "blobEndpoint" output reference from the bicep template for the Azure Storage resource. /// @@ -35,35 +41,28 @@ public class AzureStorageConstructResource(string name, Action this.IsContainer(); internal string? GetTableConnectionString() => IsEmulator - ? AzureStorageEmulatorConnectionString.Create(tablePort: GetEmulatorPort("table")) + ? AzureStorageEmulatorConnectionString.Create(tablePort: EmulatorTableEndpoint.Port) : TableEndpoint.Value; internal async ValueTask GetTableConnectionStringAsync(CancellationToken cancellationToken = default) => IsEmulator - ? AzureStorageEmulatorConnectionString.Create(tablePort: GetEmulatorPort("table")) + ? AzureStorageEmulatorConnectionString.Create(tablePort: EmulatorTableEndpoint.Port) : await TableEndpoint.GetValueAsync(cancellationToken).ConfigureAwait(false); internal string? GetQueueConnectionString() => IsEmulator - ? AzureStorageEmulatorConnectionString.Create(queuePort: GetEmulatorPort("queue")) + ? AzureStorageEmulatorConnectionString.Create(queuePort: EmulatorQueueEndpoint.Port) : QueueEndpoint.Value; internal async ValueTask GetQueueConnectionStringAsync(CancellationToken cancellationToken = default) => IsEmulator - ? AzureStorageEmulatorConnectionString.Create(queuePort: GetEmulatorPort("queue")) + ? AzureStorageEmulatorConnectionString.Create(queuePort: EmulatorQueueEndpoint.Port) : await QueueEndpoint.GetValueAsync(cancellationToken).ConfigureAwait(false); internal string? GetBlobConnectionString() => IsEmulator - ? AzureStorageEmulatorConnectionString.Create(blobPort: GetEmulatorPort("blob")) + ? AzureStorageEmulatorConnectionString.Create(blobPort: EmulatorBlobEndpoint.Port) : BlobEndpoint.Value; internal async ValueTask GetBlobConnectionStringAsync(CancellationToken cancellationToken = default) => IsEmulator - ? AzureStorageEmulatorConnectionString.Create(blobPort: GetEmulatorPort("blob")) + ? AzureStorageEmulatorConnectionString.Create(blobPort: EmulatorBlobEndpoint.Port) : await BlobEndpoint.GetValueAsync(cancellationToken).ConfigureAwait(false); - - private int GetEmulatorPort(string endpointName) => - Annotations - .OfType() - .FirstOrDefault(x => x.Name == endpointName) - ?.Port - ?? throw new DistributedApplicationException($"Azure storage resource does not have endpoint annotation with name '{endpointName}'."); } /// @@ -71,8 +70,14 @@ private int GetEmulatorPort(string endpointName) => /// /// public class AzureStorageResource(string name) : - AzureBicepResource(name, templateResourceName: "Aspire.Hosting.Azure.Bicep.storage.bicep") + AzureBicepResource(name, templateResourceName: "Aspire.Hosting.Azure.Bicep.storage.bicep"), + IResourceWithEndpoints { + // Emulator container endpoints + private EndpointReference EmulatorBlobEndpoint => new(this, "blob"); + private EndpointReference EmulatorQueueEndpoint => new(this, "queue"); + private EndpointReference EmulatorTableEndpoint => new(this, "table"); + /// /// Gets the "blobEndpoint" output reference from the bicep template for the Azure Storage resource. /// @@ -94,23 +99,16 @@ public class AzureStorageResource(string name) : public bool IsEmulator => this.IsContainer(); internal string? GetTableConnectionString() => IsEmulator - ? AzureStorageEmulatorConnectionString.Create(tablePort: GetEmulatorPort("table")) + ? AzureStorageEmulatorConnectionString.Create(tablePort: EmulatorTableEndpoint.Port) : TableEndpoint.Value; internal string? GetQueueConnectionString() => IsEmulator - ? AzureStorageEmulatorConnectionString.Create(queuePort: GetEmulatorPort("queue")) + ? AzureStorageEmulatorConnectionString.Create(queuePort: EmulatorQueueEndpoint.Port) : QueueEndpoint.Value; internal string? GetBlobConnectionString() => IsEmulator - ? AzureStorageEmulatorConnectionString.Create(blobPort: GetEmulatorPort("blob")) + ? AzureStorageEmulatorConnectionString.Create(blobPort: EmulatorBlobEndpoint.Port) : BlobEndpoint.Value; - - private int GetEmulatorPort(string endpointName) => - Annotations - .OfType() - .FirstOrDefault(x => x.Name == endpointName) - ?.Port - ?? throw new DistributedApplicationException($"Azure storage resource does not have endpoint annotation with name '{endpointName}'."); } file static class AzureStorageEmulatorConnectionString diff --git a/src/Aspire.Hosting.Azure/Extensions/AzureStorageExtensions.cs b/src/Aspire.Hosting.Azure/Extensions/AzureStorageExtensions.cs index 512aacde11b..704997d4e45 100644 --- a/src/Aspire.Hosting.Azure/Extensions/AzureStorageExtensions.cs +++ b/src/Aspire.Hosting.Azure/Extensions/AzureStorageExtensions.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.Net.Sockets; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Azure; using Azure.Provisioning.Authorization; @@ -87,9 +86,9 @@ public static IResourceBuilder AddAzureConstructS /// A reference to the . public static IResourceBuilder RunAsEmulator(this IResourceBuilder builder, Action>? configureContainer = null) { - builder.WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, name: "blob", containerPort: 10000)) - .WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, name: "queue", containerPort: 10001)) - .WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, name: "table", containerPort: 10002)) + builder.WithEndpoint(name: "blob", containerPort: 10000) + .WithEndpoint(name: "queue", containerPort: 10001) + .WithEndpoint(name: "table", containerPort: 10002) .WithAnnotation(new ContainerImageAnnotation { Image = "mcr.microsoft.com/azure-storage/azurite", Tag = "3.29.0" }); if (configureContainer != null) diff --git a/src/Aspire.Hosting.Dapr/DaprDistributedApplicationLifecycleHook.cs b/src/Aspire.Hosting.Dapr/DaprDistributedApplicationLifecycleHook.cs index efda08058b0..425b2a41673 100644 --- a/src/Aspire.Hosting.Dapr/DaprDistributedApplicationLifecycleHook.cs +++ b/src/Aspire.Hosting.Dapr/DaprDistributedApplicationLifecycleHook.cs @@ -183,12 +183,12 @@ public async Task BeforeStartAsync(DistributedApplicationModel appModel, Cancell new CommandLineArgsCallbackAnnotation( updatedArgs => { - AllocatedEndpointAnnotation? httpEndPoint = null; - if (resource.TryGetAllocatedEndPoints(out var projectEndPoints)) + EndpointReference? httpEndPoint = null; + if (resource is IResourceWithEndpoints resourceWithEndpoints) { - httpEndPoint = projectEndPoints.FirstOrDefault(endPoint => endPoint.Name == "http"); + httpEndPoint = resourceWithEndpoints.GetEndpoint("http"); - if (httpEndPoint is not null && sidecarOptions?.AppPort is null) + if (httpEndPoint.IsAllocated && sidecarOptions?.AppPort is null) { updatedArgs.AddRange(daprAppPortArg(httpEndPoint.Port)()); } @@ -203,7 +203,7 @@ public async Task BeforeStartAsync(DistributedApplicationModel appModel, Cancell } if (sidecarOptions?.AppChannelAddress is null && httpEndPoint is not null) { - updatedArgs.AddRange(daprAppChannelAddressArg(httpEndPoint.Address)()); + updatedArgs.AddRange(daprAppChannelAddressArg(httpEndPoint.Host)()); } })); diff --git a/src/Aspire.Hosting.Testing/DistributedApplicationExtensions.cs b/src/Aspire.Hosting.Testing/DistributedApplicationExtensions.cs index f9d7396a76e..53437a8c08f 100644 --- a/src/Aspire.Hosting.Testing/DistributedApplicationExtensions.cs +++ b/src/Aspire.Hosting.Testing/DistributedApplicationExtensions.cs @@ -74,19 +74,19 @@ private static IResource GetResource(DistributedApplication app, string resource private static string GetEndpointUriStringCore(DistributedApplication app, string resourceName, string? endpointName = default) { var resource = GetResource(app, resourceName); - if (!resource.TryGetAllocatedEndPoints(out var endpoints)) + if (resource is not IResourceWithEndpoints resourceWithEndpoints) { throw new InvalidOperationException($"Resource '{resourceName}' has no allocated endpoints."); } - AllocatedEndpointAnnotation? endpoint; + EndpointReference? endpoint; if (!string.IsNullOrEmpty(endpointName)) { - endpoint = GetEndpointOrDefault(endpoints, resourceName, endpointName); + endpoint = GetEndpointOrDefault(resourceWithEndpoints, endpointName); } else { - endpoint = GetEndpointOrDefault(endpoints, resourceName, "http") ?? GetEndpointOrDefault(endpoints, resourceName, "https"); + endpoint = GetEndpointOrDefault(resourceWithEndpoints, "http") ?? GetEndpointOrDefault(resourceWithEndpoints, "https"); } if (endpoint is null) @@ -94,17 +94,13 @@ private static string GetEndpointUriStringCore(DistributedApplication app, strin throw new ArgumentException($"Endpoint '{endpointName}' for resource '{resourceName}' not found.", nameof(endpointName)); } - return endpoint.UriString; + return endpoint.Url; } - static AllocatedEndpointAnnotation? GetEndpointOrDefault(IEnumerable endpoints, string resourceName, string? endpointName) + static EndpointReference? GetEndpointOrDefault(IResourceWithEndpoints resourceWithEndpoints, string endpointName) { - var filteredEndpoints = endpoints.Where(e => string.Equals(e.Name, endpointName, StringComparison.OrdinalIgnoreCase)).ToList(); - return filteredEndpoints.Count switch - { - 0 => null, - 1 => filteredEndpoints[0], - _ => throw new InvalidOperationException($"Resource '{resourceName}' has multiple endpoints named '{endpointName}'."), - }; + var reference = resourceWithEndpoints.GetEndpoint(endpointName); + + return reference.IsAllocated ? reference : null; } } diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs index 04dcec00be3..fa4b30ee29e 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs @@ -6,24 +6,26 @@ namespace Aspire.Hosting.ApplicationModel; /// /// Represents an endpoint reference for a resource with endpoints. /// -/// The resource with endpoints that owns the endpoint reference. -/// The name of the endpoint. -public sealed class EndpointReference(IResourceWithEndpoints owner, string endpointName) : IManifestExpressionProvider, IValueProvider +public sealed class EndpointReference : IManifestExpressionProvider, IValueProvider { + // A reference to the allocated endpoint annotation if it exists. + private AllocatedEndpointAnnotation? _allocatedEndpointAnnotation; + private bool? _isAllocated; + /// /// Gets the owner of the endpoint reference. /// - public IResourceWithEndpoints Owner { get; } = owner; + public IResourceWithEndpoints Owner { get; } /// /// Gets the name of the endpoint associated with the endpoint reference. /// - public string EndpointName { get; } = endpointName; + public string EndpointName { get; } /// /// Gets a value indicating whether the endpoint is allocated. /// - public bool IsAllocated => Owner.Annotations.OfType().Any(a => a.Name == EndpointName); + public bool IsAllocated => _isAllocated ??= _allocatedEndpointAnnotation is not null || GetAllocatedEndpoint() is not null; string IManifestExpressionProvider.ValueExpression => GetExpression(); @@ -49,29 +51,52 @@ public string GetExpression(EndpointProperty property = EndpointProperty.Url) /// /// Gets the port for this endpoint. /// - public int Port => GetAllocatedEndpoint().Port; + public int Port => AllocatedEndpointAnnotation.Port; /// /// Gets the host for this endpoint. /// - public string Host => GetAllocatedEndpoint().Address ?? "localhost"; + public string Host => AllocatedEndpointAnnotation.Address ?? "localhost"; /// /// Gets the scheme for this endpoint. /// - public string Scheme => GetAllocatedEndpoint().UriScheme; + public string Scheme => AllocatedEndpointAnnotation.UriScheme; /// /// Gets the URL for this endpoint. /// - public string Url => GetAllocatedEndpoint().UriString; + public string Url => AllocatedEndpointAnnotation.UriString; + + private AllocatedEndpointAnnotation AllocatedEndpointAnnotation => + _allocatedEndpointAnnotation ??= GetAllocatedEndpoint() + ?? throw new InvalidOperationException($"The endpoint `{EndpointName}` is not allocated for the resource `{Owner.Name}`."); + + private AllocatedEndpointAnnotation? GetAllocatedEndpoint() => + Owner.Annotations.OfType() + .SingleOrDefault(a => StringComparers.EndpointAnnotationName.Equals(a.Name, EndpointName)); - private AllocatedEndpointAnnotation GetAllocatedEndpoint() + /// + /// Creates a new instance of with the specified endpoint name. + /// + /// The resource with endpoints that owns the endpoint reference. + /// The name of the endpoint. + public EndpointReference(IResourceWithEndpoints owner, string endpointName) { - var allocatedEndpoint = Owner.Annotations.OfType().SingleOrDefault(a => a.Name == EndpointName) ?? - throw new InvalidOperationException($"The endpoint '{EndpointName}' is not allocated for the resource '{Owner.Name}'."); + Owner = owner; + EndpointName = endpointName; + } - return allocatedEndpoint; + /// + /// Creates a new instance of with the specified allocated endpoint annotation. + /// + /// The resource with endpoints that owns the endpoint reference. + /// The allocated endpoint annotation. + public EndpointReference(IResourceWithEndpoints owner, AllocatedEndpointAnnotation allocatedEndpointAnnotation) + { + Owner = owner; + EndpointName = allocatedEndpointAnnotation.Name; + _allocatedEndpointAnnotation = allocatedEndpointAnnotation; } } diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointReferenceAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/EndpointReferenceAnnotation.cs index 0e0f4cefe14..36ff76f39d0 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointReferenceAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointReferenceAnnotation.cs @@ -1,15 +1,14 @@ // 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.ObjectModel; using System.Diagnostics; namespace Aspire.Hosting.ApplicationModel; [DebuggerDisplay(@"Type = {GetType().Name,nq}, Resource = {Resource.Name}, EndpointNames = {string.Join("", "", EndpointNames)}")] -internal sealed class EndpointReferenceAnnotation(IResource resource) : IResourceAnnotation +internal sealed class EndpointReferenceAnnotation(IResourceWithEndpoints resource) : IResourceAnnotation { - public IResource Resource { get; } = resource; + public IResourceWithEndpoints Resource { get; } = resource; public bool UseAllEndpoints { get; set; } - public Collection EndpointNames { get; } = new(); + public HashSet EndpointNames { get; } = new(StringComparers.EndpointAnnotationName); } diff --git a/src/Aspire.Hosting/Extensions/ResourceExtensions.cs b/src/Aspire.Hosting/Extensions/ResourceExtensions.cs index 65ffaf86ab4..3935e6a357c 100644 --- a/src/Aspire.Hosting/Extensions/ResourceExtensions.cs +++ b/src/Aspire.Hosting/Extensions/ResourceExtensions.cs @@ -93,11 +93,27 @@ public static bool TryGetEndpoints(this IResource resource, [NotNullWhen(true)] /// The resource to get the allocated endpoints for. /// When this method returns, contains the allocated endpoints for the specified resource, if they exist; otherwise, null. /// true if the allocated endpoints were successfully retrieved; otherwise, false. + [Obsolete("Use GetEndpoints instead.")] public static bool TryGetAllocatedEndPoints(this IResource resource, [NotNullWhen(true)] out IEnumerable? allocatedEndPoints) { return TryGetAnnotationsOfType(resource, out allocatedEndPoints); } + /// + /// Gets the endpoints for the specified resource. + /// + /// + /// + public static IEnumerable GetEndpoints(this IResourceWithEndpoints resource) + { + if (TryGetAnnotationsOfType(resource, out var endpoints)) + { + return endpoints.Select(e => new EndpointReference(resource, e)); + } + + return []; + } + /// /// Attempts to get the container image name from the given resource. /// diff --git a/src/Aspire.Hosting/MySql/PhpMyAdminConfigWriterHook.cs b/src/Aspire.Hosting/MySql/PhpMyAdminConfigWriterHook.cs index 127dc1febad..e81f77bd150 100644 --- a/src/Aspire.Hosting/MySql/PhpMyAdminConfigWriterHook.cs +++ b/src/Aspire.Hosting/MySql/PhpMyAdminConfigWriterHook.cs @@ -29,9 +29,9 @@ public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, C if (mySqlInstances.Count() == 1) { var singleInstance = mySqlInstances.Single(); - if (singleInstance.TryGetAllocatedEndPoints(out var allocatedEndPoints)) + if (singleInstance.PrimaryEndpoint.IsAllocated) { - var endpoint = allocatedEndPoints.Where(ae => ae.Name == "tcp").Single(); + var endpoint = singleInstance.PrimaryEndpoint; myAdminResource.Annotations.Add(new EnvironmentCallbackAnnotation((EnvironmentCallbackContext context) => { context.EnvironmentVariables.Add("PMA_HOST", $"host.docker.internal:{endpoint.Port}"); @@ -51,9 +51,9 @@ public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, C writer.WriteLine(); foreach (var mySqlInstance in mySqlInstances) { - if (mySqlInstance.TryGetAllocatedEndPoints(out var allocatedEndpoints)) + if (mySqlInstance.PrimaryEndpoint.IsAllocated) { - var endpoint = allocatedEndpoints.Where(ae => ae.Name == "tcp").Single(); + var endpoint = mySqlInstance.PrimaryEndpoint; writer.WriteLine("$i++;"); writer.WriteLine($"$cfg['Servers'][$i]['host'] = 'host.docker.internal:{endpoint.Port}';"); writer.WriteLine($"$cfg['Servers'][$i]['verbose'] = '{mySqlInstance.Name}';"); diff --git a/src/Aspire.Hosting/Postgres/PgAdminConfigWriterHook.cs b/src/Aspire.Hosting/Postgres/PgAdminConfigWriterHook.cs index fdf689a7211..86fbc3a2b72 100644 --- a/src/Aspire.Hosting/Postgres/PgAdminConfigWriterHook.cs +++ b/src/Aspire.Hosting/Postgres/PgAdminConfigWriterHook.cs @@ -28,9 +28,9 @@ public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, C foreach (var postgresInstance in postgresInstances) { - if (postgresInstance.TryGetAllocatedEndPoints(out var allocatedEndpoints)) + if (postgresInstance.PrimaryEndpoint.IsAllocated) { - var endpoint = allocatedEndpoints.Where(ae => ae.Name == "tcp").Single(); + var endpoint = postgresInstance.PrimaryEndpoint; writer.WriteStartObject($"{serverIndex}"); writer.WriteString("Name", postgresInstance.Name); diff --git a/src/Aspire.Hosting/Seq/SeqBuilderExtensions.cs b/src/Aspire.Hosting/Seq/SeqBuilderExtensions.cs index dd25665e846..417d3fdc269 100644 --- a/src/Aspire.Hosting/Seq/SeqBuilderExtensions.cs +++ b/src/Aspire.Hosting/Seq/SeqBuilderExtensions.cs @@ -28,8 +28,8 @@ public static IResourceBuilder AddSeq( { var seqResource = new SeqResource(name); var resourceBuilder = builder.AddResource(seqResource) - .WithHttpEndpoint(hostPort: port, containerPort: 80) - .WithAnnotation(new ContainerImageAnnotation {Image = "datalust/seq"}) + .WithHttpEndpoint(hostPort: port, containerPort: 80, name: SeqResource.PrimaryEndpointName) + .WithAnnotation(new ContainerImageAnnotation { Image = "datalust/seq" }) .WithImageTag("2024.1") .WithEnvironment("ACCEPT_EULA", "Y"); diff --git a/src/Aspire.Hosting/Seq/SeqResource.cs b/src/Aspire.Hosting/Seq/SeqResource.cs index 998af03cd61..447ef9856c1 100644 --- a/src/Aspire.Hosting/Seq/SeqResource.cs +++ b/src/Aspire.Hosting/Seq/SeqResource.cs @@ -9,22 +9,26 @@ namespace Aspire.Hosting.ApplicationModel; /// The name of the Seq resource public class SeqResource(string name) : ContainerResource(name), IResourceWithConnectionString { + internal const string PrimaryEndpointName = "http"; + + private EndpointReference? _primaryEndpoint; + + /// + /// Gets the primary endpoint for the Seq server. + /// + public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName); + /// /// Gets the Uri of the Seq endpoint /// public string? GetConnectionString() { - if (!this.TryGetAnnotationsOfType(out var seqEndpointAnnotations)) - { - throw new DistributedApplicationException("Seq resource does not have endpoint annotation."); - } - - return seqEndpointAnnotations.Single().UriString; + return PrimaryEndpoint.Url; } /// /// Gets the connection string expression for the Seq server for the manifest. /// public string? ConnectionStringExpression => - $"{{{Name}.bindings.tcp.host}}:{{{Name}.bindings.tcp.port}}"; + PrimaryEndpoint.GetExpression(EndpointProperty.Url); } diff --git a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs index 548822a73fe..b34193d22b6 100644 --- a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs +++ b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs @@ -147,10 +147,9 @@ public async Task AllocatedPortsAssignedAfterHookRuns() foreach (var item in appModel.Resources) { - if ((item is ContainerResource || item is ProjectResource || item is ExecutableResource) && item.TryGetEndpoints(out _)) + if (item is IResourceWithEndpoints resourceWithEndpoints) { - Assert.True(item.TryGetAllocatedEndPoints(out var endpoints)); - Assert.NotEmpty(endpoints); + Assert.True(resourceWithEndpoints.GetEndpoints().All(e => e.IsAllocated)); } } } diff --git a/tests/Aspire.Hosting.Tests/Helpers/AllocatedEndpointAnnotationTestExtensions.cs b/tests/Aspire.Hosting.Tests/Helpers/AllocatedEndpointAnnotationTestExtensions.cs index a3cb4bf0a4d..b00c9545874 100644 --- a/tests/Aspire.Hosting.Tests/Helpers/AllocatedEndpointAnnotationTestExtensions.cs +++ b/tests/Aspire.Hosting.Tests/Helpers/AllocatedEndpointAnnotationTestExtensions.cs @@ -18,8 +18,8 @@ public static class AllocatedEndpointAnnotationTestExtensions public static async Task HttpGetStringAsync(this IResourceBuilder builder, HttpClient client, string bindingName, string path, CancellationToken cancellationToken) where T : IResourceWithEndpoints { - var allocatedEndpoint = builder.Resource.Annotations.OfType().Single(a => a.Name == bindingName); - var url = $"{allocatedEndpoint.UriString}{path}"; + var endpoint = builder.Resource.GetEndpoint(bindingName); + var url = $"{endpoint.Url}{path}"; var response = await client.GetStringAsync(url, cancellationToken); return response; diff --git a/tests/Aspire.Hosting.Tests/WithReferenceTests.cs b/tests/Aspire.Hosting.Tests/WithReferenceTests.cs index 94ef1d64559..97c9e9f7131 100644 --- a/tests/Aspire.Hosting.Tests/WithReferenceTests.cs +++ b/tests/Aspire.Hosting.Tests/WithReferenceTests.cs @@ -9,8 +9,10 @@ namespace Aspire.Hosting.Tests; public class WithReferenceTests { - [Fact] - public async Task ResourceWithSingleEndpointProducesSimplifiedEnvironmentVariables() + [Theory] + [InlineData("mybinding")] + [InlineData("MYbinding")] + public async Task ResourceWithSingleEndpointProducesSimplifiedEnvironmentVariables(string endpointName) { using var testProgram = CreateTestProgram(); @@ -25,7 +27,7 @@ public async Task ResourceWithSingleEndpointProducesSimplifiedEnvironmentVariabl )); // Get the service provider. - testProgram.ServiceBBuilder.WithReference(testProgram.ServiceABuilder.GetEndpoint("mybinding")); + testProgram.ServiceBBuilder.WithReference(testProgram.ServiceABuilder.GetEndpoint(endpointName)); testProgram.Build(); // Call environment variable callbacks. From c4e7f391f09cb0dd5bdbd2f1fd47f01bb8be3766 Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Thu, 7 Mar 2024 11:39:31 -0600 Subject: [PATCH 08/50] Revert the update to Pomelo.EntityFrameworkCore.MySql (#2712) This breaks the integration test. We need to fix the component to work with the latest version. Contributes to #2690 --- Directory.Packages.props | 2 +- tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs | 3 --- tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs | 3 +-- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index f7e478a81d5..93e12965cf8 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -99,7 +99,7 @@ - + diff --git a/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs b/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs index 24948a45bf2..46c1cefcd0d 100644 --- a/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs +++ b/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs @@ -307,9 +307,6 @@ private static ISet GetResourcesToSkip() resourcesToSkip.Add(nameof(TestResourceNames.cosmos)); } - // ActiveIssue: https://github.com/dotnet/aspire/issues/2690 - resourcesToSkip.Add(nameof(TestResourceNames.pomelo)); - if (BuildEnvironment.IsRunningOnCI) { resourcesToSkip.Add(nameof(TestResourceNames.cosmos)); diff --git a/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs b/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs index 8b63a367b8b..1de499f3baf 100644 --- a/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs +++ b/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs @@ -24,8 +24,7 @@ public IntegrationServicesTests(ITestOutputHelper testOutput, IntegrationService [Theory] [InlineData(TestResourceNames.mongodb)] [InlineData(TestResourceNames.mysql)] - // ActiveIssue: https://github.com/dotnet/aspire/issues/2690 - //[InlineData(TestResourceNames.pomelo)] + [InlineData(TestResourceNames.pomelo)] [InlineData(TestResourceNames.postgres)] [InlineData(TestResourceNames.rabbitmq)] [InlineData(TestResourceNames.redis)] From c01d3f6d11d243b96dc4b38a88951824de9e5723 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 7 Mar 2024 15:44:35 -0500 Subject: [PATCH 09/50] [tests] Add integration test for efcore postgresql (#2669) * [tests] Add integration tests for efcore postgresql * [tests] Add package version for Aspire.Npgsql.EntityFrameworkCore.PostgreSQL * sort list of references alphabetically - addresses review feedback from @ eerhardt --- .../IntegrationServicesTests.cs | 1 + tests/testproject/Common/TestResourceNames.cs | 1 + .../Directory.Packages.Helix.props | 5 ++-- .../Postgres/NpgsqlDbContext.cs | 8 ++++++ .../Postgres/NpgsqlEFCoreExtensions.cs | 25 +++++++++++++++++++ .../Program.cs | 8 ++++++ .../TestProject.IntegrationServiceA.csproj | 1 + 7 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 tests/testproject/TestProject.IntegrationServiceA/Postgres/NpgsqlDbContext.cs create mode 100644 tests/testproject/TestProject.IntegrationServiceA/Postgres/NpgsqlEFCoreExtensions.cs diff --git a/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs b/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs index 1de499f3baf..55cc794cc1f 100644 --- a/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs +++ b/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs @@ -29,6 +29,7 @@ public IntegrationServicesTests(ITestOutputHelper testOutput, IntegrationService [InlineData(TestResourceNames.rabbitmq)] [InlineData(TestResourceNames.redis)] [InlineData(TestResourceNames.sqlserver)] + [InlineData(TestResourceNames.efnpgsql)] public Task VerifyComponentWorks(TestResourceNames resourceName) => RunTestAsync(async () => { diff --git a/tests/testproject/Common/TestResourceNames.cs b/tests/testproject/Common/TestResourceNames.cs index 58ea63a1a70..031ef3d47cb 100644 --- a/tests/testproject/Common/TestResourceNames.cs +++ b/tests/testproject/Common/TestResourceNames.cs @@ -14,6 +14,7 @@ public enum TestResourceNames rabbitmq, redis, sqlserver, + efnpgsql } public static class TestResourceNamesExtensions diff --git a/tests/testproject/Directory.Packages.Helix.props b/tests/testproject/Directory.Packages.Helix.props index eecf59113f0..992eb8f246c 100644 --- a/tests/testproject/Directory.Packages.Helix.props +++ b/tests/testproject/Directory.Packages.Helix.props @@ -6,16 +6,17 @@ + + + - - diff --git a/tests/testproject/TestProject.IntegrationServiceA/Postgres/NpgsqlDbContext.cs b/tests/testproject/TestProject.IntegrationServiceA/Postgres/NpgsqlDbContext.cs new file mode 100644 index 00000000000..817069c9be1 --- /dev/null +++ b/tests/testproject/TestProject.IntegrationServiceA/Postgres/NpgsqlDbContext.cs @@ -0,0 +1,8 @@ +// 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 NpgsqlDbContext(DbContextOptions options) : DbContext(options) +{ +} diff --git a/tests/testproject/TestProject.IntegrationServiceA/Postgres/NpgsqlEFCoreExtensions.cs b/tests/testproject/TestProject.IntegrationServiceA/Postgres/NpgsqlEFCoreExtensions.cs new file mode 100644 index 00000000000..bce80a09094 --- /dev/null +++ b/tests/testproject/TestProject.IntegrationServiceA/Postgres/NpgsqlEFCoreExtensions.cs @@ -0,0 +1,25 @@ +// 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 static class NpgsqlEFCoreExtensions +{ + public static void MapNpgsqlEFCoreApi(this WebApplication app) + { + app.MapGet("/efnpgsql/verify", VerifyNpgsqlEFCoreAsync); + } + + private static IResult VerifyNpgsqlEFCoreAsync(NpgsqlDbContext dbContext) + { + try + { + var results = dbContext.Database.SqlQueryRaw("SELECT 1"); + return results.Any() ? Results.Ok("Success!") : Results.Problem("Failed"); + } + catch (Exception e) + { + return Results.Problem(e.ToString()); + } + } +} diff --git a/tests/testproject/TestProject.IntegrationServiceA/Program.cs b/tests/testproject/TestProject.IntegrationServiceA/Program.cs index b8bcc351731..805f3a6a3b2 100644 --- a/tests/testproject/TestProject.IntegrationServiceA/Program.cs +++ b/tests/testproject/TestProject.IntegrationServiceA/Program.cs @@ -29,6 +29,10 @@ { builder.AddNpgsqlDataSource("postgresdb"); } +if (!resourcesToSkip.Contains(TestResourceNames.efnpgsql)) +{ + builder.AddNpgsqlDbContext("postgresdb"); +} if (!resourcesToSkip.Contains(TestResourceNames.rabbitmq)) { builder.AddRabbitMQClient("rabbitmq"); @@ -88,6 +92,10 @@ { app.MapPostgresApi(); } +if (!resourcesToSkip.Contains(TestResourceNames.efnpgsql)) +{ + app.MapNpgsqlEFCoreApi(); +} if (!resourcesToSkip.Contains(TestResourceNames.sqlserver)) { diff --git a/tests/testproject/TestProject.IntegrationServiceA/TestProject.IntegrationServiceA.csproj b/tests/testproject/TestProject.IntegrationServiceA/TestProject.IntegrationServiceA.csproj index 56391484b4d..03e4317c75f 100644 --- a/tests/testproject/TestProject.IntegrationServiceA/TestProject.IntegrationServiceA.csproj +++ b/tests/testproject/TestProject.IntegrationServiceA/TestProject.IntegrationServiceA.csproj @@ -21,6 +21,7 @@ + From 0e6e2710a6f930be9378045dcfb3ae8413bd83cb Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Fri, 8 Mar 2024 06:54:29 +0800 Subject: [PATCH 10/50] Fix flaky min rate tests (#2703) --- .../ChannelExtensionsTests.cs | 2 +- tests/Aspire.Dashboard.Tests/CustomAssert.cs | 15 +++++++++++++++ .../TelemetryRepositoryTests/LogTests.cs | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 tests/Aspire.Dashboard.Tests/CustomAssert.cs diff --git a/tests/Aspire.Dashboard.Tests/ChannelExtensionsTests.cs b/tests/Aspire.Dashboard.Tests/ChannelExtensionsTests.cs index 0142e878bd0..8f73c37fc4d 100644 --- a/tests/Aspire.Dashboard.Tests/ChannelExtensionsTests.cs +++ b/tests/Aspire.Dashboard.Tests/ChannelExtensionsTests.cs @@ -96,7 +96,7 @@ public async Task GetBatchesAsync_MinReadInterval_WaitForNextRead() Assert.Equal(["d", "e", "f"], read2.Single()); var elapsed = stopwatch.Elapsed; - Assert.True(elapsed >= minReadInterval, $"Elapsed time {elapsed} should be greater than min read interval {minReadInterval} on read."); + CustomAssert.AssertExceedsMinInterval(elapsed, minReadInterval); channel.Writer.Complete(); await TaskHelpers.WaitIgnoreCancelAsync(readTask); diff --git a/tests/Aspire.Dashboard.Tests/CustomAssert.cs b/tests/Aspire.Dashboard.Tests/CustomAssert.cs new file mode 100644 index 00000000000..5fe7086089e --- /dev/null +++ b/tests/Aspire.Dashboard.Tests/CustomAssert.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Aspire.Dashboard.Tests; + +public static class CustomAssert +{ + public static void AssertExceedsMinInterval(TimeSpan duration, TimeSpan minInterval) + { + // Timers are not precise, so we allow for a small margin of error. + Assert.True(duration >= minInterval.Subtract(TimeSpan.FromMilliseconds(20)), $"Elapsed time {duration} should be greater than min interval {minInterval}."); + } +} diff --git a/tests/Aspire.Dashboard.Tests/TelemetryRepositoryTests/LogTests.cs b/tests/Aspire.Dashboard.Tests/TelemetryRepositoryTests/LogTests.cs index 9d6e093cf02..e72f20cad9a 100644 --- a/tests/Aspire.Dashboard.Tests/TelemetryRepositoryTests/LogTests.cs +++ b/tests/Aspire.Dashboard.Tests/TelemetryRepositoryTests/LogTests.cs @@ -706,6 +706,6 @@ public async Task Subscription_MultipleUpdates_MinExecuteIntervalApplied() Assert.Equal(2, read2); var elapsed = stopwatch.Elapsed; - Assert.True(elapsed >= minExecuteInterval, $"Elapsed time {elapsed} should be greater than min execute interval {minExecuteInterval} on read."); + CustomAssert.AssertExceedsMinInterval(elapsed, minExecuteInterval); } } From 1a87c72309a8c2a4bee2617a9cdebb0ebab5017d Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Fri, 8 Mar 2024 10:27:06 +0800 Subject: [PATCH 11/50] Remove key from overflow item to fix re-rendering (#2709) --- .../Components/Controls/Chart/ChartFilters.razor | 2 +- src/Aspire.Dashboard/Components/Pages/Traces.razor | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Aspire.Dashboard/Components/Controls/Chart/ChartFilters.razor b/src/Aspire.Dashboard/Components/Controls/Chart/ChartFilters.razor index 007c610a01b..bb0e3a04fc6 100644 --- a/src/Aspire.Dashboard/Components/Controls/Chart/ChartFilters.razor +++ b/src/Aspire.Dashboard/Components/Controls/Chart/ChartFilters.razor @@ -24,7 +24,7 @@ { foreach (var item in context.SelectedValues.OrderBy(g => g.Name)) { - + @item.Name } diff --git a/src/Aspire.Dashboard/Components/Pages/Traces.razor b/src/Aspire.Dashboard/Components/Pages/Traces.razor index 79745810ae9..617c63c2d6e 100644 --- a/src/Aspire.Dashboard/Components/Pages/Traces.razor +++ b/src/Aspire.Dashboard/Components/Pages/Traces.razor @@ -43,7 +43,7 @@ @foreach (var item in context.Spans.GroupBy(s => s.Source).OrderBy(g => g.Min(s => s.StartTime))) { - + @if (item.Any(s => s.Status == OtlpSpanStatusCode.Error)) { From 98a093368f2ebb10cba57560f1d783fdfb8cbbde Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 8 Mar 2024 14:05:45 +1100 Subject: [PATCH 12/50] Postgres to Azure via CDK (#2708) --- Directory.Packages.props | 2 +- .../CdkSample.ApiService.csproj | 1 + .../cdk/CdkSample.ApiService/Program.cs | 24 +++- playground/cdk/CdkSample.AppHost/Program.cs | 11 +- .../CdkSample.AppHost/aspire-manifest.json | 89 +++++++++++- .../aspire.hosting.azure.bicep.postgres.bicep | 68 +++++++++ .../cdk/CdkSample.AppHost/cache.module.bicep | 4 +- .../cdk/CdkSample.AppHost/mykv.module.bicep | 6 +- .../cdk/CdkSample.AppHost/pgsql.module.bicep | 72 ++++++++++ .../cdk/CdkSample.AppHost/pgsql2.module.bicep | 65 +++++++++ .../cdk/CdkSample.AppHost/sql.module.bicep | 6 +- .../CdkSample.AppHost/storage.module.bicep | 6 +- .../AzurePostgresResource.cs | 47 +++++++ .../Extensions/AzurePostgresExtensions.cs | 129 +++++++++++++++++- .../Extensions/AzureRedisExtensions.cs | 5 +- .../Extensions/AzureSqlExtensions.cs | 2 +- 16 files changed, 513 insertions(+), 24 deletions(-) create mode 100644 playground/cdk/CdkSample.AppHost/aspire.hosting.azure.bicep.postgres.bicep create mode 100644 playground/cdk/CdkSample.AppHost/pgsql.module.bicep create mode 100644 playground/cdk/CdkSample.AppHost/pgsql2.module.bicep diff --git a/Directory.Packages.props b/Directory.Packages.props index 93e12965cf8..4b9bda200f5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -25,7 +25,7 @@ - + diff --git a/playground/cdk/CdkSample.ApiService/CdkSample.ApiService.csproj b/playground/cdk/CdkSample.ApiService/CdkSample.ApiService.csproj index e4583087465..493052dc3be 100644 --- a/playground/cdk/CdkSample.ApiService/CdkSample.ApiService.csproj +++ b/playground/cdk/CdkSample.ApiService/CdkSample.ApiService.csproj @@ -10,6 +10,7 @@ + diff --git a/playground/cdk/CdkSample.ApiService/Program.cs b/playground/cdk/CdkSample.ApiService/Program.cs index 23cd8a93ac8..655fb820462 100644 --- a/playground/cdk/CdkSample.ApiService/Program.cs +++ b/playground/cdk/CdkSample.ApiService/Program.cs @@ -15,17 +15,19 @@ builder.AddSqlServerDbContext("sqldb"); builder.AddAzureKeyVaultClient("mykv"); builder.AddRedisClient("cache"); +builder.AddNpgsqlDbContext("pgsqldb"); var app = builder.Build(); -app.MapGet("/", async (BlobServiceClient bsc, SqlContext context, SecretClient sc, IConnectionMultiplexer connection) => +app.MapGet("/", async (BlobServiceClient bsc, SqlContext sqlContext, SecretClient sc, IConnectionMultiplexer connection, NpgsqlContext npgsqlContext) => { return new { redisEntries = await TestRedisAsync(connection), secretChecked = await TestSecretAsync(sc), blobFiles = await TestBlobStorageAsync(bsc), - sqlRows = await TestSqlServerAsync(context) + sqlRows = await TestSqlServerAsync(sqlContext), + npgsqlRows = await TestNpgsqlAsync(npgsqlContext), }; }); app.Run(); @@ -89,10 +91,28 @@ static async Task> TestSqlServerAsync(SqlContext context) return entries; } +static async Task> TestNpgsqlAsync(NpgsqlContext context) +{ + await context.Database.EnsureCreatedAsync(); + + var entry = new Entry(); + await context.Entries.AddAsync(entry); + await context.SaveChangesAsync(); + + var entries = await context.Entries.ToListAsync(); + return entries; +} + +public class NpgsqlContext(DbContextOptions options) : DbContext(options) +{ + public DbSet Entries { get; set; } +} + public class SqlContext(DbContextOptions options) : DbContext(options) { public DbSet Entries { get; set; } } + public class Entry { public Guid Id { get; set; } = Guid.NewGuid(); diff --git a/playground/cdk/CdkSample.AppHost/Program.cs b/playground/cdk/CdkSample.AppHost/Program.cs index 4ba95fd3251..89252acb288 100644 --- a/playground/cdk/CdkSample.AppHost/Program.cs +++ b/playground/cdk/CdkSample.AppHost/Program.cs @@ -28,11 +28,20 @@ var cache = builder.AddRedis("cache").AsAzureRedisConstruct(); +var pgsqlAdministratorLogin = builder.AddParameter("pgsqlAdministratorLogin"); +var pgsqlAdministratorLoginPassword = builder.AddParameter("pgsqlAdministratorLoginPassword", secret: true); +var pgsqldb = builder.AddPostgres("pgsql") + .AsAzurePostgresFlexibleServerConstruct(pgsqlAdministratorLogin, pgsqlAdministratorLoginPassword) + .AddDatabase("pgsqldb"); + +var pgsql2 = builder.AddPostgres("pgsql2").AsAzurePostgresFlexibleServerConstruct(); + builder.AddProject("api") .WithReference(blobs) .WithReference(sqldb) .WithReference(keyvault) - .WithReference(cache); + .WithReference(cache) + .WithReference(pgsqldb); // This project is only added in playground projects to support development/debugging // of the dashboard. It is not required in end developer code. Comment out this code diff --git a/playground/cdk/CdkSample.AppHost/aspire-manifest.json b/playground/cdk/CdkSample.AppHost/aspire-manifest.json index 53f854fd7c5..ea57b86c658 100644 --- a/playground/cdk/CdkSample.AppHost/aspire-manifest.json +++ b/playground/cdk/CdkSample.AppHost/aspire-manifest.json @@ -48,6 +48,17 @@ "params": { "principalId": "", "principalName": "" + }, + "inputs": { + "password": { + "type": "string", + "secret": true, + "default": { + "generate": { + "minLength": 10 + } + } + } } }, "sqldb": { @@ -73,6 +84,81 @@ "keyVaultName": "" } }, + "pgsqlAdministratorLogin": { + "type": "parameter.v0", + "value": "{pgsqlAdministratorLogin.inputs.value}", + "inputs": { + "value": { + "type": "string" + } + } + }, + "pgsqlAdministratorLoginPassword": { + "type": "parameter.v0", + "value": "{pgsqlAdministratorLoginPassword.inputs.value}", + "inputs": { + "value": { + "type": "string", + "secret": true + } + } + }, + "pgsql": { + "type": "azure.bicep.v0", + "connectionString": "{pgsql.secretOutputs.connectionString}", + "path": "pgsql.module.bicep", + "params": { + "principalId": "", + "keyVaultName": "", + "administratorLogin": "{pgsqlAdministratorLogin.value}", + "administratorLoginPassword": "{pgsqlAdministratorLoginPassword.value}" + }, + "inputs": { + "password": { + "type": "string", + "secret": true, + "default": { + "generate": { + "minLength": 10 + } + } + } + } + }, + "pgsqldb": { + "type": "value.v0", + "connectionString": "{pgsql.connectionString};Database=pgsqldb" + }, + "pgsql2": { + "type": "azure.bicep.v0", + "connectionString": "{pgsql2.secretOutputs.connectionString}", + "path": "pgsql2.module.bicep", + "params": { + "principalId": "", + "keyVaultName": "", + "administratorLogin": "{pgsql2.inputs.username}", + "administratorLoginPassword": "{pgsql2.inputs.password}" + }, + "inputs": { + "password": { + "type": "string", + "secret": true, + "default": { + "generate": { + "minLength": 10 + } + } + }, + "username": { + "type": "string", + "default": { + "generate": { + "minLength": 10 + } + } + } + } + }, "api": { "type": "project.v0", "path": "../CdkSample.ApiService/CdkSample.ApiService.csproj", @@ -82,7 +168,8 @@ "ConnectionStrings__blobs": "{blobs.connectionString}", "ConnectionStrings__sqldb": "{sqldb.connectionString}", "ConnectionStrings__mykv": "{mykv.connectionString}", - "ConnectionStrings__cache": "{cache.connectionString}" + "ConnectionStrings__cache": "{cache.connectionString}", + "ConnectionStrings__pgsqldb": "{pgsqldb.connectionString}" }, "bindings": { "http": { diff --git a/playground/cdk/CdkSample.AppHost/aspire.hosting.azure.bicep.postgres.bicep b/playground/cdk/CdkSample.AppHost/aspire.hosting.azure.bicep.postgres.bicep new file mode 100644 index 00000000000..77c57ac31cd --- /dev/null +++ b/playground/cdk/CdkSample.AppHost/aspire.hosting.azure.bicep.postgres.bicep @@ -0,0 +1,68 @@ +param administratorLogin string +param keyVaultName string + +@secure() +param administratorLoginPassword string +param location string = resourceGroup().location +param serverName string +param serverEdition string = 'Burstable' +param skuSizeGB int = 32 +param dbInstanceType string = 'Standard_B1ms' +param haMode string = 'Disabled' +param availabilityZone string = '1' +param version string = '16' +param databases array = [] + +var resourceToken = uniqueString(resourceGroup().id) + +resource pgserver 'Microsoft.DBforPostgreSQL/flexibleServers@2021-06-01' = { + name: '${serverName}-${resourceToken}' + location: location + sku: { + name: dbInstanceType + tier: serverEdition + } + properties: { + version: version + administratorLogin: administratorLogin + administratorLoginPassword: administratorLoginPassword + network: { + delegatedSubnetResourceId: null + privateDnsZoneArmResourceId: null + } + highAvailability: { + mode: haMode + } + storage: { + storageSizeGB: skuSizeGB + } + backup: { + backupRetentionDays: 7 + geoRedundantBackup: 'Disabled' + } + availabilityZone: availabilityZone + } + + resource firewallRules 'firewallRules@2021-06-01' = { + name: 'fw-pg-localdev' + properties: { + startIpAddress: '0.0.0.0' + endIpAddress: '255.255.255.255' + } + } + + resource database 'databases@2021-06-01' = [for name in databases: { + name: name + }] +} + +resource vault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { + name: keyVaultName + + resource secret 'secrets@2023-07-01' = { + name: 'connectionString' + properties: { + value: 'Host=${pgserver.properties.fullyQualifiedDomainName};Username=${administratorLogin};Password=${administratorLoginPassword}' + } + } +} diff --git a/playground/cdk/CdkSample.AppHost/cache.module.bicep b/playground/cdk/CdkSample.AppHost/cache.module.bicep index 32e5fe0636b..bbf78fdd251 100644 --- a/playground/cdk/CdkSample.AppHost/cache.module.bicep +++ b/playground/cdk/CdkSample.AppHost/cache.module.bicep @@ -1,13 +1,13 @@ targetScope = 'resourceGroup' @description('') -param principalId string +param location string = resourceGroup().location @description('') param keyVaultName string @description('') -param location string = resourceGroup().location +param principalId string resource keyVault_IeF8jZvXV 'Microsoft.KeyVault/vaults@2023-02-01' existing = { diff --git a/playground/cdk/CdkSample.AppHost/mykv.module.bicep b/playground/cdk/CdkSample.AppHost/mykv.module.bicep index aff9e9c3daa..ae9a8310bfd 100644 --- a/playground/cdk/CdkSample.AppHost/mykv.module.bicep +++ b/playground/cdk/CdkSample.AppHost/mykv.module.bicep @@ -1,13 +1,13 @@ targetScope = 'resourceGroup' @description('') -param principalId string +param location string = resourceGroup().location @description('') -param principalType string +param principalId string @description('') -param location string = resourceGroup().location +param principalType string @description('') param signaturesecret string diff --git a/playground/cdk/CdkSample.AppHost/pgsql.module.bicep b/playground/cdk/CdkSample.AppHost/pgsql.module.bicep new file mode 100644 index 00000000000..12a85ae3a74 --- /dev/null +++ b/playground/cdk/CdkSample.AppHost/pgsql.module.bicep @@ -0,0 +1,72 @@ +targetScope = 'resourceGroup' + +@description('') +param location string = resourceGroup().location + +@description('') +param administratorLogin string + +@secure() +@description('') +param administratorLoginPassword string + +@description('') +param principalId string + +@description('') +param keyVaultName string + + +resource keyVault_IeF8jZvXV 'Microsoft.KeyVault/vaults@2023-02-01' existing = { + name: keyVaultName +} + +resource postgreSqlFlexibleServer_UTKFzAL0U 'Microsoft.DBforPostgreSQL/flexibleServers@2022-12-01' = { + name: toLower(take(concat('pgsql', uniqueString(resourceGroup().id)), 24)) + location: location + sku: { + name: 'Standard_B1ms' + tier: 'Burstable' + } + properties: { + administratorLogin: administratorLogin + administratorLoginPassword: administratorLoginPassword + version: '16' + storage: { + storageSizeGB: 32 + } + backup: { + backupRetentionDays: 7 + geoRedundantBackup: 'Disabled' + } + highAvailability: { + mode: 'Disabled' + } + availabilityZone: '1' + } +} + +resource postgreSqlFirewallRule_TT2MuwakC 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules@2023-03-01-preview' = { + parent: postgreSqlFlexibleServer_UTKFzAL0U + name: 'AllowAllAzureIps' + properties: { + startIpAddress: '0.0.0.0' + endIpAddress: '0.0.0.0' + } +} + +resource postgreSqlFlexibleServerDatabase_MVhrhEeMJ 'Microsoft.DBforPostgreSQL/flexibleServers/databases@2022-12-01' = { + parent: postgreSqlFlexibleServer_UTKFzAL0U + name: 'pgsqldb' + properties: { + } +} + +resource keyVaultSecret_Ddsc3HjrA 'Microsoft.KeyVault/vaults/secrets@2023-02-01' = { + parent: keyVault_IeF8jZvXV + name: 'connectionString' + location: location + properties: { + value: 'Host=${postgreSqlFlexibleServer_UTKFzAL0U.properties.fullyQualifiedDomainName};Username=${administratorLogin};Password=${administratorLoginPassword}' + } +} diff --git a/playground/cdk/CdkSample.AppHost/pgsql2.module.bicep b/playground/cdk/CdkSample.AppHost/pgsql2.module.bicep new file mode 100644 index 00000000000..d1cc554dffa --- /dev/null +++ b/playground/cdk/CdkSample.AppHost/pgsql2.module.bicep @@ -0,0 +1,65 @@ +targetScope = 'resourceGroup' + +@description('') +param location string = resourceGroup().location + +@description('') +param administratorLogin string + +@secure() +@description('') +param administratorLoginPassword string + +@description('') +param principalId string + +@description('') +param keyVaultName string + + +resource keyVault_IeF8jZvXV 'Microsoft.KeyVault/vaults@2023-02-01' existing = { + name: keyVaultName +} + +resource postgreSqlFlexibleServer_L4yCjMLWz 'Microsoft.DBforPostgreSQL/flexibleServers@2022-12-01' = { + name: toLower(take(concat('pgsql2', uniqueString(resourceGroup().id)), 24)) + location: location + sku: { + name: 'Standard_B1ms' + tier: 'Burstable' + } + properties: { + administratorLogin: administratorLogin + administratorLoginPassword: administratorLoginPassword + version: '16' + storage: { + storageSizeGB: 32 + } + backup: { + backupRetentionDays: 7 + geoRedundantBackup: 'Disabled' + } + highAvailability: { + mode: 'Disabled' + } + availabilityZone: '1' + } +} + +resource postgreSqlFirewallRule_b2WDQTOKx 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules@2023-03-01-preview' = { + parent: postgreSqlFlexibleServer_L4yCjMLWz + name: 'AllowAllAzureIps' + properties: { + startIpAddress: '0.0.0.0' + endIpAddress: '0.0.0.0' + } +} + +resource keyVaultSecret_Ddsc3HjrA 'Microsoft.KeyVault/vaults/secrets@2023-02-01' = { + parent: keyVault_IeF8jZvXV + name: 'connectionString' + location: location + properties: { + value: 'Host=${postgreSqlFlexibleServer_L4yCjMLWz.properties.fullyQualifiedDomainName};Username=${administratorLogin};Password=${administratorLoginPassword}' + } +} diff --git a/playground/cdk/CdkSample.AppHost/sql.module.bicep b/playground/cdk/CdkSample.AppHost/sql.module.bicep index 3091ae52f50..401a32590e0 100644 --- a/playground/cdk/CdkSample.AppHost/sql.module.bicep +++ b/playground/cdk/CdkSample.AppHost/sql.module.bicep @@ -1,13 +1,13 @@ targetScope = 'resourceGroup' @description('') -param principalId string +param location string = resourceGroup().location @description('') -param principalName string +param principalId string @description('') -param location string = resourceGroup().location +param principalName string resource sqlServer_l5O9GRsSn 'Microsoft.Sql/servers@2022-08-01-preview' = { diff --git a/playground/cdk/CdkSample.AppHost/storage.module.bicep b/playground/cdk/CdkSample.AppHost/storage.module.bicep index 877a07f7036..44f00a68cdb 100644 --- a/playground/cdk/CdkSample.AppHost/storage.module.bicep +++ b/playground/cdk/CdkSample.AppHost/storage.module.bicep @@ -1,13 +1,13 @@ targetScope = 'resourceGroup' @description('') -param principalId string +param location string = resourceGroup().location @description('') -param principalType string +param principalId string @description('') -param location string = resourceGroup().location +param principalType string @description('') param storagesku string diff --git a/src/Aspire.Hosting.Azure/AzurePostgresResource.cs b/src/Aspire.Hosting.Azure/AzurePostgresResource.cs index 987158fafab..c39ff98f5f1 100644 --- a/src/Aspire.Hosting.Azure/AzurePostgresResource.cs +++ b/src/Aspire.Hosting.Azure/AzurePostgresResource.cs @@ -50,3 +50,50 @@ public class AzurePostgresResource(PostgresServerResource innerResource) : /// public override ResourceAnnotationCollection Annotations => innerResource.Annotations; } + +/// +/// Represents an resource for Azure Postgres Flexible Server. +/// +/// that this resource wraps. +/// Callback to configure construct. +public class AzurePostgresConstructResource(PostgresServerResource innerResource, Action configureConstruct) : + AzureConstructResource(innerResource.Name, configureConstruct), + IResourceWithConnectionString +{ + /// + /// Gets the "connectionString" secret output reference from the bicep template for the Azure Postgres Flexible Server. + /// + public BicepSecretOutputReference ConnectionString => new("connectionString", this); + + /// + /// Gets the connection template for the manifest for the Azure Postgres Flexible Server. + /// + public string ConnectionStringExpression => ConnectionString.ValueExpression; + + /// + /// Gets the connection string for the Azure Postgres Flexible Server. + /// + /// The connection string. + public string? GetConnectionString() => ConnectionString.Value; + + /// + /// Gets the connection string for the Azure Postgres Flexible Server. + /// + /// A to observe while waiting for the task to complete. + /// The connection string. + public async ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) + { + if (ProvisioningTaskCompletionSource is not null) + { + await ProvisioningTaskCompletionSource.Task.WaitAsync(cancellationToken).ConfigureAwait(false); + } + + return GetConnectionString(); + } + + /// + public override string Name => innerResource.Name; + + /// + public override ResourceAnnotationCollection Annotations => innerResource.Annotations; +} diff --git a/src/Aspire.Hosting.Azure/Extensions/AzurePostgresExtensions.cs b/src/Aspire.Hosting.Azure/Extensions/AzurePostgresExtensions.cs index 305624100bd..b3f7083977f 100644 --- a/src/Aspire.Hosting.Azure/Extensions/AzurePostgresExtensions.cs +++ b/src/Aspire.Hosting.Azure/Extensions/AzurePostgresExtensions.cs @@ -3,6 +3,9 @@ using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Azure; +using Azure.Provisioning; +using Azure.Provisioning.KeyVaults; +using Azure.Provisioning.PostgreSql; namespace Aspire.Hosting; @@ -83,10 +86,10 @@ private static IResourceBuilder ConfigureDefaults(this IR .WithParameter(AzureBicepResource.KnownParameters.KeyVaultName); } - private static IResourceBuilder WithLoginAndPassword( - this IResourceBuilder builder, + private static IResourceBuilder WithLoginAndPassword( + this IResourceBuilder builder, IResourceBuilder? administratorLogin, - IResourceBuilder? administratorLoginPassword) + IResourceBuilder? administratorLoginPassword) where T: AzureBicepResource { if (administratorLogin is null) { @@ -116,4 +119,124 @@ private static IResourceBuilder WithLoginAndPassword( return builder; } + + internal static IResourceBuilder PublishAsAzurePostgresFlexibleServerConstruct( + this IResourceBuilder builder, + IResourceBuilder? administratorLogin = null, + IResourceBuilder? administratorLoginPassword = null, + Action, ResourceModuleConstruct, PostgreSqlFlexibleServer>? configureResource = null, + bool useProvisioner = false) + { + var configureConstruct = (ResourceModuleConstruct construct) => + { + var administratorLogin = new Parameter("administratorLogin"); + var administratorLoginPassword = new Parameter("administratorLoginPassword", isSecure: true); + + var postgres = new PostgreSqlFlexibleServer(construct, administratorLogin, administratorLoginPassword, name: construct.Resource.Name); + postgres.AssignProperty(x => x.Sku.Name, "'Standard_B1ms'"); + postgres.AssignProperty(x => x.Sku.Tier, "'Burstable'"); + postgres.AssignProperty(x => x.Version, "'16'"); + postgres.AssignProperty(x => x.HighAvailability.Mode, "'Disabled'"); + postgres.AssignProperty(x => x.Storage.StorageSizeInGB, "32"); + postgres.AssignProperty(x => x.Backup.BackupRetentionDays, "7"); + postgres.AssignProperty(x => x.Backup.GeoRedundantBackup, "'Disabled'"); + postgres.AssignProperty(x => x.AvailabilityZone, "'1'"); + + // Opens access to all Azure services. + var azureServicesFirewallRule = new PostgreSqlFirewallRule(construct, "0.0.0.0", "0.0.0.0", postgres, "AllowAllAzureIps"); + + if (builder.ApplicationBuilder.ExecutionContext.IsRunMode) + { + // Opens access to the Internet. + var openFirewallRule = new PostgreSqlFirewallRule(construct, "0.0.0.0", "255.255.255.255", postgres, "AllowAllIps"); + } + + List sqlDatabases = new List(); + foreach (var databaseNames in builder.Resource.Databases) + { + var databaseName = databaseNames.Value; + var pgsqlDatabase = new PostgreSqlFlexibleServerDatabase(construct, postgres, databaseName); + sqlDatabases.Add(pgsqlDatabase); + } + + var keyVault = KeyVault.FromExisting(construct, "keyVaultName"); + _ = new KeyVaultSecret(construct, "connectionString", postgres.GetConnectionString(administratorLogin, administratorLoginPassword)); + + if (configureResource != null) + { + var azureResource = (AzurePostgresConstructResource)construct.Resource; + var azureResourceBuilder = builder.ApplicationBuilder.CreateResourceBuilder(azureResource); + configureResource(azureResourceBuilder, construct, postgres); + } + }; + + var resource = new AzurePostgresConstructResource(builder.Resource, configureConstruct); + var resourceBuilder = builder.ApplicationBuilder.CreateResourceBuilder(resource) + .WithParameter(AzureBicepResource.KnownParameters.PrincipalId) + .WithParameter(AzureBicepResource.KnownParameters.KeyVaultName) + .WithManifestPublishingCallback(resource.WriteToManifest) + .WithLoginAndPassword(administratorLogin, administratorLoginPassword); + + if (builder.ApplicationBuilder.ExecutionContext.IsRunMode) + { + resourceBuilder.WithParameter(AzureBicepResource.KnownParameters.PrincipalType); + } + + if (useProvisioner) + { + // Used to hold a reference to the azure surrogate for use with the provisioner. + builder.WithAnnotation(new AzureBicepResourceAnnotation(resource)); + builder.WithConnectionStringRedirection(resource); + + // Remove the container annotation so that DCP doesn't do anything with it. + if (builder.Resource.Annotations.OfType().SingleOrDefault() is { } containerAnnotation) + { + builder.Resource.Annotations.Remove(containerAnnotation); + } + } + + return builder; + } + + /// + /// Configures Postgres Server resource to be deployed as Azure Postgres Flexible Server. + /// + /// The builder. + /// + /// + /// Callback to configure Azure resource. + /// A reference to the builder. + public static IResourceBuilder PublishAsAzurePostgresFlexibleServerConstruct( + this IResourceBuilder builder, + IResourceBuilder? administratorLogin = null, + IResourceBuilder? administratorLoginPassword = null, + Action, ResourceModuleConstruct, PostgreSqlFlexibleServer>? configureResource = null) + { + return builder.PublishAsAzurePostgresFlexibleServerConstruct( + administratorLogin, + administratorLoginPassword, + configureResource, + useProvisioner: false); + } + + /// + /// Configures resource to use Azure for local development and when doing a deployment via the Azure Developer CLI. + /// + /// The builder. + /// + /// + /// Callback to configure Azure resource. + /// A reference to the builder. + public static IResourceBuilder AsAzurePostgresFlexibleServerConstruct( + this IResourceBuilder builder, + IResourceBuilder? administratorLogin = null, + IResourceBuilder? administratorLoginPassword = null, + Action, ResourceModuleConstruct, PostgreSqlFlexibleServer>? configureResource = null) + { + return builder.PublishAsAzurePostgresFlexibleServerConstruct( + administratorLogin, + administratorLoginPassword, + configureResource, + useProvisioner: true); + } } diff --git a/src/Aspire.Hosting.Azure/Extensions/AzureRedisExtensions.cs b/src/Aspire.Hosting.Azure/Extensions/AzureRedisExtensions.cs index 299cbb0a2f5..4322dabf6c7 100644 --- a/src/Aspire.Hosting.Azure/Extensions/AzureRedisExtensions.cs +++ b/src/Aspire.Hosting.Azure/Extensions/AzureRedisExtensions.cs @@ -119,10 +119,7 @@ internal static IResourceBuilder PublishAsAzureRedisConstruct(thi if (builder.ApplicationBuilder.ExecutionContext.IsRunMode) { - if (builder.ApplicationBuilder.ExecutionContext.IsRunMode) - { - resourceBuilder.WithParameter(AzureBicepResource.KnownParameters.PrincipalType); - } + resourceBuilder.WithParameter(AzureBicepResource.KnownParameters.PrincipalType); } if (useProvisioner) diff --git a/src/Aspire.Hosting.Azure/Extensions/AzureSqlExtensions.cs b/src/Aspire.Hosting.Azure/Extensions/AzureSqlExtensions.cs index 9e8c9cf45e0..09e3055e9b5 100644 --- a/src/Aspire.Hosting.Azure/Extensions/AzureSqlExtensions.cs +++ b/src/Aspire.Hosting.Azure/Extensions/AzureSqlExtensions.cs @@ -95,7 +95,7 @@ internal static IResourceBuilder PublishAsAzureSqlDatab // the principalType. sqlServer.AssignProperty(x => x.Administrators.PrincipalType, construct.PrincipalTypeParameter); - var sqlFirewall = new SqlFirewallRule(construct); + var sqlFirewall = new SqlFirewallRule(construct, sqlServer); sqlFirewall.AssignProperty(x => x.StartIPAddress, "'0.0.0.0'"); sqlFirewall.AssignProperty(x => x.EndIPAddress, "'255.255.255.255'"); } From 550044a2e648a4af6160718fbfd68f02aab2313f Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Fri, 8 Mar 2024 11:11:47 +0800 Subject: [PATCH 13/50] Update Blazor FluentUI to 4.5.0 (#2723) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 4b9bda200f5..a4004d2ba0a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -80,7 +80,7 @@ - + From cfcbd111de28eb2cecbcfd355b77f393366ea36c Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 8 Mar 2024 17:52:40 +1100 Subject: [PATCH 14/50] Turn off detailed logging in ARM. (#2724) * Turn off detailed logging in ARM. * Leave response content. --- .../Provisioning/Provisioners/BicepProvisioner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs b/src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs index 585cb9d2322..08b1a6aa5fc 100644 --- a/src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs +++ b/src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs @@ -209,7 +209,7 @@ await notificationService.PublishUpdateAsync(resource, state => state with { Template = BinaryData.FromString(armTemplateContents.ToString()), Parameters = BinaryData.FromObjectAsJson(parameters), - DebugSettingDetailLevel = "RequestContent, ResponseContent", + DebugSettingDetailLevel = "ResponseContent" }), cancellationToken).ConfigureAwait(false); From 559f328f602159319de60a0f675de3417bdd4f5f Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 8 Mar 2024 18:37:57 +1100 Subject: [PATCH 15/50] Azure Cosmos via CDK (#2710) --- .../CdkSample.ApiService.csproj | 1 + .../cdk/CdkSample.ApiService/Program.cs | 21 +++- playground/cdk/CdkSample.AppHost/Program.cs | 6 +- .../CdkSample.AppHost/aspire-manifest.json | 27 ++-- .../cdk/CdkSample.AppHost/cosmos.module.bicep | 50 ++++++++ .../AzureCosmosDBResource.cs | 117 ++++++++++++++++++ .../Azure/AzureBicepResourceTests.cs | 39 ++++++ 7 files changed, 249 insertions(+), 12 deletions(-) create mode 100644 playground/cdk/CdkSample.AppHost/cosmos.module.bicep diff --git a/playground/cdk/CdkSample.ApiService/CdkSample.ApiService.csproj b/playground/cdk/CdkSample.ApiService/CdkSample.ApiService.csproj index 493052dc3be..bd02a6cc98b 100644 --- a/playground/cdk/CdkSample.ApiService/CdkSample.ApiService.csproj +++ b/playground/cdk/CdkSample.ApiService/CdkSample.ApiService.csproj @@ -9,6 +9,7 @@ + diff --git a/playground/cdk/CdkSample.ApiService/Program.cs b/playground/cdk/CdkSample.ApiService/Program.cs index 655fb820462..43561e8bd21 100644 --- a/playground/cdk/CdkSample.ApiService/Program.cs +++ b/playground/cdk/CdkSample.ApiService/Program.cs @@ -15,14 +15,16 @@ builder.AddSqlServerDbContext("sqldb"); builder.AddAzureKeyVaultClient("mykv"); builder.AddRedisClient("cache"); +builder.AddCosmosDbContext("cosmos", "cosmosdb"); builder.AddNpgsqlDbContext("pgsqldb"); var app = builder.Build(); -app.MapGet("/", async (BlobServiceClient bsc, SqlContext sqlContext, SecretClient sc, IConnectionMultiplexer connection, NpgsqlContext npgsqlContext) => +app.MapGet("/", async (BlobServiceClient bsc, SqlContext sqlContext, SecretClient sc, IConnectionMultiplexer connection, CosmosContext cosmosContext, NpgsqlContext npgsqlContext) => { return new { + cosmosDocuments = await TestCosmosAsync(cosmosContext), redisEntries = await TestRedisAsync(connection), secretChecked = await TestSecretAsync(sc), blobFiles = await TestBlobStorageAsync(bsc), @@ -103,6 +105,18 @@ static async Task> TestNpgsqlAsync(NpgsqlContext context) return entries; } +static async Task> TestCosmosAsync(CosmosContext context) +{ + await context.Database.EnsureCreatedAsync(); + + var entry = new Entry(); + await context.Entries.AddAsync(entry); + await context.SaveChangesAsync(); + + var entries = await context.Entries.ToListAsync(); + return entries; +} + public class NpgsqlContext(DbContextOptions options) : DbContext(options) { public DbSet Entries { get; set; } @@ -113,6 +127,11 @@ public class SqlContext(DbContextOptions options) : DbContext(option public DbSet Entries { get; set; } } +public class CosmosContext(DbContextOptions options) : DbContext(options) +{ + public DbSet Entries { get; set; } +} + public class Entry { public Guid Id { get; set; } = Guid.NewGuid(); diff --git a/playground/cdk/CdkSample.AppHost/Program.cs b/playground/cdk/CdkSample.AppHost/Program.cs index 89252acb288..578a3fe1c75 100644 --- a/playground/cdk/CdkSample.AppHost/Program.cs +++ b/playground/cdk/CdkSample.AppHost/Program.cs @@ -6,10 +6,10 @@ var builder = DistributedApplication.CreateBuilder(args); builder.AddAzureProvisioning(); +var cosmosdb = builder.AddAzureCosmosDBConstruct("cosmos").AddDatabase("cosmosdb"); + var sku = builder.AddParameter("storagesku"); var locationOverride = builder.AddParameter("locationOverride"); -var signaturesecret = builder.AddParameter("signaturesecret"); - var storage = builder.AddAzureConstructStorage("storage", (_, account) => { account.AssignProperty(sa => sa.Sku.Name, sku); @@ -20,6 +20,7 @@ var sqldb = builder.AddSqlServer("sql").AsAzureSqlDatabaseConstruct().AddDatabase("sqldb"); +var signaturesecret = builder.AddParameter("signaturesecret"); var keyvault = builder.AddAzureKeyVaultConstruct("mykv", (construct, keyVault) => { var secret = new KeyVaultSecret(construct, name: "mysecret"); @@ -41,6 +42,7 @@ .WithReference(sqldb) .WithReference(keyvault) .WithReference(cache) + .WithReference(cosmosdb) .WithReference(pgsqldb); // This project is only added in playground projects to support development/debugging diff --git a/playground/cdk/CdkSample.AppHost/aspire-manifest.json b/playground/cdk/CdkSample.AppHost/aspire-manifest.json index ea57b86c658..52d29bf99b3 100644 --- a/playground/cdk/CdkSample.AppHost/aspire-manifest.json +++ b/playground/cdk/CdkSample.AppHost/aspire-manifest.json @@ -1,5 +1,13 @@ { "resources": { + "cosmos": { + "type": "azure.bicep.v0", + "connectionString": "{cosmos.secretOutputs.connectionString}", + "path": "cosmos.module.bicep", + "params": { + "keyVaultName": "" + } + }, "storagesku": { "type": "parameter.v0", "value": "{storagesku.inputs.value}", @@ -18,15 +26,6 @@ } } }, - "signaturesecret": { - "type": "parameter.v0", - "value": "{signaturesecret.inputs.value}", - "inputs": { - "value": { - "type": "string" - } - } - }, "storage": { "type": "azure.bicep.v0", "path": "storage.module.bicep", @@ -65,6 +64,15 @@ "type": "value.v0", "connectionString": "{sql.connectionString};Database=sqldb" }, + "signaturesecret": { + "type": "parameter.v0", + "value": "{signaturesecret.inputs.value}", + "inputs": { + "value": { + "type": "string" + } + } + }, "mykv": { "type": "azure.bicep.v0", "connectionString": "{mykv.outputs.vaultUri}", @@ -169,6 +177,7 @@ "ConnectionStrings__sqldb": "{sqldb.connectionString}", "ConnectionStrings__mykv": "{mykv.connectionString}", "ConnectionStrings__cache": "{cache.connectionString}", + "ConnectionStrings__cosmos": "{cosmos.connectionString}", "ConnectionStrings__pgsqldb": "{pgsqldb.connectionString}" }, "bindings": { diff --git a/playground/cdk/CdkSample.AppHost/cosmos.module.bicep b/playground/cdk/CdkSample.AppHost/cosmos.module.bicep new file mode 100644 index 00000000000..c39e442e705 --- /dev/null +++ b/playground/cdk/CdkSample.AppHost/cosmos.module.bicep @@ -0,0 +1,50 @@ +targetScope = 'resourceGroup' + +@description('') +param location string = resourceGroup().location + +@description('') +param keyVaultName string + + +resource keyVault_IeF8jZvXV 'Microsoft.KeyVault/vaults@2023-02-01' existing = { + name: keyVaultName +} + +resource cosmosDBAccount_5pKmb8KAZ 'Microsoft.DocumentDB/databaseAccounts@2023-04-15' = { + name: toLower(take(concat('cosmos', uniqueString(resourceGroup().id)), 24)) + location: location + kind: 'GlobalDocumentDB' + properties: { + databaseAccountOfferType: 'Standard' + consistencyPolicy: { + defaultConsistencyLevel: 'Session' + } + locations: [ + { + locationName: location + failoverPriority: 0 + } + ] + } +} + +resource cosmosDBSqlDatabase_q2Ny71tR3 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2022-05-15' = { + parent: cosmosDBAccount_5pKmb8KAZ + name: 'cosmosdb' + location: location + properties: { + resource: { + id: 'cosmosdb' + } + } +} + +resource keyVaultSecret_Ddsc3HjrA 'Microsoft.KeyVault/vaults/secrets@2023-02-01' = { + parent: keyVault_IeF8jZvXV + name: 'connectionString' + location: location + properties: { + value: 'AccountEndpoint=${cosmosDBAccount_5pKmb8KAZ.properties.documentEndpoint};AccountKey=${cosmosDBAccount_5pKmb8KAZ.listkeys(cosmosDBAccount_5pKmb8KAZ.apiVersion).primaryMasterKey}' + } +} diff --git a/src/Aspire.Hosting.Azure/AzureCosmosDBResource.cs b/src/Aspire.Hosting.Azure/AzureCosmosDBResource.cs index 77255452eac..0e69e447fb4 100644 --- a/src/Aspire.Hosting.Azure/AzureCosmosDBResource.cs +++ b/src/Aspire.Hosting.Azure/AzureCosmosDBResource.cs @@ -4,6 +4,10 @@ using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Azure; using Aspire.Hosting.Azure.Cosmos; +using Azure.Provisioning; +using Azure.Provisioning.CosmosDB; +using Azure.Provisioning.KeyVaults; +using Azure.ResourceManager.CosmosDB.Models; namespace Aspire.Hosting; @@ -64,6 +68,63 @@ public class AzureCosmosDBResource(string name) : } } +/// +/// A resource that represents an Azure Cosmos DB. +/// +public class AzureCosmosDBConstructResource(string name, Action configureConstruct) : + AzureConstructResource(name, configureConstruct), + IResourceWithConnectionString, + IResourceWithEndpoints +{ + internal List Databases { get; } = []; + + internal EndpointReference EmulatorEndpoint => new(this, "emulator"); + + /// + /// Gets the "connectionString" reference from the secret outputs of the Azure Cosmos DB resource. + /// + public BicepSecretOutputReference ConnectionString => new("connectionString", this); + + /// + /// Gets a value indicating whether the Azure Cosmos DB resource is running in the local emulator. + /// + public bool IsEmulator => this.IsContainer(); + + /// + /// Gets the connection string template for the manifest for the Azure Cosmos DB resource. + /// + public string ConnectionStringExpression => ConnectionString.ValueExpression; + + /// + /// Gets the connection string to use for this database. + /// + /// A to observe while waiting for the task to complete. + /// The connection string to use for this database. + public async ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) + { + if (ProvisioningTaskCompletionSource is not null) + { + await ProvisioningTaskCompletionSource.Task.WaitAsync(cancellationToken).ConfigureAwait(false); + } + + return GetConnectionString(); + } + + /// + /// Gets the connection string to use for this database. + /// + /// The connection string to use for this database. + public string? GetConnectionString() + { + if (IsEmulator) + { + return AzureCosmosDBEmulatorConnectionString.Create(EmulatorEndpoint.Port); + } + + return ConnectionString.Value; + } +} + /// /// Extension methods for adding Azure Cosmos DB resources to the application model. /// @@ -85,6 +146,50 @@ public static IResourceBuilder AddAzureCosmosDB(this IDis .WithManifestPublishingCallback(resource.WriteToManifest); } + /// + /// Adds an Azure Cosmos DB connection to the application model. + /// + /// The . + /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. + /// + /// A reference to the . + public static IResourceBuilder AddAzureCosmosDBConstruct(this IDistributedApplicationBuilder builder, string name, Action, ResourceModuleConstruct, CosmosDBAccount, IEnumerable>? configureResource = null) + { + var configureConstruct = (ResourceModuleConstruct construct) => + { + var cosmosAccount = new CosmosDBAccount(construct, CosmosDBAccountKind.GlobalDocumentDB, name: name); + cosmosAccount.AssignProperty(x => x.ConsistencyPolicy.DefaultConsistencyLevel, "'Session'"); + cosmosAccount.AssignProperty(x => x.DatabaseAccountOfferType, "'Standard'"); + cosmosAccount.AssignProperty(x => x.Locations[0].LocationName, "location"); + cosmosAccount.AssignProperty(x => x.Locations[0].FailoverPriority, "0"); + + var keyVaultNameParameter = new Parameter("keyVaultName"); + construct.AddParameter(keyVaultNameParameter); + + var azureResource = (AzureCosmosDBConstructResource)construct.Resource; + var azureResourceBuilder = builder.CreateResourceBuilder(azureResource); + List cosmosSqlDatabases = new List(); + foreach (var databaseName in azureResource.Databases) + { + var cosmosSqlDatabase = new CosmosDBSqlDatabase(construct, cosmosAccount, name: databaseName); + cosmosSqlDatabases.Add(cosmosSqlDatabase); + } + + var keyVault = KeyVault.FromExisting(construct, "keyVaultName"); + _ = new KeyVaultSecret(construct, "connectionString", cosmosAccount.GetConnectionString()); + + if (configureResource != null) + { + configureResource(azureResourceBuilder, construct, cosmosAccount, cosmosSqlDatabases); + } + }; + + var resource = new AzureCosmosDBConstructResource(name, configureConstruct); + return builder.AddResource(resource) + .WithParameter(AzureBicepResource.KnownParameters.KeyVaultName) + .WithManifestPublishingCallback(resource.WriteToManifest); + } + /// /// Configures an Azure Cosmos DB resource to be emulated using the Azure Cosmos DB emulator with the NoSQL API. This resource requires an to be added to the application model. /// For more information on the Azure Cosmos DB emulator, see @@ -153,6 +258,18 @@ public static IResourceBuilder AddDatabase(this IResource builder.Resource.Databases.Add(databaseName); return builder; } + + /// + /// Adds a database to the associated Cosmos DB account resource. + /// + /// AzureCosmosDB resource builder. + /// Name of database. + /// A reference to the . + public static IResourceBuilder AddDatabase(this IResourceBuilder builder, string databaseName) + { + builder.Resource.Databases.Add(databaseName); + return builder; + } } file static class AzureCosmosDBEmulatorConnectionString diff --git a/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs b/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs index fe08ff871f3..3a89b0abba5 100644 --- a/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs +++ b/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs @@ -6,6 +6,7 @@ using Aspire.Hosting.Azure; using Aspire.Hosting.Tests.Utils; using Aspire.Hosting.Utils; +using Azure.Provisioning.CosmosDB; using Azure.Provisioning.Sql; using Azure.Provisioning.Storage; using Azure.ResourceManager.Storage.Models; @@ -122,6 +123,44 @@ public void AddAzureCosmosDb() Assert.Equal("{cosmos.secretOutputs.connectionString}", cosmos.Resource.ConnectionStringExpression); } + [Fact] + public async Task AddAzureCosmosDbConstruct() + { + var builder = DistributedApplication.CreateBuilder(); + + IEnumerable? callbackDatabases = null; + var cosmos = builder.AddAzureCosmosDBConstruct("cosmos", (resource, construct, account, databases) => + { + callbackDatabases = databases; + }); + cosmos.AddDatabase("mydatabase"); + + cosmos.Resource.SecretOutputs["connectionString"] = "mycosmosconnectionstring"; + + var manifest = await ManifestUtils.GetManifest(cosmos.Resource); + var expectedManifest = """ + { + "type": "azure.bicep.v0", + "connectionString": "{cosmos.secretOutputs.connectionString}", + "path": "cosmos.module.bicep", + "params": { + "keyVaultName": "" + } + } + """; + + Assert.Equal(expectedManifest, manifest.ToString()); + + Assert.NotNull(callbackDatabases); + Assert.Collection( + callbackDatabases, + (database) => Assert.Equal("mydatabase", database.Properties.Name) + ); + + Assert.Equal("cosmos", cosmos.Resource.Name); + Assert.Equal("mycosmosconnectionstring", cosmos.Resource.GetConnectionString()); + } + [Fact] public void AddAppConfiguration() { From 3b6551013cdae01f0bb955f2d44ac64be07a8a87 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Fri, 8 Mar 2024 16:06:41 +0800 Subject: [PATCH 16/50] Fix reading launchSettings.json with extra semicolon (#2725) --- .../ProjectResourceBuilderExtensions.cs | 2 +- .../ProjectResourceTests.cs | 35 +++++++++++++++++-- .../Properties/launchSettings.json | 4 +-- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/Aspire.Hosting/Extensions/ProjectResourceBuilderExtensions.cs b/src/Aspire.Hosting/Extensions/ProjectResourceBuilderExtensions.cs index 00d07e0c1cb..3bf42ad93e9 100644 --- a/src/Aspire.Hosting/Extensions/ProjectResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/Extensions/ProjectResourceBuilderExtensions.cs @@ -114,7 +114,7 @@ private static IResourceBuilder WithProjectDefaults(this IResou return builder; } - var urlsFromApplicationUrl = launchProfile.ApplicationUrl?.Split(';') ?? []; + var urlsFromApplicationUrl = launchProfile.ApplicationUrl?.Split(';', StringSplitOptions.RemoveEmptyEntries) ?? []; foreach (var url in urlsFromApplicationUrl) { var uri = new Uri(url); diff --git a/tests/Aspire.Hosting.Tests/ProjectResourceTests.cs b/tests/Aspire.Hosting.Tests/ProjectResourceTests.cs index 3b3817c1ffb..b7e15f8a05e 100644 --- a/tests/Aspire.Hosting.Tests/ProjectResourceTests.cs +++ b/tests/Aspire.Hosting.Tests/ProjectResourceTests.cs @@ -111,6 +111,36 @@ public void WithLaunchProfileAddsAnnotationToProject() Assert.Contains(resource.Annotations, a => a.GetType().Name == "LaunchProfileAnnotation"); } + [Fact] + public void WithLaunchProfile_ApplicationUrlTrailingSemiColon_Ignore() + { + var appBuilder = CreateBuilder(operation: DistributedApplicationOperation.Run); + + appBuilder.AddProject("projectName", launchProfileName: "https"); + using var app = appBuilder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var projectResources = appModel.GetProjectResources(); + + var resource = Assert.Single(projectResources); + + Assert.Collection( + resource.Annotations.OfType(), + a => + { + Assert.Equal("https", a.Name); + Assert.Equal("https", a.UriScheme); + Assert.Equal(7123, a.Port); + }, + a => + { + Assert.Equal("http", a.Name); + Assert.Equal("http", a.UriScheme); + Assert.Equal(5156, a.Port); + }); + } + [Fact] public void AddProjectFailsIfFileDoesNotExist() { @@ -188,9 +218,10 @@ public async Task VerifyManifest() Assert.Equal(expectedManifest, manifest.ToString()); } - private static IDistributedApplicationBuilder CreateBuilder() + private static IDistributedApplicationBuilder CreateBuilder(DistributedApplicationOperation operation = DistributedApplicationOperation.Publish) { - var appBuilder = DistributedApplication.CreateBuilder(["--publisher", "manifest"]); + var args = operation == DistributedApplicationOperation.Publish ? new[] { "--publisher", "manifest" } : Array.Empty(); + var appBuilder = DistributedApplication.CreateBuilder(args); // Block DCP from actually starting anything up as we don't need it for this test. appBuilder.Services.AddKeyedSingleton("manifest"); diff --git a/tests/testproject/TestProject.ServiceA/Properties/launchSettings.json b/tests/testproject/TestProject.ServiceA/Properties/launchSettings.json index 5bf67cb7065..fdf74885753 100644 --- a/tests/testproject/TestProject.ServiceA/Properties/launchSettings.json +++ b/tests/testproject/TestProject.ServiceA/Properties/launchSettings.json @@ -1,4 +1,4 @@ -{ +{ "$schema": "http://json.schemastore.org/launchsettings.json", "iisSettings": { "windowsAuthentication": false, @@ -22,7 +22,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "applicationUrl": "https://localhost:7123;http://localhost:5156", + "applicationUrl": "https://localhost:7123;http://localhost:5156;", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } From e86f9fd285fa32fc59d1151e9cfc92b9959ea9e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mats=20Dyr=C3=B8y?= Date: Fri, 8 Mar 2024 10:45:56 +0100 Subject: [PATCH 17/50] Fix initial resource display status (#2727) --- .../ResourcesGridColumns/StateColumnDisplay.razor | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Aspire.Dashboard/Components/ResourcesGridColumns/StateColumnDisplay.razor b/src/Aspire.Dashboard/Components/ResourcesGridColumns/StateColumnDisplay.razor index ae41dcbf635..b4f6e6a1f2a 100644 --- a/src/Aspire.Dashboard/Components/ResourcesGridColumns/StateColumnDisplay.razor +++ b/src/Aspire.Dashboard/Components/ResourcesGridColumns/StateColumnDisplay.razor @@ -30,6 +30,12 @@ else if (Resource is { State: ResourceStates.StartingState }) Color="Color.Info" Class="severity-icon"/> } +else if (Resource is { State: /* unknown */ null }) +{ + +} else { } -@Resource.State.Humanize() +@(Resource.State?.Humanize() ?? "Unknown") + @code { From bc75e1c2810f9e24247ac2a90b1455f005e6a768 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Fri, 8 Mar 2024 18:42:43 +0800 Subject: [PATCH 18/50] Add stress playground (#2600) --- Aspire.sln | 78 +- Directory.Packages.props | 1 + .../Stress/Stress.ApiService/Program.cs | 12 + .../Properties/launchSettings.json | 14 + .../Stress.ApiService.csproj | 13 + .../appsettings.Development.json | 8 + .../Stress/Stress.ApiService/appsettings.json | 9 + .../Stress.AppHost/Directory.Build.props | 8 + .../Stress.AppHost/Directory.Build.targets | 9 + playground/Stress/Stress.AppHost/Program.cs | 28 + .../Properties/launchSettings.json | 29 + .../Stress.AppHost/Stress.AppHost.csproj | 22 + .../Stress/Stress.AppHost/TestResource.cs | 83 +++ .../appsettings.Development.json | 8 + .../Stress/Stress.AppHost/appsettings.json | 9 + .../opentelemetry/proto/collector/README.md | 10 + .../collector/logs/v1/logs_service.proto | 79 ++ .../collector/logs/v1/logs_service_http.yaml | 9 + .../metrics/v1/metrics_service.proto | 79 ++ .../metrics/v1/metrics_service_http.yaml | 9 + .../collector/trace/v1/trace_service.proto | 79 ++ .../trace/v1/trace_service_http.yaml | 9 + .../proto/common/v1/common.proto | 81 +++ .../opentelemetry/proto/logs/v1/logs.proto | 203 ++++++ .../proto/metrics/v1/metrics.proto | 676 ++++++++++++++++++ .../proto/resource/v1/resource.proto | 37 + .../opentelemetry/proto/trace/v1/trace.proto | 276 +++++++ .../Stress/Stress.TelemetryService/Program.cs | 13 + .../Stress.TelemetryService.csproj | 27 + .../TelemetryStresser.cs | 119 +++ 30 files changed, 1999 insertions(+), 38 deletions(-) create mode 100644 playground/Stress/Stress.ApiService/Program.cs create mode 100644 playground/Stress/Stress.ApiService/Properties/launchSettings.json create mode 100644 playground/Stress/Stress.ApiService/Stress.ApiService.csproj create mode 100644 playground/Stress/Stress.ApiService/appsettings.Development.json create mode 100644 playground/Stress/Stress.ApiService/appsettings.json create mode 100644 playground/Stress/Stress.AppHost/Directory.Build.props create mode 100644 playground/Stress/Stress.AppHost/Directory.Build.targets create mode 100644 playground/Stress/Stress.AppHost/Program.cs create mode 100644 playground/Stress/Stress.AppHost/Properties/launchSettings.json create mode 100644 playground/Stress/Stress.AppHost/Stress.AppHost.csproj create mode 100644 playground/Stress/Stress.AppHost/TestResource.cs create mode 100644 playground/Stress/Stress.AppHost/appsettings.Development.json create mode 100644 playground/Stress/Stress.AppHost/appsettings.json create mode 100644 playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/collector/README.md create mode 100644 playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/collector/logs/v1/logs_service.proto create mode 100644 playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/collector/logs/v1/logs_service_http.yaml create mode 100644 playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/collector/metrics/v1/metrics_service.proto create mode 100644 playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/collector/metrics/v1/metrics_service_http.yaml create mode 100644 playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/collector/trace/v1/trace_service.proto create mode 100644 playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/collector/trace/v1/trace_service_http.yaml create mode 100644 playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/common/v1/common.proto create mode 100644 playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/logs/v1/logs.proto create mode 100644 playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/metrics/v1/metrics.proto create mode 100644 playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/resource/v1/resource.proto create mode 100644 playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/trace/v1/trace.proto create mode 100644 playground/Stress/Stress.TelemetryService/Program.cs create mode 100644 playground/Stress/Stress.TelemetryService/Stress.TelemetryService.csproj create mode 100644 playground/Stress/Stress.TelemetryService/TelemetryStresser.cs diff --git a/Aspire.sln b/Aspire.sln index ce803c3f0f1..6b81e1ab1e8 100644 --- a/Aspire.sln +++ b/Aspire.sln @@ -273,13 +273,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureStorageEndToEnd.AppHos EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureStorageEndToEnd.ApiService", "playground\AzureStorageEndToEnd\AzureStorageEndToEnd.ApiService\AzureStorageEndToEnd.ApiService.csproj", "{921CB408-5E37-4354-B4CF-EAE517F633DC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.NATS.Net.Tests", "tests\Aspire.NATS.Net.Tests\Aspire.NATS.Net.Tests.csproj", "{C774BE00-EE93-4148-B866-8F0F2BA1E473}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.NATS.Net.Tests", "tests\Aspire.NATS.Net.Tests\Aspire.NATS.Net.Tests.csproj", "{C774BE00-EE93-4148-B866-8F0F2BA1E473}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Dashboard.Components.Tests", "tests\Aspire.Dashboard.Components.Tests\Aspire.Dashboard.Components.Tests.csproj", "{0870A667-FB0C-4758-AEAF-9E5F092AD7C1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nats.Backend", "playground\nats\Nats.Backend\Nats.Backend.csproj", "{C4833DEC-0A4F-4504-B8D0-06C60B84119C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nats.Backend", "playground\nats\Nats.Backend\Nats.Backend.csproj", "{C4833DEC-0A4F-4504-B8D0-06C60B84119C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nats.Common", "playground\nats\Nats.Common\Nats.Common.csproj", "{9CA94707-E801-444F-A582-D5BD0104CF9B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nats.Common", "playground\nats\Nats.Common\Nats.Common.csproj", "{9CA94707-E801-444F-A582-D5BD0104CF9B}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Testing", "Testing", "{A7C6452C-FEDB-4883-9AE7-29892D260AA3}" EndProject @@ -319,15 +319,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProxylessEndToEnd.ApiServic EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProxylessEndToEnd.AppHost", "playground\ProxylessEndToEnd\ProxylessEndToEnd.AppHost\ProxylessEndToEnd.AppHost.csproj", "{0244203D-7491-4414-9C88-10BFED9C5B2D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Seq", "src\Components\Aspire.Seq\Aspire.Seq.csproj", "{42F560BA-BEB0-4A95-B673-5E50BF3EFB5E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Seq", "src\Components\Aspire.Seq\Aspire.Seq.csproj", "{42F560BA-BEB0-4A95-B673-5E50BF3EFB5E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Seq.ServiceDefaults", "playground\seq\Seq.ServiceDefaults\Seq.ServiceDefaults.csproj", "{E785C7A8-F8A3-49D6-9600-9D8E10FB5624}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Seq.ServiceDefaults", "playground\seq\Seq.ServiceDefaults\Seq.ServiceDefaults.csproj", "{E785C7A8-F8A3-49D6-9600-9D8E10FB5624}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "seq", "seq", "{78117273-982B-46F2-BA69-402C42294335}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Seq.ApiService", "playground\seq\Seq.ApiService\Seq.ApiService.csproj", "{CE25EF01-58F6-49C8-A5ED-292489DD9E62}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Seq.ApiService", "playground\seq\Seq.ApiService\Seq.ApiService.csproj", "{CE25EF01-58F6-49C8-A5ED-292489DD9E62}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Seq.AppHost", "playground\seq\Seq.AppHost\Seq.AppHost.csproj", "{D30ED884-BAB5-4FE7-9E78-C66411050F7B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Seq.AppHost", "playground\seq\Seq.AppHost\Seq.AppHost.csproj", "{D30ED884-BAB5-4FE7-9E78-C66411050F7B}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "cdk", "cdk", "{C3F48531-87D9-4E52-90AC-715A3E55751A}" EndProject @@ -339,6 +339,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CustomResources", "CustomRe EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomResources.AppHost", "playground\CustomResources\CustomResources.AppHost\CustomResources.AppHost.csproj", "{4231B6F1-1110-4992-A727-8F1176A47440}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Stress", "Stress", "{CFDA7AC5-251C-43C7-B334-71AE8040A147}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stress.AppHost", "playground\Stress\Stress.AppHost\Stress.AppHost.csproj", "{A74C1916-231A-410A-B718-A0E7BDB1DD7F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stress.TelemetryService", "playground\Stress\Stress.TelemetryService\Stress.TelemetryService.csproj", "{9114FFB4-328C-43FC-B2E4-EB6A16659113}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stress.ApiService", "playground\Stress\Stress.ApiService\Stress.ApiService.csproj", "{1167077B-F696-4A1C-BB0B-6EF70D530BB8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -853,14 +861,6 @@ Global {0244203D-7491-4414-9C88-10BFED9C5B2D}.Debug|Any CPU.Build.0 = Debug|Any CPU {0244203D-7491-4414-9C88-10BFED9C5B2D}.Release|Any CPU.ActiveCfg = Release|Any CPU {0244203D-7491-4414-9C88-10BFED9C5B2D}.Release|Any CPU.Build.0 = Release|Any CPU - {A357411A-5909-4A49-9519-12A935F84395}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A357411A-5909-4A49-9519-12A935F84395}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A357411A-5909-4A49-9519-12A935F84395}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A357411A-5909-4A49-9519-12A935F84395}.Release|Any CPU.Build.0 = Release|Any CPU - {4601F5A2-E445-41B2-9C1F-2CE016642E62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4601F5A2-E445-41B2-9C1F-2CE016642E62}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4601F5A2-E445-41B2-9C1F-2CE016642E62}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4601F5A2-E445-41B2-9C1F-2CE016642E62}.Release|Any CPU.Build.0 = Release|Any CPU {42F560BA-BEB0-4A95-B673-5E50BF3EFB5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {42F560BA-BEB0-4A95-B673-5E50BF3EFB5E}.Debug|Any CPU.Build.0 = Debug|Any CPU {42F560BA-BEB0-4A95-B673-5E50BF3EFB5E}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -877,32 +877,30 @@ Global {D30ED884-BAB5-4FE7-9E78-C66411050F7B}.Debug|Any CPU.Build.0 = Debug|Any CPU {D30ED884-BAB5-4FE7-9E78-C66411050F7B}.Release|Any CPU.ActiveCfg = Release|Any CPU {D30ED884-BAB5-4FE7-9E78-C66411050F7B}.Release|Any CPU.Build.0 = Release|Any CPU + {A357411A-5909-4A49-9519-12A935F84395}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A357411A-5909-4A49-9519-12A935F84395}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A357411A-5909-4A49-9519-12A935F84395}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A357411A-5909-4A49-9519-12A935F84395}.Release|Any CPU.Build.0 = Release|Any CPU + {4601F5A2-E445-41B2-9C1F-2CE016642E62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4601F5A2-E445-41B2-9C1F-2CE016642E62}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4601F5A2-E445-41B2-9C1F-2CE016642E62}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4601F5A2-E445-41B2-9C1F-2CE016642E62}.Release|Any CPU.Build.0 = Release|Any CPU {4231B6F1-1110-4992-A727-8F1176A47440}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4231B6F1-1110-4992-A727-8F1176A47440}.Debug|Any CPU.Build.0 = Debug|Any CPU {4231B6F1-1110-4992-A727-8F1176A47440}.Release|Any CPU.ActiveCfg = Release|Any CPU {4231B6F1-1110-4992-A727-8F1176A47440}.Release|Any CPU.Build.0 = Release|Any CPU - {041C0A17-0968-4EB8-9208-5B8E257A0B33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {041C0A17-0968-4EB8-9208-5B8E257A0B33}.Debug|Any CPU.Build.0 = Debug|Any CPU - {041C0A17-0968-4EB8-9208-5B8E257A0B33}.Release|Any CPU.ActiveCfg = Release|Any CPU - {041C0A17-0968-4EB8-9208-5B8E257A0B33}.Release|Any CPU.Build.0 = Release|Any CPU - {3D2D2428-BCB4-464F-9D03-7E9E6739D54B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3D2D2428-BCB4-464F-9D03-7E9E6739D54B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3D2D2428-BCB4-464F-9D03-7E9E6739D54B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3D2D2428-BCB4-464F-9D03-7E9E6739D54B}.Release|Any CPU.Build.0 = Release|Any CPU - {6723830C-7E39-4709-836B-17E5EE426751}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6723830C-7E39-4709-836B-17E5EE426751}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6723830C-7E39-4709-836B-17E5EE426751}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6723830C-7E39-4709-836B-17E5EE426751}.Release|Any CPU.Build.0 = Release|Any CPU - {5345A33F-B845-4F2D-A934-F9D73327B1CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5345A33F-B845-4F2D-A934-F9D73327B1CE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5345A33F-B845-4F2D-A934-F9D73327B1CE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5345A33F-B845-4F2D-A934-F9D73327B1CE}.Release|Any CPU.Build.0 = Release|Any CPU - {CE25EF01-58F6-49C8-A5ED-292489DD9E62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CE25EF01-58F6-49C8-A5ED-292489DD9E62}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D30ED884-BAB5-4FE7-9E78-C66411050F7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D30ED884-BAB5-4FE7-9E78-C66411050F7B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E785C7A8-F8A3-49D6-9600-9D8E10FB5624}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E785C7A8-F8A3-49D6-9600-9D8E10FB5624}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A74C1916-231A-410A-B718-A0E7BDB1DD7F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A74C1916-231A-410A-B718-A0E7BDB1DD7F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A74C1916-231A-410A-B718-A0E7BDB1DD7F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A74C1916-231A-410A-B718-A0E7BDB1DD7F}.Release|Any CPU.Build.0 = Release|Any CPU + {9114FFB4-328C-43FC-B2E4-EB6A16659113}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9114FFB4-328C-43FC-B2E4-EB6A16659113}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9114FFB4-328C-43FC-B2E4-EB6A16659113}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9114FFB4-328C-43FC-B2E4-EB6A16659113}.Release|Any CPU.Build.0 = Release|Any CPU + {1167077B-F696-4A1C-BB0B-6EF70D530BB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1167077B-F696-4A1C-BB0B-6EF70D530BB8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1167077B-F696-4A1C-BB0B-6EF70D530BB8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1167077B-F696-4A1C-BB0B-6EF70D530BB8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1056,8 +1054,8 @@ Global {51654CD7-2E05-4664-B2EB-95308A300609} = {9C30FFD6-2262-45E7-B010-24B30E0433C2} {0244203D-7491-4414-9C88-10BFED9C5B2D} = {9C30FFD6-2262-45E7-B010-24B30E0433C2} {42F560BA-BEB0-4A95-B673-5E50BF3EFB5E} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2} - {78117273-982B-46F2-BA69-402C42294335} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0} {E785C7A8-F8A3-49D6-9600-9D8E10FB5624} = {78117273-982B-46F2-BA69-402C42294335} + {78117273-982B-46F2-BA69-402C42294335} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0} {CE25EF01-58F6-49C8-A5ED-292489DD9E62} = {78117273-982B-46F2-BA69-402C42294335} {D30ED884-BAB5-4FE7-9E78-C66411050F7B} = {78117273-982B-46F2-BA69-402C42294335} {C3F48531-87D9-4E52-90AC-715A3E55751A} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0} @@ -1065,6 +1063,10 @@ Global {4601F5A2-E445-41B2-9C1F-2CE016642E62} = {C3F48531-87D9-4E52-90AC-715A3E55751A} {867A00A7-AF8E-4396-9583-982FBB31762C} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0} {4231B6F1-1110-4992-A727-8F1176A47440} = {867A00A7-AF8E-4396-9583-982FBB31762C} + {CFDA7AC5-251C-43C7-B334-71AE8040A147} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0} + {A74C1916-231A-410A-B718-A0E7BDB1DD7F} = {CFDA7AC5-251C-43C7-B334-71AE8040A147} + {9114FFB4-328C-43FC-B2E4-EB6A16659113} = {CFDA7AC5-251C-43C7-B334-71AE8040A147} + {1167077B-F696-4A1C-BB0B-6EF70D530BB8} = {CFDA7AC5-251C-43C7-B334-71AE8040A147} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6DCEDFEC-988E-4CB3-B45B-191EB5086E0C} diff --git a/Directory.Packages.props b/Directory.Packages.props index a4004d2ba0a..87801159540 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -74,6 +74,7 @@ + diff --git a/playground/Stress/Stress.ApiService/Program.cs b/playground/Stress/Stress.ApiService/Program.cs new file mode 100644 index 00000000000..2015dba3099 --- /dev/null +++ b/playground/Stress/Stress.ApiService/Program.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +var app = builder.Build(); + +app.MapGet("/", () => "Hello world"); + +app.Run(); diff --git a/playground/Stress/Stress.ApiService/Properties/launchSettings.json b/playground/Stress/Stress.ApiService/Properties/launchSettings.json new file mode 100644 index 00000000000..f7bf310e7ae --- /dev/null +++ b/playground/Stress/Stress.ApiService/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5180", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/playground/Stress/Stress.ApiService/Stress.ApiService.csproj b/playground/Stress/Stress.ApiService/Stress.ApiService.csproj new file mode 100644 index 00000000000..88b35ed33aa --- /dev/null +++ b/playground/Stress/Stress.ApiService/Stress.ApiService.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/playground/Stress/Stress.ApiService/appsettings.Development.json b/playground/Stress/Stress.ApiService/appsettings.Development.json new file mode 100644 index 00000000000..0c208ae9181 --- /dev/null +++ b/playground/Stress/Stress.ApiService/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/playground/Stress/Stress.ApiService/appsettings.json b/playground/Stress/Stress.ApiService/appsettings.json new file mode 100644 index 00000000000..10f68b8c8b4 --- /dev/null +++ b/playground/Stress/Stress.ApiService/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/playground/Stress/Stress.AppHost/Directory.Build.props b/playground/Stress/Stress.AppHost/Directory.Build.props new file mode 100644 index 00000000000..b9b39c05e81 --- /dev/null +++ b/playground/Stress/Stress.AppHost/Directory.Build.props @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/playground/Stress/Stress.AppHost/Directory.Build.targets b/playground/Stress/Stress.AppHost/Directory.Build.targets new file mode 100644 index 00000000000..b7ba77268f8 --- /dev/null +++ b/playground/Stress/Stress.AppHost/Directory.Build.targets @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/playground/Stress/Stress.AppHost/Program.cs b/playground/Stress/Stress.AppHost/Program.cs new file mode 100644 index 00000000000..d9316e88aaf --- /dev/null +++ b/playground/Stress/Stress.AppHost/Program.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +var builder = DistributedApplication.CreateBuilder(args); + +for (var i = 0; i < 10; i++) +{ + builder.AddTestResource($"test-{i:0000}"); +} + +var serviceBuilder = builder.AddProject("stress-apiservice", launchProfileName: null); + +for (var i = 0; i < 30; i++) +{ + var port = 5180 + i; + serviceBuilder.WithHttpEndpoint(port, $"http-{port}"); +} + +builder.AddProject("stress-telemetryservice"); + +// This project is only added in playground projects to support development/debugging +// of the dashboard. It is not required in end developer code. Comment out this code +// to test end developer dashboard launch experience. Refer to Directory.Build.props +// for the path to the dashboard binary (defaults to the Aspire.Dashboard bin output +// in the artifacts dir). +builder.AddProject(KnownResourceNames.AspireDashboard); + +builder.Build().Run(); diff --git a/playground/Stress/Stress.AppHost/Properties/launchSettings.json b/playground/Stress/Stress.AppHost/Properties/launchSettings.json new file mode 100644 index 00000000000..32514f6378b --- /dev/null +++ b/playground/Stress/Stress.AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "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_ASPIRE_SHOW_DASHBOARD_RESOURCES": "true" + } + }, + "generate-manifest": { + "commandName": "Project", + "launchBrowser": true, + "dotnetRunMessages": true, + "commandLineArgs": "--publisher manifest --output-path aspire-manifest.json", + "applicationUrl": "http://localhost:15888", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16175" + } + } + } +} diff --git a/playground/Stress/Stress.AppHost/Stress.AppHost.csproj b/playground/Stress/Stress.AppHost/Stress.AppHost.csproj new file mode 100644 index 00000000000..ce064addae3 --- /dev/null +++ b/playground/Stress/Stress.AppHost/Stress.AppHost.csproj @@ -0,0 +1,22 @@ + + + + Exe + net8.0 + enable + enable + true + + + + + + + + + + + + + + diff --git a/playground/Stress/Stress.AppHost/TestResource.cs b/playground/Stress/Stress.AppHost/TestResource.cs new file mode 100644 index 00000000000..8192748888f --- /dev/null +++ b/playground/Stress/Stress.AppHost/TestResource.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using Aspire.Hosting.Lifecycle; +using Microsoft.Extensions.Logging; + +static class TestResourceExtensions +{ + public static IResourceBuilder AddTestResource(this IDistributedApplicationBuilder builder, string name) + { + builder.Services.TryAddLifecycleHook(); + + var rb = builder.AddResource(new TestResource(name)) + .WithInitialState(new() + { + ResourceType = "Test Resource", + State = "Starting", + Properties = [ + ("P1", "P2"), + (CustomResourceKnownProperties.Source, "Custom") + ] + }) + .ExcludeFromManifest(); + + return rb; + } +} + +internal sealed class TestResourceLifecycleHook(ResourceNotificationService notificationService, ResourceLoggerService loggerService) : IDistributedApplicationLifecycleHook, IAsyncDisposable +{ + private readonly CancellationTokenSource _tokenSource = new(); + + public Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default) + { + foreach (var resource in appModel.Resources.OfType()) + { + var states = new[] { "Starting", "Running", "Finished" }; + + var logger = loggerService.GetLogger(resource); + + Task.Run(async () => + { + var seconds = Random.Shared.Next(2, 12); + + logger.LogInformation("Starting test resource {ResourceName} with update interval {Interval} seconds", resource.Name, seconds); + + await notificationService.PublishUpdateAsync(resource, state => state with + { + Properties = [.. state.Properties, ("Interval", seconds.ToString(CultureInfo.InvariantCulture))] + }); + + using var timer = new PeriodicTimer(TimeSpan.FromSeconds(seconds)); + + while (await timer.WaitForNextTickAsync(_tokenSource.Token)) + { + var randomState = states[Random.Shared.Next(0, states.Length)]; + + await notificationService.PublishUpdateAsync(resource, state => state with + { + State = randomState + }); + + logger.LogInformation("Test resource {ResourceName} is now in state {State}", resource.Name, randomState); + } + }, + cancellationToken); + } + + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() + { + _tokenSource.Cancel(); + return default; + } +} + +sealed class TestResource(string name) : Resource(name) +{ + +} diff --git a/playground/Stress/Stress.AppHost/appsettings.Development.json b/playground/Stress/Stress.AppHost/appsettings.Development.json new file mode 100644 index 00000000000..0c208ae9181 --- /dev/null +++ b/playground/Stress/Stress.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/playground/Stress/Stress.AppHost/appsettings.json b/playground/Stress/Stress.AppHost/appsettings.json new file mode 100644 index 00000000000..31c092aa450 --- /dev/null +++ b/playground/Stress/Stress.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/collector/README.md b/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/collector/README.md new file mode 100644 index 00000000000..f82dbb0278b --- /dev/null +++ b/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/collector/README.md @@ -0,0 +1,10 @@ +# OpenTelemetry Collector Proto + +This package describes the OpenTelemetry collector protocol. + +## Packages + +1. `common` package contains the common messages shared between different services. +2. `trace` package contains the Trace Service protos. +3. `metrics` package contains the Metrics Service protos. +4. `logs` package contains the Logs Service protos. diff --git a/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/collector/logs/v1/logs_service.proto b/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/collector/logs/v1/logs_service.proto new file mode 100644 index 00000000000..8260d8aaeb8 --- /dev/null +++ b/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/collector/logs/v1/logs_service.proto @@ -0,0 +1,79 @@ +// Copyright 2020, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package opentelemetry.proto.collector.logs.v1; + +import "opentelemetry/proto/logs/v1/logs.proto"; + +option csharp_namespace = "OpenTelemetry.Proto.Collector.Logs.V1"; +option java_multiple_files = true; +option java_package = "io.opentelemetry.proto.collector.logs.v1"; +option java_outer_classname = "LogsServiceProto"; +option go_package = "go.opentelemetry.io/proto/otlp/collector/logs/v1"; + +// Service that can be used to push logs between one Application instrumented with +// OpenTelemetry and an collector, or between an collector and a central collector (in this +// case logs are sent/received to/from multiple Applications). +service LogsService { + // For performance reasons, it is recommended to keep this RPC + // alive for the entire life of the application. + rpc Export(ExportLogsServiceRequest) returns (ExportLogsServiceResponse) {} +} + +message ExportLogsServiceRequest { + // An array of ResourceLogs. + // For data coming from a single resource this array will typically contain one + // element. Intermediary nodes (such as OpenTelemetry Collector) that receive + // data from multiple origins typically batch the data before forwarding further and + // in that case this array will contain multiple elements. + repeated opentelemetry.proto.logs.v1.ResourceLogs resource_logs = 1; +} + +message ExportLogsServiceResponse { + // The details of a partially successful export request. + // + // If the request is only partially accepted + // (i.e. when the server accepts only parts of the data and rejects the rest) + // the server MUST initialize the `partial_success` field and MUST + // set the `rejected_` with the number of items it rejected. + // + // Servers MAY also make use of the `partial_success` field to convey + // warnings/suggestions to senders even when the request was fully accepted. + // In such cases, the `rejected_` MUST have a value of `0` and + // the `error_message` MUST be non-empty. + // + // A `partial_success` message with an empty value (rejected_ = 0 and + // `error_message` = "") is equivalent to it not being set/present. Senders + // SHOULD interpret it the same way as in the full success case. + ExportLogsPartialSuccess partial_success = 1; +} + +message ExportLogsPartialSuccess { + // The number of rejected log records. + // + // A `rejected_` field holding a `0` value indicates that the + // request was fully accepted. + int64 rejected_log_records = 1; + + // A developer-facing human-readable message in English. It should be used + // either to explain why the server rejected parts of the data during a partial + // success or to convey warnings/suggestions during a full success. The message + // should offer guidance on how users can address such issues. + // + // error_message is an optional field. An error_message with an empty value + // is equivalent to it not being set. + string error_message = 2; +} diff --git a/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/collector/logs/v1/logs_service_http.yaml b/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/collector/logs/v1/logs_service_http.yaml new file mode 100644 index 00000000000..507473b9b3b --- /dev/null +++ b/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/collector/logs/v1/logs_service_http.yaml @@ -0,0 +1,9 @@ +# This is an API configuration to generate an HTTP/JSON -> gRPC gateway for the +# OpenTelemetry service using github.com/grpc-ecosystem/grpc-gateway. +type: google.api.Service +config_version: 3 +http: + rules: + - selector: opentelemetry.proto.collector.logs.v1.LogsService.Export + post: /v1/logs + body: "*" \ No newline at end of file diff --git a/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/collector/metrics/v1/metrics_service.proto b/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/collector/metrics/v1/metrics_service.proto new file mode 100644 index 00000000000..dd48f1ad3a1 --- /dev/null +++ b/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/collector/metrics/v1/metrics_service.proto @@ -0,0 +1,79 @@ +// Copyright 2019, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package opentelemetry.proto.collector.metrics.v1; + +import "opentelemetry/proto/metrics/v1/metrics.proto"; + +option csharp_namespace = "OpenTelemetry.Proto.Collector.Metrics.V1"; +option java_multiple_files = true; +option java_package = "io.opentelemetry.proto.collector.metrics.v1"; +option java_outer_classname = "MetricsServiceProto"; +option go_package = "go.opentelemetry.io/proto/otlp/collector/metrics/v1"; + +// Service that can be used to push metrics between one Application +// instrumented with OpenTelemetry and a collector, or between a collector and a +// central collector. +service MetricsService { + // For performance reasons, it is recommended to keep this RPC + // alive for the entire life of the application. + rpc Export(ExportMetricsServiceRequest) returns (ExportMetricsServiceResponse) {} +} + +message ExportMetricsServiceRequest { + // An array of ResourceMetrics. + // For data coming from a single resource this array will typically contain one + // element. Intermediary nodes (such as OpenTelemetry Collector) that receive + // data from multiple origins typically batch the data before forwarding further and + // in that case this array will contain multiple elements. + repeated opentelemetry.proto.metrics.v1.ResourceMetrics resource_metrics = 1; +} + +message ExportMetricsServiceResponse { + // The details of a partially successful export request. + // + // If the request is only partially accepted + // (i.e. when the server accepts only parts of the data and rejects the rest) + // the server MUST initialize the `partial_success` field and MUST + // set the `rejected_` with the number of items it rejected. + // + // Servers MAY also make use of the `partial_success` field to convey + // warnings/suggestions to senders even when the request was fully accepted. + // In such cases, the `rejected_` MUST have a value of `0` and + // the `error_message` MUST be non-empty. + // + // A `partial_success` message with an empty value (rejected_ = 0 and + // `error_message` = "") is equivalent to it not being set/present. Senders + // SHOULD interpret it the same way as in the full success case. + ExportMetricsPartialSuccess partial_success = 1; +} + +message ExportMetricsPartialSuccess { + // The number of rejected data points. + // + // A `rejected_` field holding a `0` value indicates that the + // request was fully accepted. + int64 rejected_data_points = 1; + + // A developer-facing human-readable message in English. It should be used + // either to explain why the server rejected parts of the data during a partial + // success or to convey warnings/suggestions during a full success. The message + // should offer guidance on how users can address such issues. + // + // error_message is an optional field. An error_message with an empty value + // is equivalent to it not being set. + string error_message = 2; +} diff --git a/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/collector/metrics/v1/metrics_service_http.yaml b/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/collector/metrics/v1/metrics_service_http.yaml new file mode 100644 index 00000000000..a5456502607 --- /dev/null +++ b/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/collector/metrics/v1/metrics_service_http.yaml @@ -0,0 +1,9 @@ +# This is an API configuration to generate an HTTP/JSON -> gRPC gateway for the +# OpenTelemetry service using github.com/grpc-ecosystem/grpc-gateway. +type: google.api.Service +config_version: 3 +http: + rules: + - selector: opentelemetry.proto.collector.metrics.v1.MetricsService.Export + post: /v1/metrics + body: "*" \ No newline at end of file diff --git a/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/collector/trace/v1/trace_service.proto b/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/collector/trace/v1/trace_service.proto new file mode 100644 index 00000000000..d6fe67f9e55 --- /dev/null +++ b/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/collector/trace/v1/trace_service.proto @@ -0,0 +1,79 @@ +// Copyright 2019, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package opentelemetry.proto.collector.trace.v1; + +import "opentelemetry/proto/trace/v1/trace.proto"; + +option csharp_namespace = "OpenTelemetry.Proto.Collector.Trace.V1"; +option java_multiple_files = true; +option java_package = "io.opentelemetry.proto.collector.trace.v1"; +option java_outer_classname = "TraceServiceProto"; +option go_package = "go.opentelemetry.io/proto/otlp/collector/trace/v1"; + +// Service that can be used to push spans between one Application instrumented with +// OpenTelemetry and a collector, or between a collector and a central collector (in this +// case spans are sent/received to/from multiple Applications). +service TraceService { + // For performance reasons, it is recommended to keep this RPC + // alive for the entire life of the application. + rpc Export(ExportTraceServiceRequest) returns (ExportTraceServiceResponse) {} +} + +message ExportTraceServiceRequest { + // An array of ResourceSpans. + // For data coming from a single resource this array will typically contain one + // element. Intermediary nodes (such as OpenTelemetry Collector) that receive + // data from multiple origins typically batch the data before forwarding further and + // in that case this array will contain multiple elements. + repeated opentelemetry.proto.trace.v1.ResourceSpans resource_spans = 1; +} + +message ExportTraceServiceResponse { + // The details of a partially successful export request. + // + // If the request is only partially accepted + // (i.e. when the server accepts only parts of the data and rejects the rest) + // the server MUST initialize the `partial_success` field and MUST + // set the `rejected_` with the number of items it rejected. + // + // Servers MAY also make use of the `partial_success` field to convey + // warnings/suggestions to senders even when the request was fully accepted. + // In such cases, the `rejected_` MUST have a value of `0` and + // the `error_message` MUST be non-empty. + // + // A `partial_success` message with an empty value (rejected_ = 0 and + // `error_message` = "") is equivalent to it not being set/present. Senders + // SHOULD interpret it the same way as in the full success case. + ExportTracePartialSuccess partial_success = 1; +} + +message ExportTracePartialSuccess { + // The number of rejected spans. + // + // A `rejected_` field holding a `0` value indicates that the + // request was fully accepted. + int64 rejected_spans = 1; + + // A developer-facing human-readable message in English. It should be used + // either to explain why the server rejected parts of the data during a partial + // success or to convey warnings/suggestions during a full success. The message + // should offer guidance on how users can address such issues. + // + // error_message is an optional field. An error_message with an empty value + // is equivalent to it not being set. + string error_message = 2; +} diff --git a/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/collector/trace/v1/trace_service_http.yaml b/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/collector/trace/v1/trace_service_http.yaml new file mode 100644 index 00000000000..d091b3a8d53 --- /dev/null +++ b/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/collector/trace/v1/trace_service_http.yaml @@ -0,0 +1,9 @@ +# This is an API configuration to generate an HTTP/JSON -> gRPC gateway for the +# OpenTelemetry service using github.com/grpc-ecosystem/grpc-gateway. +type: google.api.Service +config_version: 3 +http: + rules: + - selector: opentelemetry.proto.collector.trace.v1.TraceService.Export + post: /v1/traces + body: "*" diff --git a/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/common/v1/common.proto b/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/common/v1/common.proto new file mode 100644 index 00000000000..ff8a21a1fa0 --- /dev/null +++ b/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/common/v1/common.proto @@ -0,0 +1,81 @@ +// Copyright 2019, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package opentelemetry.proto.common.v1; + +option csharp_namespace = "OpenTelemetry.Proto.Common.V1"; +option java_multiple_files = true; +option java_package = "io.opentelemetry.proto.common.v1"; +option java_outer_classname = "CommonProto"; +option go_package = "go.opentelemetry.io/proto/otlp/common/v1"; + +// AnyValue is used to represent any type of attribute value. AnyValue may contain a +// primitive value such as a string or integer or it may contain an arbitrary nested +// object containing arrays, key-value lists and primitives. +message AnyValue { + // The value is one of the listed fields. It is valid for all values to be unspecified + // in which case this AnyValue is considered to be "empty". + oneof value { + string string_value = 1; + bool bool_value = 2; + int64 int_value = 3; + double double_value = 4; + ArrayValue array_value = 5; + KeyValueList kvlist_value = 6; + bytes bytes_value = 7; + } +} + +// ArrayValue is a list of AnyValue messages. We need ArrayValue as a message +// since oneof in AnyValue does not allow repeated fields. +message ArrayValue { + // Array of values. The array may be empty (contain 0 elements). + repeated AnyValue values = 1; +} + +// KeyValueList is a list of KeyValue messages. We need KeyValueList as a message +// since `oneof` in AnyValue does not allow repeated fields. Everywhere else where we need +// a list of KeyValue messages (e.g. in Span) we use `repeated KeyValue` directly to +// avoid unnecessary extra wrapping (which slows down the protocol). The 2 approaches +// are semantically equivalent. +message KeyValueList { + // A collection of key/value pairs of key-value pairs. The list may be empty (may + // contain 0 elements). + // The keys MUST be unique (it is not allowed to have more than one + // value with the same key). + repeated KeyValue values = 1; +} + +// KeyValue is a key-value pair that is used to store Span attributes, Link +// attributes, etc. +message KeyValue { + string key = 1; + AnyValue value = 2; +} + +// InstrumentationScope is a message representing the instrumentation scope information +// such as the fully qualified name and version. +message InstrumentationScope { + // An empty instrumentation scope name means the name is unknown. + string name = 1; + string version = 2; + + // Additional attributes that describe the scope. [Optional]. + // Attribute keys MUST be unique (it is not allowed to have more than one + // attribute with the same key). + repeated KeyValue attributes = 3; + uint32 dropped_attributes_count = 4; +} diff --git a/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/logs/v1/logs.proto b/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/logs/v1/logs.proto new file mode 100644 index 00000000000..0b4b649729c --- /dev/null +++ b/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/logs/v1/logs.proto @@ -0,0 +1,203 @@ +// Copyright 2020, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package opentelemetry.proto.logs.v1; + +import "opentelemetry/proto/common/v1/common.proto"; +import "opentelemetry/proto/resource/v1/resource.proto"; + +option csharp_namespace = "OpenTelemetry.Proto.Logs.V1"; +option java_multiple_files = true; +option java_package = "io.opentelemetry.proto.logs.v1"; +option java_outer_classname = "LogsProto"; +option go_package = "go.opentelemetry.io/proto/otlp/logs/v1"; + +// LogsData represents the logs data that can be stored in a persistent storage, +// OR can be embedded by other protocols that transfer OTLP logs data but do not +// implement the OTLP protocol. +// +// The main difference between this message and collector protocol is that +// in this message there will not be any "control" or "metadata" specific to +// OTLP protocol. +// +// When new fields are added into this message, the OTLP request MUST be updated +// as well. +message LogsData { + // An array of ResourceLogs. + // For data coming from a single resource this array will typically contain + // one element. Intermediary nodes that receive data from multiple origins + // typically batch the data before forwarding further and in that case this + // array will contain multiple elements. + repeated ResourceLogs resource_logs = 1; +} + +// A collection of ScopeLogs from a Resource. +message ResourceLogs { + reserved 1000; + + // The resource for the logs in this message. + // If this field is not set then resource info is unknown. + opentelemetry.proto.resource.v1.Resource resource = 1; + + // A list of ScopeLogs that originate from a resource. + repeated ScopeLogs scope_logs = 2; + + // This schema_url applies to the data in the "resource" field. It does not apply + // to the data in the "scope_logs" field which have their own schema_url field. + string schema_url = 3; +} + +// A collection of Logs produced by a Scope. +message ScopeLogs { + // The instrumentation scope information for the logs in this message. + // Semantically when InstrumentationScope isn't set, it is equivalent with + // an empty instrumentation scope name (unknown). + opentelemetry.proto.common.v1.InstrumentationScope scope = 1; + + // A list of log records. + repeated LogRecord log_records = 2; + + // This schema_url applies to all logs in the "logs" field. + string schema_url = 3; +} + +// Possible values for LogRecord.SeverityNumber. +enum SeverityNumber { + // UNSPECIFIED is the default SeverityNumber, it MUST NOT be used. + SEVERITY_NUMBER_UNSPECIFIED = 0; + SEVERITY_NUMBER_TRACE = 1; + SEVERITY_NUMBER_TRACE2 = 2; + SEVERITY_NUMBER_TRACE3 = 3; + SEVERITY_NUMBER_TRACE4 = 4; + SEVERITY_NUMBER_DEBUG = 5; + SEVERITY_NUMBER_DEBUG2 = 6; + SEVERITY_NUMBER_DEBUG3 = 7; + SEVERITY_NUMBER_DEBUG4 = 8; + SEVERITY_NUMBER_INFO = 9; + SEVERITY_NUMBER_INFO2 = 10; + SEVERITY_NUMBER_INFO3 = 11; + SEVERITY_NUMBER_INFO4 = 12; + SEVERITY_NUMBER_WARN = 13; + SEVERITY_NUMBER_WARN2 = 14; + SEVERITY_NUMBER_WARN3 = 15; + SEVERITY_NUMBER_WARN4 = 16; + SEVERITY_NUMBER_ERROR = 17; + SEVERITY_NUMBER_ERROR2 = 18; + SEVERITY_NUMBER_ERROR3 = 19; + SEVERITY_NUMBER_ERROR4 = 20; + SEVERITY_NUMBER_FATAL = 21; + SEVERITY_NUMBER_FATAL2 = 22; + SEVERITY_NUMBER_FATAL3 = 23; + SEVERITY_NUMBER_FATAL4 = 24; +} + +// LogRecordFlags is defined as a protobuf 'uint32' type and is to be used as +// bit-fields. Each non-zero value defined in this enum is a bit-mask. +// To extract the bit-field, for example, use an expression like: +// +// (logRecord.flags & LOG_RECORD_FLAGS_TRACE_FLAGS_MASK) +// +enum LogRecordFlags { + // The zero value for the enum. Should not be used for comparisons. + // Instead use bitwise "and" with the appropriate mask as shown above. + LOG_RECORD_FLAGS_DO_NOT_USE = 0; + + // Bits 0-7 are used for trace flags. + LOG_RECORD_FLAGS_TRACE_FLAGS_MASK = 0x000000FF; + + // Bits 8-31 are reserved for future use. +} + +// A log record according to OpenTelemetry Log Data Model: +// https://github.com/open-telemetry/oteps/blob/main/text/logs/0097-log-data-model.md +message LogRecord { + reserved 4; + + // time_unix_nano is the time when the event occurred. + // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. + // Value of 0 indicates unknown or missing timestamp. + fixed64 time_unix_nano = 1; + + // Time when the event was observed by the collection system. + // For events that originate in OpenTelemetry (e.g. using OpenTelemetry Logging SDK) + // this timestamp is typically set at the generation time and is equal to Timestamp. + // For events originating externally and collected by OpenTelemetry (e.g. using + // Collector) this is the time when OpenTelemetry's code observed the event measured + // by the clock of the OpenTelemetry code. This field MUST be set once the event is + // observed by OpenTelemetry. + // + // For converting OpenTelemetry log data to formats that support only one timestamp or + // when receiving OpenTelemetry log data by recipients that support only one timestamp + // internally the following logic is recommended: + // - Use time_unix_nano if it is present, otherwise use observed_time_unix_nano. + // + // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. + // Value of 0 indicates unknown or missing timestamp. + fixed64 observed_time_unix_nano = 11; + + // Numerical value of the severity, normalized to values described in Log Data Model. + // [Optional]. + SeverityNumber severity_number = 2; + + // The severity text (also known as log level). The original string representation as + // it is known at the source. [Optional]. + string severity_text = 3; + + // A value containing the body of the log record. Can be for example a human-readable + // string message (including multi-line) describing the event in a free form or it can + // be a structured data composed of arrays and maps of other values. [Optional]. + opentelemetry.proto.common.v1.AnyValue body = 5; + + // Additional attributes that describe the specific event occurrence. [Optional]. + // Attribute keys MUST be unique (it is not allowed to have more than one + // attribute with the same key). + repeated opentelemetry.proto.common.v1.KeyValue attributes = 6; + uint32 dropped_attributes_count = 7; + + // Flags, a bit field. 8 least significant bits are the trace flags as + // defined in W3C Trace Context specification. 24 most significant bits are reserved + // and must be set to 0. Readers must not assume that 24 most significant bits + // will be zero and must correctly mask the bits when reading 8-bit trace flag (use + // flags & LOG_RECORD_FLAGS_TRACE_FLAGS_MASK). [Optional]. + fixed32 flags = 8; + + // A unique identifier for a trace. All logs from the same trace share + // the same `trace_id`. The ID is a 16-byte array. An ID with all zeroes OR + // of length other than 16 bytes is considered invalid (empty string in OTLP/JSON + // is zero-length and thus is also invalid). + // + // This field is optional. + // + // The receivers SHOULD assume that the log record is not associated with a + // trace if any of the following is true: + // - the field is not present, + // - the field contains an invalid value. + bytes trace_id = 9; + + // A unique identifier for a span within a trace, assigned when the span + // is created. The ID is an 8-byte array. An ID with all zeroes OR of length + // other than 8 bytes is considered invalid (empty string in OTLP/JSON + // is zero-length and thus is also invalid). + // + // This field is optional. If the sender specifies a valid span_id then it SHOULD also + // specify a valid trace_id. + // + // The receivers SHOULD assume that the log record is not associated with a + // span if any of the following is true: + // - the field is not present, + // - the field contains an invalid value. + bytes span_id = 10; +} diff --git a/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/metrics/v1/metrics.proto b/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/metrics/v1/metrics.proto new file mode 100644 index 00000000000..da986dda185 --- /dev/null +++ b/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/metrics/v1/metrics.proto @@ -0,0 +1,676 @@ +// Copyright 2019, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package opentelemetry.proto.metrics.v1; + +import "opentelemetry/proto/common/v1/common.proto"; +import "opentelemetry/proto/resource/v1/resource.proto"; + +option csharp_namespace = "OpenTelemetry.Proto.Metrics.V1"; +option java_multiple_files = true; +option java_package = "io.opentelemetry.proto.metrics.v1"; +option java_outer_classname = "MetricsProto"; +option go_package = "go.opentelemetry.io/proto/otlp/metrics/v1"; + +// MetricsData represents the metrics data that can be stored in a persistent +// storage, OR can be embedded by other protocols that transfer OTLP metrics +// data but do not implement the OTLP protocol. +// +// The main difference between this message and collector protocol is that +// in this message there will not be any "control" or "metadata" specific to +// OTLP protocol. +// +// When new fields are added into this message, the OTLP request MUST be updated +// as well. +message MetricsData { + // An array of ResourceMetrics. + // For data coming from a single resource this array will typically contain + // one element. Intermediary nodes that receive data from multiple origins + // typically batch the data before forwarding further and in that case this + // array will contain multiple elements. + repeated ResourceMetrics resource_metrics = 1; +} + +// A collection of ScopeMetrics from a Resource. +message ResourceMetrics { + reserved 1000; + + // The resource for the metrics in this message. + // If this field is not set then no resource info is known. + opentelemetry.proto.resource.v1.Resource resource = 1; + + // A list of metrics that originate from a resource. + repeated ScopeMetrics scope_metrics = 2; + + // This schema_url applies to the data in the "resource" field. It does not apply + // to the data in the "scope_metrics" field which have their own schema_url field. + string schema_url = 3; +} + +// A collection of Metrics produced by an Scope. +message ScopeMetrics { + // The instrumentation scope information for the metrics in this message. + // Semantically when InstrumentationScope isn't set, it is equivalent with + // an empty instrumentation scope name (unknown). + opentelemetry.proto.common.v1.InstrumentationScope scope = 1; + + // A list of metrics that originate from an instrumentation library. + repeated Metric metrics = 2; + + // This schema_url applies to all metrics in the "metrics" field. + string schema_url = 3; +} + +// Defines a Metric which has one or more timeseries. The following is a +// brief summary of the Metric data model. For more details, see: +// +// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md +// +// +// The data model and relation between entities is shown in the +// diagram below. Here, "DataPoint" is the term used to refer to any +// one of the specific data point value types, and "points" is the term used +// to refer to any one of the lists of points contained in the Metric. +// +// - Metric is composed of a metadata and data. +// - Metadata part contains a name, description, unit. +// - Data is one of the possible types (Sum, Gauge, Histogram, Summary). +// - DataPoint contains timestamps, attributes, and one of the possible value type +// fields. +// +// Metric +// +------------+ +// |name | +// |description | +// |unit | +------------------------------------+ +// |data |---> |Gauge, Sum, Histogram, Summary, ... | +// +------------+ +------------------------------------+ +// +// Data [One of Gauge, Sum, Histogram, Summary, ...] +// +-----------+ +// |... | // Metadata about the Data. +// |points |--+ +// +-----------+ | +// | +---------------------------+ +// | |DataPoint 1 | +// v |+------+------+ +------+ | +// +-----+ ||label |label |...|label | | +// | 1 |-->||value1|value2|...|valueN| | +// +-----+ |+------+------+ +------+ | +// | . | |+-----+ | +// | . | ||value| | +// | . | |+-----+ | +// | . | +---------------------------+ +// | . | . +// | . | . +// | . | . +// | . | +---------------------------+ +// | . | |DataPoint M | +// +-----+ |+------+------+ +------+ | +// | M |-->||label |label |...|label | | +// +-----+ ||value1|value2|...|valueN| | +// |+------+------+ +------+ | +// |+-----+ | +// ||value| | +// |+-----+ | +// +---------------------------+ +// +// Each distinct type of DataPoint represents the output of a specific +// aggregation function, the result of applying the DataPoint's +// associated function of to one or more measurements. +// +// All DataPoint types have three common fields: +// - Attributes includes key-value pairs associated with the data point +// - TimeUnixNano is required, set to the end time of the aggregation +// - StartTimeUnixNano is optional, but strongly encouraged for DataPoints +// having an AggregationTemporality field, as discussed below. +// +// Both TimeUnixNano and StartTimeUnixNano values are expressed as +// UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. +// +// # TimeUnixNano +// +// This field is required, having consistent interpretation across +// DataPoint types. TimeUnixNano is the moment corresponding to when +// the data point's aggregate value was captured. +// +// Data points with the 0 value for TimeUnixNano SHOULD be rejected +// by consumers. +// +// # StartTimeUnixNano +// +// StartTimeUnixNano in general allows detecting when a sequence of +// observations is unbroken. This field indicates to consumers the +// start time for points with cumulative and delta +// AggregationTemporality, and it should be included whenever possible +// to support correct rate calculation. Although it may be omitted +// when the start time is truly unknown, setting StartTimeUnixNano is +// strongly encouraged. +message Metric { + reserved 4, 6, 8; + + // name of the metric, including its DNS name prefix. It must be unique. + string name = 1; + + // description of the metric, which can be used in documentation. + string description = 2; + + // unit in which the metric value is reported. Follows the format + // described by http://unitsofmeasure.org/ucum.html. + string unit = 3; + + // Data determines the aggregation type (if any) of the metric, what is the + // reported value type for the data points, as well as the relatationship to + // the time interval over which they are reported. + oneof data { + Gauge gauge = 5; + Sum sum = 7; + Histogram histogram = 9; + ExponentialHistogram exponential_histogram = 10; + Summary summary = 11; + } +} + +// Gauge represents the type of a scalar metric that always exports the +// "current value" for every data point. It should be used for an "unknown" +// aggregation. +// +// A Gauge does not support different aggregation temporalities. Given the +// aggregation is unknown, points cannot be combined using the same +// aggregation, regardless of aggregation temporalities. Therefore, +// AggregationTemporality is not included. Consequently, this also means +// "StartTimeUnixNano" is ignored for all data points. +message Gauge { + repeated NumberDataPoint data_points = 1; +} + +// Sum represents the type of a scalar metric that is calculated as a sum of all +// reported measurements over a time interval. +message Sum { + repeated NumberDataPoint data_points = 1; + + // aggregation_temporality describes if the aggregator reports delta changes + // since last report time, or cumulative changes since a fixed start time. + AggregationTemporality aggregation_temporality = 2; + + // If "true" means that the sum is monotonic. + bool is_monotonic = 3; +} + +// Histogram represents the type of a metric that is calculated by aggregating +// as a Histogram of all reported measurements over a time interval. +message Histogram { + repeated HistogramDataPoint data_points = 1; + + // aggregation_temporality describes if the aggregator reports delta changes + // since last report time, or cumulative changes since a fixed start time. + AggregationTemporality aggregation_temporality = 2; +} + +// ExponentialHistogram represents the type of a metric that is calculated by aggregating +// as a ExponentialHistogram of all reported double measurements over a time interval. +message ExponentialHistogram { + repeated ExponentialHistogramDataPoint data_points = 1; + + // aggregation_temporality describes if the aggregator reports delta changes + // since last report time, or cumulative changes since a fixed start time. + AggregationTemporality aggregation_temporality = 2; +} + +// Summary metric data are used to convey quantile summaries, +// a Prometheus (see: https://prometheus.io/docs/concepts/metric_types/#summary) +// and OpenMetrics (see: https://github.com/OpenObservability/OpenMetrics/blob/4dbf6075567ab43296eed941037c12951faafb92/protos/prometheus.proto#L45) +// data type. These data points cannot always be merged in a meaningful way. +// While they can be useful in some applications, histogram data points are +// recommended for new applications. +message Summary { + repeated SummaryDataPoint data_points = 1; +} + +// AggregationTemporality defines how a metric aggregator reports aggregated +// values. It describes how those values relate to the time interval over +// which they are aggregated. +enum AggregationTemporality { + // UNSPECIFIED is the default AggregationTemporality, it MUST not be used. + AGGREGATION_TEMPORALITY_UNSPECIFIED = 0; + + // DELTA is an AggregationTemporality for a metric aggregator which reports + // changes since last report time. Successive metrics contain aggregation of + // values from continuous and non-overlapping intervals. + // + // The values for a DELTA metric are based only on the time interval + // associated with one measurement cycle. There is no dependency on + // previous measurements like is the case for CUMULATIVE metrics. + // + // For example, consider a system measuring the number of requests that + // it receives and reports the sum of these requests every second as a + // DELTA metric: + // + // 1. The system starts receiving at time=t_0. + // 2. A request is received, the system measures 1 request. + // 3. A request is received, the system measures 1 request. + // 4. A request is received, the system measures 1 request. + // 5. The 1 second collection cycle ends. A metric is exported for the + // number of requests received over the interval of time t_0 to + // t_0+1 with a value of 3. + // 6. A request is received, the system measures 1 request. + // 7. A request is received, the system measures 1 request. + // 8. The 1 second collection cycle ends. A metric is exported for the + // number of requests received over the interval of time t_0+1 to + // t_0+2 with a value of 2. + AGGREGATION_TEMPORALITY_DELTA = 1; + + // CUMULATIVE is an AggregationTemporality for a metric aggregator which + // reports changes since a fixed start time. This means that current values + // of a CUMULATIVE metric depend on all previous measurements since the + // start time. Because of this, the sender is required to retain this state + // in some form. If this state is lost or invalidated, the CUMULATIVE metric + // values MUST be reset and a new fixed start time following the last + // reported measurement time sent MUST be used. + // + // For example, consider a system measuring the number of requests that + // it receives and reports the sum of these requests every second as a + // CUMULATIVE metric: + // + // 1. The system starts receiving at time=t_0. + // 2. A request is received, the system measures 1 request. + // 3. A request is received, the system measures 1 request. + // 4. A request is received, the system measures 1 request. + // 5. The 1 second collection cycle ends. A metric is exported for the + // number of requests received over the interval of time t_0 to + // t_0+1 with a value of 3. + // 6. A request is received, the system measures 1 request. + // 7. A request is received, the system measures 1 request. + // 8. The 1 second collection cycle ends. A metric is exported for the + // number of requests received over the interval of time t_0 to + // t_0+2 with a value of 5. + // 9. The system experiences a fault and loses state. + // 10. The system recovers and resumes receiving at time=t_1. + // 11. A request is received, the system measures 1 request. + // 12. The 1 second collection cycle ends. A metric is exported for the + // number of requests received over the interval of time t_1 to + // t_0+1 with a value of 1. + // + // Note: Even though, when reporting changes since last report time, using + // CUMULATIVE is valid, it is not recommended. This may cause problems for + // systems that do not use start_time to determine when the aggregation + // value was reset (e.g. Prometheus). + AGGREGATION_TEMPORALITY_CUMULATIVE = 2; +} + +// DataPointFlags is defined as a protobuf 'uint32' type and is to be used as a +// bit-field representing 32 distinct boolean flags. Each flag defined in this +// enum is a bit-mask. To test the presence of a single flag in the flags of +// a data point, for example, use an expression like: +// +// (point.flags & DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK) == DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK +// +enum DataPointFlags { + // The zero value for the enum. Should not be used for comparisons. + // Instead use bitwise "and" with the appropriate mask as shown above. + DATA_POINT_FLAGS_DO_NOT_USE = 0; + + // This DataPoint is valid but has no recorded value. This value + // SHOULD be used to reflect explicitly missing data in a series, as + // for an equivalent to the Prometheus "staleness marker". + DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK = 1; + + // Bits 2-31 are reserved for future use. +} + +// NumberDataPoint is a single data point in a timeseries that describes the +// time-varying scalar value of a metric. +message NumberDataPoint { + reserved 1; + + // The set of key/value pairs that uniquely identify the timeseries from + // where this point belongs. The list may be empty (may contain 0 elements). + // Attribute keys MUST be unique (it is not allowed to have more than one + // attribute with the same key). + repeated opentelemetry.proto.common.v1.KeyValue attributes = 7; + + // StartTimeUnixNano is optional but strongly encouraged, see the + // the detailed comments above Metric. + // + // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + // 1970. + fixed64 start_time_unix_nano = 2; + + // TimeUnixNano is required, see the detailed comments above Metric. + // + // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + // 1970. + fixed64 time_unix_nano = 3; + + // The value itself. A point is considered invalid when one of the recognized + // value fields is not present inside this oneof. + oneof value { + double as_double = 4; + sfixed64 as_int = 6; + } + + // (Optional) List of exemplars collected from + // measurements that were used to form the data point + repeated Exemplar exemplars = 5; + + // Flags that apply to this specific data point. See DataPointFlags + // for the available flags and their meaning. + uint32 flags = 8; +} + +// HistogramDataPoint is a single data point in a timeseries that describes the +// time-varying values of a Histogram. A Histogram contains summary statistics +// for a population of values, it may optionally contain the distribution of +// those values across a set of buckets. +// +// If the histogram contains the distribution of values, then both +// "explicit_bounds" and "bucket counts" fields must be defined. +// If the histogram does not contain the distribution of values, then both +// "explicit_bounds" and "bucket_counts" must be omitted and only "count" and +// "sum" are known. +message HistogramDataPoint { + reserved 1; + + // The set of key/value pairs that uniquely identify the timeseries from + // where this point belongs. The list may be empty (may contain 0 elements). + // Attribute keys MUST be unique (it is not allowed to have more than one + // attribute with the same key). + repeated opentelemetry.proto.common.v1.KeyValue attributes = 9; + + // StartTimeUnixNano is optional but strongly encouraged, see the + // the detailed comments above Metric. + // + // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + // 1970. + fixed64 start_time_unix_nano = 2; + + // TimeUnixNano is required, see the detailed comments above Metric. + // + // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + // 1970. + fixed64 time_unix_nano = 3; + + // count is the number of values in the population. Must be non-negative. This + // value must be equal to the sum of the "count" fields in buckets if a + // histogram is provided. + fixed64 count = 4; + + // sum of the values in the population. If count is zero then this field + // must be zero. + // + // Note: Sum should only be filled out when measuring non-negative discrete + // events, and is assumed to be monotonic over the values of these events. + // Negative events *can* be recorded, but sum should not be filled out when + // doing so. This is specifically to enforce compatibility w/ OpenMetrics, + // see: https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#histogram + optional double sum = 5; + + // bucket_counts is an optional field contains the count values of histogram + // for each bucket. + // + // The sum of the bucket_counts must equal the value in the count field. + // + // The number of elements in bucket_counts array must be by one greater than + // the number of elements in explicit_bounds array. + repeated fixed64 bucket_counts = 6; + + // explicit_bounds specifies buckets with explicitly defined bounds for values. + // + // The boundaries for bucket at index i are: + // + // (-infinity, explicit_bounds[i]] for i == 0 + // (explicit_bounds[i-1], explicit_bounds[i]] for 0 < i < size(explicit_bounds) + // (explicit_bounds[i-1], +infinity) for i == size(explicit_bounds) + // + // The values in the explicit_bounds array must be strictly increasing. + // + // Histogram buckets are inclusive of their upper boundary, except the last + // bucket where the boundary is at infinity. This format is intentionally + // compatible with the OpenMetrics histogram definition. + repeated double explicit_bounds = 7; + + // (Optional) List of exemplars collected from + // measurements that were used to form the data point + repeated Exemplar exemplars = 8; + + // Flags that apply to this specific data point. See DataPointFlags + // for the available flags and their meaning. + uint32 flags = 10; + + // min is the minimum value over (start_time, end_time]. + optional double min = 11; + + // max is the maximum value over (start_time, end_time]. + optional double max = 12; +} + +// ExponentialHistogramDataPoint is a single data point in a timeseries that describes the +// time-varying values of a ExponentialHistogram of double values. A ExponentialHistogram contains +// summary statistics for a population of values, it may optionally contain the +// distribution of those values across a set of buckets. +// +message ExponentialHistogramDataPoint { + // The set of key/value pairs that uniquely identify the timeseries from + // where this point belongs. The list may be empty (may contain 0 elements). + // Attribute keys MUST be unique (it is not allowed to have more than one + // attribute with the same key). + repeated opentelemetry.proto.common.v1.KeyValue attributes = 1; + + // StartTimeUnixNano is optional but strongly encouraged, see the + // the detailed comments above Metric. + // + // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + // 1970. + fixed64 start_time_unix_nano = 2; + + // TimeUnixNano is required, see the detailed comments above Metric. + // + // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + // 1970. + fixed64 time_unix_nano = 3; + + // count is the number of values in the population. Must be + // non-negative. This value must be equal to the sum of the "bucket_counts" + // values in the positive and negative Buckets plus the "zero_count" field. + fixed64 count = 4; + + // sum of the values in the population. If count is zero then this field + // must be zero. + // + // Note: Sum should only be filled out when measuring non-negative discrete + // events, and is assumed to be monotonic over the values of these events. + // Negative events *can* be recorded, but sum should not be filled out when + // doing so. This is specifically to enforce compatibility w/ OpenMetrics, + // see: https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#histogram + optional double sum = 5; + + // scale describes the resolution of the histogram. Boundaries are + // located at powers of the base, where: + // + // base = (2^(2^-scale)) + // + // The histogram bucket identified by `index`, a signed integer, + // contains values that are greater than (base^index) and + // less than or equal to (base^(index+1)). + // + // The positive and negative ranges of the histogram are expressed + // separately. Negative values are mapped by their absolute value + // into the negative range using the same scale as the positive range. + // + // scale is not restricted by the protocol, as the permissible + // values depend on the range of the data. + sint32 scale = 6; + + // zero_count is the count of values that are either exactly zero or + // within the region considered zero by the instrumentation at the + // tolerated degree of precision. This bucket stores values that + // cannot be expressed using the standard exponential formula as + // well as values that have been rounded to zero. + // + // Implementations MAY consider the zero bucket to have probability + // mass equal to (zero_count / count). + fixed64 zero_count = 7; + + // positive carries the positive range of exponential bucket counts. + Buckets positive = 8; + + // negative carries the negative range of exponential bucket counts. + Buckets negative = 9; + + // Buckets are a set of bucket counts, encoded in a contiguous array + // of counts. + message Buckets { + // Offset is the bucket index of the first entry in the bucket_counts array. + // + // Note: This uses a varint encoding as a simple form of compression. + sint32 offset = 1; + + // bucket_counts is an array of count values, where bucket_counts[i] carries + // the count of the bucket at index (offset+i). bucket_counts[i] is the count + // of values greater than base^(offset+i) and less than or equal to + // base^(offset+i+1). + // + // Note: By contrast, the explicit HistogramDataPoint uses + // fixed64. This field is expected to have many buckets, + // especially zeros, so uint64 has been selected to ensure + // varint encoding. + repeated uint64 bucket_counts = 2; + } + + // Flags that apply to this specific data point. See DataPointFlags + // for the available flags and their meaning. + uint32 flags = 10; + + // (Optional) List of exemplars collected from + // measurements that were used to form the data point + repeated Exemplar exemplars = 11; + + // min is the minimum value over (start_time, end_time]. + optional double min = 12; + + // max is the maximum value over (start_time, end_time]. + optional double max = 13; + + // ZeroThreshold may be optionally set to convey the width of the zero + // region. Where the zero region is defined as the closed interval + // [-ZeroThreshold, ZeroThreshold]. + // When ZeroThreshold is 0, zero count bucket stores values that cannot be + // expressed using the standard exponential formula as well as values that + // have been rounded to zero. + double zero_threshold = 14; +} + +// SummaryDataPoint is a single data point in a timeseries that describes the +// time-varying values of a Summary metric. +message SummaryDataPoint { + reserved 1; + + // The set of key/value pairs that uniquely identify the timeseries from + // where this point belongs. The list may be empty (may contain 0 elements). + // Attribute keys MUST be unique (it is not allowed to have more than one + // attribute with the same key). + repeated opentelemetry.proto.common.v1.KeyValue attributes = 7; + + // StartTimeUnixNano is optional but strongly encouraged, see the + // the detailed comments above Metric. + // + // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + // 1970. + fixed64 start_time_unix_nano = 2; + + // TimeUnixNano is required, see the detailed comments above Metric. + // + // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + // 1970. + fixed64 time_unix_nano = 3; + + // count is the number of values in the population. Must be non-negative. + fixed64 count = 4; + + // sum of the values in the population. If count is zero then this field + // must be zero. + // + // Note: Sum should only be filled out when measuring non-negative discrete + // events, and is assumed to be monotonic over the values of these events. + // Negative events *can* be recorded, but sum should not be filled out when + // doing so. This is specifically to enforce compatibility w/ OpenMetrics, + // see: https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#summary + double sum = 5; + + // Represents the value at a given quantile of a distribution. + // + // To record Min and Max values following conventions are used: + // - The 1.0 quantile is equivalent to the maximum value observed. + // - The 0.0 quantile is equivalent to the minimum value observed. + // + // See the following issue for more context: + // https://github.com/open-telemetry/opentelemetry-proto/issues/125 + message ValueAtQuantile { + // The quantile of a distribution. Must be in the interval + // [0.0, 1.0]. + double quantile = 1; + + // The value at the given quantile of a distribution. + // + // Quantile values must NOT be negative. + double value = 2; + } + + // (Optional) list of values at different quantiles of the distribution calculated + // from the current snapshot. The quantiles must be strictly increasing. + repeated ValueAtQuantile quantile_values = 6; + + // Flags that apply to this specific data point. See DataPointFlags + // for the available flags and their meaning. + uint32 flags = 8; +} + +// A representation of an exemplar, which is a sample input measurement. +// Exemplars also hold information about the environment when the measurement +// was recorded, for example the span and trace ID of the active span when the +// exemplar was recorded. +message Exemplar { + reserved 1; + + // The set of key/value pairs that were filtered out by the aggregator, but + // recorded alongside the original measurement. Only key/value pairs that were + // filtered out by the aggregator should be included + repeated opentelemetry.proto.common.v1.KeyValue filtered_attributes = 7; + + // time_unix_nano is the exact time when this exemplar was recorded + // + // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + // 1970. + fixed64 time_unix_nano = 2; + + // The value of the measurement that was recorded. An exemplar is + // considered invalid when one of the recognized value fields is not present + // inside this oneof. + oneof value { + double as_double = 3; + sfixed64 as_int = 6; + } + + // (Optional) Span ID of the exemplar trace. + // span_id may be missing if the measurement is not recorded inside a trace + // or if the trace is not sampled. + bytes span_id = 4; + + // (Optional) Trace ID of the exemplar trace. + // trace_id may be missing if the measurement is not recorded inside a trace + // or if the trace is not sampled. + bytes trace_id = 5; +} diff --git a/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/resource/v1/resource.proto b/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/resource/v1/resource.proto new file mode 100644 index 00000000000..6637560bc35 --- /dev/null +++ b/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/resource/v1/resource.proto @@ -0,0 +1,37 @@ +// Copyright 2019, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package opentelemetry.proto.resource.v1; + +import "opentelemetry/proto/common/v1/common.proto"; + +option csharp_namespace = "OpenTelemetry.Proto.Resource.V1"; +option java_multiple_files = true; +option java_package = "io.opentelemetry.proto.resource.v1"; +option java_outer_classname = "ResourceProto"; +option go_package = "go.opentelemetry.io/proto/otlp/resource/v1"; + +// Resource information. +message Resource { + // Set of attributes that describe the resource. + // Attribute keys MUST be unique (it is not allowed to have more than one + // attribute with the same key). + repeated opentelemetry.proto.common.v1.KeyValue attributes = 1; + + // dropped_attributes_count is the number of dropped attributes. If the value is 0, then + // no attributes were dropped. + uint32 dropped_attributes_count = 2; +} diff --git a/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/trace/v1/trace.proto b/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/trace/v1/trace.proto new file mode 100644 index 00000000000..b2869edc421 --- /dev/null +++ b/playground/Stress/Stress.TelemetryService/Otlp/opentelemetry/proto/trace/v1/trace.proto @@ -0,0 +1,276 @@ +// Copyright 2019, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package opentelemetry.proto.trace.v1; + +import "opentelemetry/proto/common/v1/common.proto"; +import "opentelemetry/proto/resource/v1/resource.proto"; + +option csharp_namespace = "OpenTelemetry.Proto.Trace.V1"; +option java_multiple_files = true; +option java_package = "io.opentelemetry.proto.trace.v1"; +option java_outer_classname = "TraceProto"; +option go_package = "go.opentelemetry.io/proto/otlp/trace/v1"; + +// TracesData represents the traces data that can be stored in a persistent storage, +// OR can be embedded by other protocols that transfer OTLP traces data but do +// not implement the OTLP protocol. +// +// The main difference between this message and collector protocol is that +// in this message there will not be any "control" or "metadata" specific to +// OTLP protocol. +// +// When new fields are added into this message, the OTLP request MUST be updated +// as well. +message TracesData { + // An array of ResourceSpans. + // For data coming from a single resource this array will typically contain + // one element. Intermediary nodes that receive data from multiple origins + // typically batch the data before forwarding further and in that case this + // array will contain multiple elements. + repeated ResourceSpans resource_spans = 1; +} + +// A collection of ScopeSpans from a Resource. +message ResourceSpans { + reserved 1000; + + // The resource for the spans in this message. + // If this field is not set then no resource info is known. + opentelemetry.proto.resource.v1.Resource resource = 1; + + // A list of ScopeSpans that originate from a resource. + repeated ScopeSpans scope_spans = 2; + + // This schema_url applies to the data in the "resource" field. It does not apply + // to the data in the "scope_spans" field which have their own schema_url field. + string schema_url = 3; +} + +// A collection of Spans produced by an InstrumentationScope. +message ScopeSpans { + // The instrumentation scope information for the spans in this message. + // Semantically when InstrumentationScope isn't set, it is equivalent with + // an empty instrumentation scope name (unknown). + opentelemetry.proto.common.v1.InstrumentationScope scope = 1; + + // A list of Spans that originate from an instrumentation scope. + repeated Span spans = 2; + + // This schema_url applies to all spans and span events in the "spans" field. + string schema_url = 3; +} + +// A Span represents a single operation performed by a single component of the system. +// +// The next available field id is 17. +message Span { + // A unique identifier for a trace. All spans from the same trace share + // the same `trace_id`. The ID is a 16-byte array. An ID with all zeroes OR + // of length other than 16 bytes is considered invalid (empty string in OTLP/JSON + // is zero-length and thus is also invalid). + // + // This field is required. + bytes trace_id = 1; + + // A unique identifier for a span within a trace, assigned when the span + // is created. The ID is an 8-byte array. An ID with all zeroes OR of length + // other than 8 bytes is considered invalid (empty string in OTLP/JSON + // is zero-length and thus is also invalid). + // + // This field is required. + bytes span_id = 2; + + // trace_state conveys information about request position in multiple distributed tracing graphs. + // It is a trace_state in w3c-trace-context format: https://www.w3.org/TR/trace-context/#tracestate-header + // See also https://github.com/w3c/distributed-tracing for more details about this field. + string trace_state = 3; + + // The `span_id` of this span's parent span. If this is a root span, then this + // field must be empty. The ID is an 8-byte array. + bytes parent_span_id = 4; + + // A description of the span's operation. + // + // For example, the name can be a qualified method name or a file name + // and a line number where the operation is called. A best practice is to use + // the same display name at the same call point in an application. + // This makes it easier to correlate spans in different traces. + // + // This field is semantically required to be set to non-empty string. + // Empty value is equivalent to an unknown span name. + // + // This field is required. + string name = 5; + + // SpanKind is the type of span. Can be used to specify additional relationships between spans + // in addition to a parent/child relationship. + enum SpanKind { + // Unspecified. Do NOT use as default. + // Implementations MAY assume SpanKind to be INTERNAL when receiving UNSPECIFIED. + SPAN_KIND_UNSPECIFIED = 0; + + // Indicates that the span represents an internal operation within an application, + // as opposed to an operation happening at the boundaries. Default value. + SPAN_KIND_INTERNAL = 1; + + // Indicates that the span covers server-side handling of an RPC or other + // remote network request. + SPAN_KIND_SERVER = 2; + + // Indicates that the span describes a request to some remote service. + SPAN_KIND_CLIENT = 3; + + // Indicates that the span describes a producer sending a message to a broker. + // Unlike CLIENT and SERVER, there is often no direct critical path latency relationship + // between producer and consumer spans. A PRODUCER span ends when the message was accepted + // by the broker while the logical processing of the message might span a much longer time. + SPAN_KIND_PRODUCER = 4; + + // Indicates that the span describes consumer receiving a message from a broker. + // Like the PRODUCER kind, there is often no direct critical path latency relationship + // between producer and consumer spans. + SPAN_KIND_CONSUMER = 5; + } + + // Distinguishes between spans generated in a particular context. For example, + // two spans with the same name may be distinguished using `CLIENT` (caller) + // and `SERVER` (callee) to identify queueing latency associated with the span. + SpanKind kind = 6; + + // start_time_unix_nano is the start time of the span. On the client side, this is the time + // kept by the local machine where the span execution starts. On the server side, this + // is the time when the server's application handler starts running. + // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. + // + // This field is semantically required and it is expected that end_time >= start_time. + fixed64 start_time_unix_nano = 7; + + // end_time_unix_nano is the end time of the span. On the client side, this is the time + // kept by the local machine where the span execution ends. On the server side, this + // is the time when the server application handler stops running. + // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. + // + // This field is semantically required and it is expected that end_time >= start_time. + fixed64 end_time_unix_nano = 8; + + // attributes is a collection of key/value pairs. Note, global attributes + // like server name can be set using the resource API. Examples of attributes: + // + // "/http/user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36" + // "/http/server_latency": 300 + // "example.com/myattribute": true + // "example.com/score": 10.239 + // + // The OpenTelemetry API specification further restricts the allowed value types: + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/common/README.md#attribute + // Attribute keys MUST be unique (it is not allowed to have more than one + // attribute with the same key). + repeated opentelemetry.proto.common.v1.KeyValue attributes = 9; + + // dropped_attributes_count is the number of attributes that were discarded. Attributes + // can be discarded because their keys are too long or because there are too many + // attributes. If this value is 0, then no attributes were dropped. + uint32 dropped_attributes_count = 10; + + // Event is a time-stamped annotation of the span, consisting of user-supplied + // text description and key-value pairs. + message Event { + // time_unix_nano is the time the event occurred. + fixed64 time_unix_nano = 1; + + // name of the event. + // This field is semantically required to be set to non-empty string. + string name = 2; + + // attributes is a collection of attribute key/value pairs on the event. + // Attribute keys MUST be unique (it is not allowed to have more than one + // attribute with the same key). + repeated opentelemetry.proto.common.v1.KeyValue attributes = 3; + + // dropped_attributes_count is the number of dropped attributes. If the value is 0, + // then no attributes were dropped. + uint32 dropped_attributes_count = 4; + } + + // events is a collection of Event items. + repeated Event events = 11; + + // dropped_events_count is the number of dropped events. If the value is 0, then no + // events were dropped. + uint32 dropped_events_count = 12; + + // A pointer from the current span to another span in the same trace or in a + // different trace. For example, this can be used in batching operations, + // where a single batch handler processes multiple requests from different + // traces or when the handler receives a request from a different project. + message Link { + // A unique identifier of a trace that this linked span is part of. The ID is a + // 16-byte array. + bytes trace_id = 1; + + // A unique identifier for the linked span. The ID is an 8-byte array. + bytes span_id = 2; + + // The trace_state associated with the link. + string trace_state = 3; + + // attributes is a collection of attribute key/value pairs on the link. + // Attribute keys MUST be unique (it is not allowed to have more than one + // attribute with the same key). + repeated opentelemetry.proto.common.v1.KeyValue attributes = 4; + + // dropped_attributes_count is the number of dropped attributes. If the value is 0, + // then no attributes were dropped. + uint32 dropped_attributes_count = 5; + } + + // links is a collection of Links, which are references from this span to a span + // in the same or different trace. + repeated Link links = 13; + + // dropped_links_count is the number of dropped links after the maximum size was + // enforced. If this value is 0, then no links were dropped. + uint32 dropped_links_count = 14; + + // An optional final status for this span. Semantically when Status isn't set, it means + // span's status code is unset, i.e. assume STATUS_CODE_UNSET (code = 0). + Status status = 15; +} + +// The Status type defines a logical error model that is suitable for different +// programming environments, including REST APIs and RPC APIs. +message Status { + reserved 1; + + // A developer-facing human readable error message. + string message = 2; + + // For the semantics of status codes see + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#set-status + enum StatusCode { + // The default status. + STATUS_CODE_UNSET = 0; + // The Span has been validated by an Application developer or Operator to + // have completed successfully. + STATUS_CODE_OK = 1; + // The Span contains an error. + STATUS_CODE_ERROR = 2; + }; + + // The status code. + StatusCode code = 3; +} diff --git a/playground/Stress/Stress.TelemetryService/Program.cs b/playground/Stress/Stress.TelemetryService/Program.cs new file mode 100644 index 00000000000..14811cf9f0b --- /dev/null +++ b/playground/Stress/Stress.TelemetryService/Program.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Stress.ApiService; + +var builder = Host.CreateApplicationBuilder(args); +builder.Services.AddHostedService(); + +builder.AddServiceDefaults(); + +var app = builder.Build(); + +app.Run(); diff --git a/playground/Stress/Stress.TelemetryService/Stress.TelemetryService.csproj b/playground/Stress/Stress.TelemetryService/Stress.TelemetryService.csproj new file mode 100644 index 00000000000..a2b4bf41c2e --- /dev/null +++ b/playground/Stress/Stress.TelemetryService/Stress.TelemetryService.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + + Otlp + + + + diff --git a/playground/Stress/Stress.TelemetryService/TelemetryStresser.cs b/playground/Stress/Stress.TelemetryService/TelemetryStresser.cs new file mode 100644 index 00000000000..ff02c97a1a5 --- /dev/null +++ b/playground/Stress/Stress.TelemetryService/TelemetryStresser.cs @@ -0,0 +1,119 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Grpc.Net.Client; +using OpenTelemetry.Proto.Collector.Metrics.V1; +using OpenTelemetry.Proto.Common.V1; +using OpenTelemetry.Proto.Metrics.V1; +using OpenTelemetry.Proto.Resource.V1; + +namespace Stress.ApiService; + +/// +/// Send OTLP directly to the dashboard instead of going via opentelemetry-dotnet SDK to send raw and unlimited data. +/// +public class TelemetryStresser(ILogger logger, IConfiguration config) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + var address = config["OTEL_EXPORTER_OTLP_ENDPOINT"]!; + var channel = GrpcChannel.ForAddress(address); + + var client = new MetricsService.MetricsServiceClient(channel); + + var value = 0; + while (!cancellationToken.IsCancellationRequested) + { + value += Random.Shared.Next(0, 10); + + var request = new ExportMetricsServiceRequest + { + ResourceMetrics = + { + new ResourceMetrics + { + Resource = CreateResource("TestResource", "TestResource"), + ScopeMetrics = + { + new ScopeMetrics + { + Scope = new InstrumentationScope + { + Name = "TestScope-Bold" + }, + Metrics = + { + CreateSumMetric("Test-Bold", DateTime.UtcNow, value: value) + } + } + } + } + } + }; + + logger.LogDebug("Exporting metrics"); + var response = await client.ExportAsync(request, cancellationToken: cancellationToken); + logger.LogDebug($"Export complete. Rejected count: {response.PartialSuccess?.RejectedDataPoints ?? 0}"); + + await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); + } + } + + public static Resource CreateResource(string? name = null, string? instanceId = null) + { + return new Resource() + { + Attributes = + { + new KeyValue { Key = "service.name", Value = new AnyValue { StringValue = name ?? "TestService" } }, + new KeyValue { Key = "service.instance.id", Value = new AnyValue { StringValue = instanceId ?? "TestId" } } + } + }; + } + + public static Metric CreateSumMetric(string metricName, DateTime startTime, KeyValuePair[]? attributes = null, int? value = null) + { + return new Metric + { + Name = metricName, + Description = "Description-Bold", + Unit = "Widget-Bold", + Sum = new Sum + { + AggregationTemporality = AggregationTemporality.Cumulative, + IsMonotonic = true, + DataPoints = + { + CreateNumberPoint(startTime, value ?? 1, attributes) + } + } + }; + } + + private static NumberDataPoint CreateNumberPoint(DateTime startTime, int value, KeyValuePair[]? attributes = null) + { + var point = new NumberDataPoint + { + AsInt = value, + StartTimeUnixNano = DateTimeToUnixNanoseconds(startTime), + TimeUnixNano = DateTimeToUnixNanoseconds(startTime) + }; + if (attributes != null) + { + foreach (var attribute in attributes) + { + point.Attributes.Add(new KeyValue { Key = attribute.Key, Value = new AnyValue { StringValue = attribute.Value } }); + } + } + + return point; + } + + public static ulong DateTimeToUnixNanoseconds(DateTime dateTime) + { + var unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + var timeSinceEpoch = dateTime.ToUniversalTime() - unixEpoch; + + return (ulong)timeSinceEpoch.Ticks * 100; + } +} From 4aca143485fdde29483df9e8cce6113135f2260c Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Fri, 8 Mar 2024 09:18:05 -0600 Subject: [PATCH 19/50] Rework password generation (#2717) - base it off InputAnnotation - add extra generation parameters to support various password and user name generation needs Contributes to #2633 --- .../Extensions/AzurePostgresExtensions.cs | 8 +- .../ApplicationModel/InputAnnotation.cs | 102 ++++++++++++- .../InputAnnotationExtensions.cs | 20 --- .../MySql/MySqlBuilderExtensions.cs | 8 +- .../MySql/MySqlServerResource.cs | 24 ++- .../Oracle/OracleDatabaseBuilderExtensions.cs | 8 +- .../Oracle/OracleDatabaseServerResource.cs | 24 ++- .../Postgres/PostgresBuilderExtensions.cs | 8 +- .../Postgres/PostgresServerResource.cs | 24 ++- .../RabbitMQ/RabbitMQBuilderExtensions.cs | 10 +- .../RabbitMQ/RabbitMQServerResource.cs | 27 ++-- .../SqlServer/SqlServerBuilderExtensions.cs | 9 +- .../SqlServer/SqlServerServerResource.cs | 25 +++- src/Aspire.Hosting/Utils/PasswordGenerator.cs | 102 +++++++++---- .../Azure/AzureBicepResourceTests.cs | 21 ++- .../ManifestGenerationTests.cs | 49 ++++++ .../MySql/AddMySqlTests.cs | 2 +- .../Oracle/AddOracleDatabaseTests.cs | 2 +- .../Postgres/AddPostgresTests.cs | 2 +- .../RabbitMQ/AddRabbitMQTests.cs | 3 +- .../SqlServer/AddSqlServerTests.cs | 5 +- .../Utils/PasswordGeneratorTests.cs | 139 ++++++++++-------- 22 files changed, 415 insertions(+), 207 deletions(-) delete mode 100644 src/Aspire.Hosting/ApplicationModel/InputAnnotationExtensions.cs diff --git a/src/Aspire.Hosting.Azure/Extensions/AzurePostgresExtensions.cs b/src/Aspire.Hosting.Azure/Extensions/AzurePostgresExtensions.cs index b3f7083977f..0026f58aaf9 100644 --- a/src/Aspire.Hosting.Azure/Extensions/AzurePostgresExtensions.cs +++ b/src/Aspire.Hosting.Azure/Extensions/AzurePostgresExtensions.cs @@ -97,7 +97,13 @@ private static IResourceBuilder WithLoginAndPassword( // generate a username since a parameter was not provided builder.WithAnnotation(new InputAnnotation(usernameInput) { - Default = new GenerateInputDefault { MinLength = 10 } + Default = new GenerateInputDefault + { + MinLength = 10, + // just use letters for the username since it can't start with a number + Numeric = false, + Special = false + } }); builder.WithParameter("administratorLogin", new InputReference(builder.Resource, usernameInput)); diff --git a/src/Aspire.Hosting/ApplicationModel/InputAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/InputAnnotation.cs index 330c7fdb7e6..063795c961c 100644 --- a/src/Aspire.Hosting/ApplicationModel/InputAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/InputAnnotation.cs @@ -75,6 +75,32 @@ private string GenerateValue() return Default.GenerateDefaultValue(); } + + internal static InputAnnotation CreateDefaultPasswordInput(string? password, + bool lower = true, bool upper = true, bool numeric = true, bool special = true, + int minLower = 0, int minUpper = 0, int minNumeric = 0, int minSpecial = 0) + { + var passwordInput = new InputAnnotation("password", secret: true); + passwordInput.Default = new GenerateInputDefault + { + MinLength = 22, // enough to give 128 bits of entropy + Lower = lower, + Upper = upper, + Numeric = numeric, + Special = special, + MinLower = minLower, + MinUpper = minUpper, + MinNumeric = minNumeric, + MinSpecial = minSpecial + }; + + if (password is not null) + { + passwordInput.SetValueGetter(() => password); + } + + return passwordInput; + } } /// @@ -101,22 +127,86 @@ public abstract class InputDefault public sealed class GenerateInputDefault : InputDefault { /// - /// The minimum length of the generated value. + /// Gets or sets the minimum length of the generated value. /// public int MinLength { get; set; } + /// + /// Gets or sets a value indicating whether to include lowercase alphabet characters in the result. + /// + public bool Lower { get; set; } = true; + + /// + /// Gets or sets a value indicating whether to include uppercase alphabet characters in the result. + /// + public bool Upper { get; set; } = true; + + /// + /// Gets or sets a value indicating whether to include numeric characters in the result. + /// + public bool Numeric { get; set; } = true; + + /// + /// Gets or sets a value indicating whether to include special characters in the result. + /// + public bool Special { get; set; } = true; + + /// + /// Gets or sets the minimum number of lowercase characters in the result. + /// + public int MinLower { get; set; } + + /// + /// Gets or sets the minimum number of uppercase characters in the result. + /// + public int MinUpper { get; set; } + + /// + /// Gets or sets the minimum number of numeric characters in the result. + /// + public int MinNumeric { get; set; } + + /// + /// Gets or sets the minimum number of special characters in the result. + /// + public int MinSpecial { get; set; } + /// public override void WriteToManifest(ManifestPublishingContext context) { context.Writer.WriteStartObject("generate"); context.Writer.WriteNumber("minLength", MinLength); + + static void WriteBoolIfNotTrue(ManifestPublishingContext context, string propertyName, bool value) + { + if (value != true) + { + context.Writer.WriteBoolean(propertyName, value); + } + } + + WriteBoolIfNotTrue(context, "lower", Lower); + WriteBoolIfNotTrue(context, "upper", Upper); + WriteBoolIfNotTrue(context, "numeric", Numeric); + WriteBoolIfNotTrue(context, "special", Special); + + static void WriteIntIfNotZero(ManifestPublishingContext context, string propertyName, int value) + { + if (value != 0) + { + context.Writer.WriteNumber(propertyName, value); + } + } + + WriteIntIfNotZero(context, "minLower", MinLower); + WriteIntIfNotZero(context, "minUpper", MinUpper); + WriteIntIfNotZero(context, "minNumeric", MinNumeric); + WriteIntIfNotZero(context, "minSpecial", MinSpecial); + context.Writer.WriteEndObject(); } /// - public override string GenerateDefaultValue() - { - // https://github.com/Azure/azure-dev/issues/3462 tracks adding more generation options - return PasswordGenerator.GenerateRandomLettersValue(MinLength); - } + public override string GenerateDefaultValue() => + PasswordGenerator.Generate(MinLength, Lower, Upper, Numeric, Special, MinLower, MinUpper, MinNumeric, MinSpecial); } diff --git a/src/Aspire.Hosting/ApplicationModel/InputAnnotationExtensions.cs b/src/Aspire.Hosting/ApplicationModel/InputAnnotationExtensions.cs deleted file mode 100644 index 624a339b1d9..00000000000 --- a/src/Aspire.Hosting/ApplicationModel/InputAnnotationExtensions.cs +++ /dev/null @@ -1,20 +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 Aspire.Hosting.ApplicationModel; - -/// -/// Provides extension methods for . -/// -internal static class InputAnnotationExtensions -{ - internal static IResourceBuilder WithDefaultPassword(this IResourceBuilder builder) where T : IResource - { - builder.WithAnnotation(new InputAnnotation("password", secret: true) - { - Default = new GenerateInputDefault { MinLength = 10 } - }); - - return builder; - } -} diff --git a/src/Aspire.Hosting/MySql/MySqlBuilderExtensions.cs b/src/Aspire.Hosting/MySql/MySqlBuilderExtensions.cs index 90d954a0b33..48b3d7c5dfb 100644 --- a/src/Aspire.Hosting/MySql/MySqlBuilderExtensions.cs +++ b/src/Aspire.Hosting/MySql/MySqlBuilderExtensions.cs @@ -4,7 +4,6 @@ using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Lifecycle; using Aspire.Hosting.MySql; -using Aspire.Hosting.Utils; namespace Aspire.Hosting; @@ -25,18 +24,13 @@ public static class MySqlBuilderExtensions /// A reference to the . public static IResourceBuilder AddMySql(this IDistributedApplicationBuilder builder, string name, int? port = null, string? password = null) { - password ??= PasswordGenerator.GeneratePassword(6, 6, 2, 2); - var resource = new MySqlServerResource(name, password); return builder.AddResource(resource) .WithEndpoint(hostPort: port, containerPort: 3306, name: MySqlServerResource.PrimaryEndpointName) // Internal port is always 3306. .WithAnnotation(new ContainerImageAnnotation { Image = "mysql", Tag = "8.3.0" }) - .WithDefaultPassword() .WithEnvironment(context => { - context.EnvironmentVariables[PasswordEnvVarName] = context.ExecutionContext.IsPublishMode - ? resource.PasswordInput - : resource.Password; + context.EnvironmentVariables[PasswordEnvVarName] = resource.PasswordInput; }) .PublishAsContainer(); } diff --git a/src/Aspire.Hosting/MySql/MySqlServerResource.cs b/src/Aspire.Hosting/MySql/MySqlServerResource.cs index e0b0141d723..972269d79f2 100644 --- a/src/Aspire.Hosting/MySql/MySqlServerResource.cs +++ b/src/Aspire.Hosting/MySql/MySqlServerResource.cs @@ -8,26 +8,34 @@ namespace Aspire.Hosting.ApplicationModel; /// /// A resource that represents a MySQL container. /// -/// The name of the resource. -/// The MySQL server root password. -public class MySqlServerResource(string name, string password) : ContainerResource(name), IResourceWithConnectionString +public class MySqlServerResource : ContainerResource, IResourceWithConnectionString { internal static string PrimaryEndpointName => "tcp"; - private EndpointReference? _primaryEndpoint; - private InputReference? _passwordInput; + /// + /// Initializes a new instance of the class. + /// + /// The name of the resource. + /// The MySQL server root password, or to generate a random password. + public MySqlServerResource(string name, string? password = null) : base(name) + { + PrimaryEndpoint = new(this, PrimaryEndpointName); + PasswordInput = new(this, "password"); + + Annotations.Add(InputAnnotation.CreateDefaultPasswordInput(password)); + } /// /// Gets the primary endpoint for the MySQL server. /// - public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName); + public EndpointReference PrimaryEndpoint { get; } - internal InputReference PasswordInput => _passwordInput ??= new(this, "password"); + internal InputReference PasswordInput { get; } /// /// Gets the MySQL server root password. /// - public string Password { get; } = password; + public string Password => PasswordInput.Input.Value ?? throw new InvalidOperationException("Password cannot be null."); /// /// Gets the connection string expression for the MySQL server. diff --git a/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs b/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs index e22a59bfebb..9d0a1b05492 100644 --- a/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs +++ b/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Hosting.ApplicationModel; -using Aspire.Hosting.Utils; namespace Aspire.Hosting; @@ -37,18 +36,13 @@ public static IResourceBuilder AddOracleDatabase(t /// A reference to the . public static IResourceBuilder AddOracle(this IDistributedApplicationBuilder builder, string name, int? port = null, string? password = null) { - password ??= PasswordGenerator.GeneratePassword(6, 6, 2, 2); - var oracleDatabaseServer = new OracleDatabaseServerResource(name, password); return builder.AddResource(oracleDatabaseServer) .WithEndpoint(hostPort: port, containerPort: 1521, name: OracleDatabaseServerResource.PrimaryEndpointName) .WithAnnotation(new ContainerImageAnnotation { Image = "database/free", Tag = "23.3.0.0", Registry = "container-registry.oracle.com" }) - .WithDefaultPassword() .WithEnvironment(context => { - context.EnvironmentVariables[PasswordEnvVarName] = context.ExecutionContext.IsPublishMode - ? oracleDatabaseServer.PasswordInput - : oracleDatabaseServer.Password; + context.EnvironmentVariables[PasswordEnvVarName] = oracleDatabaseServer.PasswordInput; }) .PublishAsContainer(); } diff --git a/src/Aspire.Hosting/Oracle/OracleDatabaseServerResource.cs b/src/Aspire.Hosting/Oracle/OracleDatabaseServerResource.cs index f52324622ef..49b298a70b3 100644 --- a/src/Aspire.Hosting/Oracle/OracleDatabaseServerResource.cs +++ b/src/Aspire.Hosting/Oracle/OracleDatabaseServerResource.cs @@ -8,26 +8,34 @@ namespace Aspire.Hosting.ApplicationModel; /// /// A resource that represents an Oracle Database container. /// -/// The name of the resource. -/// The Oracle Database server password. -public class OracleDatabaseServerResource(string name, string password) : ContainerResource(name), IResourceWithConnectionString +public class OracleDatabaseServerResource : ContainerResource, IResourceWithConnectionString { internal const string PrimaryEndpointName = "tcp"; - private EndpointReference? _primaryEndpoint; - private InputReference? _passwordInput; + /// + /// Initializes a new instance of the class. + /// + /// The name of the resource. + /// The Oracle Database server password, or to generate a random password. + public OracleDatabaseServerResource(string name, string? password = null) : base(name) + { + PrimaryEndpoint = new(this, PrimaryEndpointName); + PasswordInput = new(this, "password"); + + Annotations.Add(InputAnnotation.CreateDefaultPasswordInput(password)); + } /// /// Gets the primary endpoint for the Redis server. /// - public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName); + public EndpointReference PrimaryEndpoint { get; } - internal InputReference PasswordInput => _passwordInput ??= new(this, "password"); + internal InputReference PasswordInput { get; } /// /// Gets the Oracle Database server password. /// - public string Password { get; } = password; + public string Password => PasswordInput.Input.Value ?? throw new InvalidOperationException("Password cannot be null."); /// /// Gets the connection string expression for the Oracle Database server. diff --git a/src/Aspire.Hosting/Postgres/PostgresBuilderExtensions.cs b/src/Aspire.Hosting/Postgres/PostgresBuilderExtensions.cs index acc3e16a55d..f9e3c49de47 100644 --- a/src/Aspire.Hosting/Postgres/PostgresBuilderExtensions.cs +++ b/src/Aspire.Hosting/Postgres/PostgresBuilderExtensions.cs @@ -4,7 +4,6 @@ using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Lifecycle; using Aspire.Hosting.Postgres; -using Aspire.Hosting.Utils; namespace Aspire.Hosting; @@ -25,20 +24,15 @@ public static class PostgresBuilderExtensions /// A reference to the . public static IResourceBuilder AddPostgres(this IDistributedApplicationBuilder builder, string name, int? port = null, string? password = null) { - password ??= PasswordGenerator.GeneratePassword(6, 6, 2, 2); - var postgresServer = new PostgresServerResource(name, password); return builder.AddResource(postgresServer) .WithEndpoint(hostPort: port, containerPort: 5432, name: PostgresServerResource.PrimaryEndpointName) // Internal port is always 5432. .WithAnnotation(new ContainerImageAnnotation { Image = "postgres", Tag = "16.2" }) - .WithDefaultPassword() .WithEnvironment("POSTGRES_HOST_AUTH_METHOD", "scram-sha-256") .WithEnvironment("POSTGRES_INITDB_ARGS", "--auth-host=scram-sha-256 --auth-local=scram-sha-256") .WithEnvironment(context => { - context.EnvironmentVariables[PasswordEnvVarName] = context.ExecutionContext.IsPublishMode - ? postgresServer.PasswordInput - : postgresServer.Password; + context.EnvironmentVariables[PasswordEnvVarName] = postgresServer.PasswordInput; }) .PublishAsContainer(); } diff --git a/src/Aspire.Hosting/Postgres/PostgresServerResource.cs b/src/Aspire.Hosting/Postgres/PostgresServerResource.cs index 5e287ddf679..d31500d5ed0 100644 --- a/src/Aspire.Hosting/Postgres/PostgresServerResource.cs +++ b/src/Aspire.Hosting/Postgres/PostgresServerResource.cs @@ -8,26 +8,34 @@ namespace Aspire.Hosting.ApplicationModel; /// /// A resource that represents a PostgreSQL container. /// -/// The name of the resource. -/// The PostgreSQL server password. -public class PostgresServerResource(string name, string password) : ContainerResource(name), IResourceWithConnectionString +public class PostgresServerResource : ContainerResource, IResourceWithConnectionString { internal const string PrimaryEndpointName = "tcp"; - private EndpointReference? _primaryEndpoint; - private InputReference? _passwordInput; + /// + /// Initializes a new instance of the class. + /// + /// The name of the resource. + /// The PostgreSQL server password, or to generate a random password. + public PostgresServerResource(string name, string? password = null) : base(name) + { + PrimaryEndpoint = new(this, PrimaryEndpointName); + PasswordInput = new(this, "password"); + + Annotations.Add(InputAnnotation.CreateDefaultPasswordInput(password)); + } /// /// Gets the primary endpoint for the Redis server. /// - public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName); + public EndpointReference PrimaryEndpoint { get; } - internal InputReference PasswordInput => _passwordInput ??= new(this, "password"); + internal InputReference PasswordInput { get; } /// /// Gets the PostgreSQL server password. /// - public string Password { get; } = password; + public string Password => PasswordInput.Input.Value ?? throw new InvalidOperationException("Password cannot be null."); /// /// Gets the connection string expression for the PostgreSQL server for the manifest. diff --git a/src/Aspire.Hosting/RabbitMQ/RabbitMQBuilderExtensions.cs b/src/Aspire.Hosting/RabbitMQ/RabbitMQBuilderExtensions.cs index 3a6e49fc9bb..ab63d33f280 100644 --- a/src/Aspire.Hosting/RabbitMQ/RabbitMQBuilderExtensions.cs +++ b/src/Aspire.Hosting/RabbitMQ/RabbitMQBuilderExtensions.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Hosting.ApplicationModel; -using Aspire.Hosting.Utils; namespace Aspire.Hosting; @@ -20,19 +19,14 @@ public static class RabbitMQBuilderExtensions /// A reference to the . public static IResourceBuilder AddRabbitMQ(this IDistributedApplicationBuilder builder, string name, int? port = null) { - var password = PasswordGenerator.GeneratePassword(6, 6, 2, 2); - - var rabbitMq = new RabbitMQServerResource(name, password); + var rabbitMq = new RabbitMQServerResource(name); return builder.AddResource(rabbitMq) .WithEndpoint(hostPort: port, containerPort: 5672, name: RabbitMQServerResource.PrimaryEndpointName) .WithAnnotation(new ContainerImageAnnotation { Image = "rabbitmq", Tag = "3" }) - .WithDefaultPassword() .WithEnvironment("RABBITMQ_DEFAULT_USER", "guest") .WithEnvironment(context => { - context.EnvironmentVariables["RABBITMQ_DEFAULT_PASS"] = context.ExecutionContext.IsPublishMode - ? rabbitMq.PasswordInput - : rabbitMq.Password; + context.EnvironmentVariables["RABBITMQ_DEFAULT_PASS"] = rabbitMq.PasswordInput; }) .PublishAsContainer(); } diff --git a/src/Aspire.Hosting/RabbitMQ/RabbitMQServerResource.cs b/src/Aspire.Hosting/RabbitMQ/RabbitMQServerResource.cs index 6223d2599fd..1e14cea4ba1 100644 --- a/src/Aspire.Hosting/RabbitMQ/RabbitMQServerResource.cs +++ b/src/Aspire.Hosting/RabbitMQ/RabbitMQServerResource.cs @@ -6,26 +6,35 @@ namespace Aspire.Hosting.ApplicationModel; /// /// A resource that represents a RabbitMQ resource. /// -/// The name of the resource. -/// The RabbitMQ server password. -public class RabbitMQServerResource(string name, string password) : ContainerResource(name), IResourceWithConnectionString, IResourceWithEnvironment +public class RabbitMQServerResource : ContainerResource, IResourceWithConnectionString, IResourceWithEnvironment { internal const string PrimaryEndpointName = "tcp"; - private EndpointReference? _primaryEndpoint; - private InputReference? _passwordInput; + /// + /// Initializes a new instance of the class. + /// + /// The name of the resource. + /// The RabbitMQ server password, or to generate a random password. + public RabbitMQServerResource(string name, string? password = null) : base(name) + { + PrimaryEndpoint = new(this, PrimaryEndpointName); + PasswordInput = new(this, "password"); + + // don't use special characters in the password, since it goes into a URI + Annotations.Add(InputAnnotation.CreateDefaultPasswordInput(password, special: false)); + } /// /// Gets the primary endpoint for the Redis server. /// - public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName); + public EndpointReference PrimaryEndpoint { get; } - internal InputReference PasswordInput => _passwordInput ??= new(this, "password"); + internal InputReference PasswordInput { get; } /// - /// The RabbitMQ server password. + /// Gets the RabbitMQ server password. /// - public string Password { get; } = password; + public string Password => PasswordInput.Input.Value ?? throw new InvalidOperationException("Password cannot be null."); /// /// Gets the connection string expression for the RabbitMQ server for the manifest. diff --git a/src/Aspire.Hosting/SqlServer/SqlServerBuilderExtensions.cs b/src/Aspire.Hosting/SqlServer/SqlServerBuilderExtensions.cs index 8a348b8b7f8..f4c3effddf5 100644 --- a/src/Aspire.Hosting/SqlServer/SqlServerBuilderExtensions.cs +++ b/src/Aspire.Hosting/SqlServer/SqlServerBuilderExtensions.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Hosting.ApplicationModel; -using Aspire.Hosting.Utils; namespace Aspire.Hosting; @@ -21,21 +20,15 @@ public static class SqlServerBuilderExtensions /// A reference to the . public static IResourceBuilder AddSqlServer(this IDistributedApplicationBuilder builder, string name, string? password = null, int? port = null) { - // The password must be at least 8 characters long and contain characters from three of the following four sets: Uppercase letters, Lowercase letters, Base 10 digits, and Symbols - password ??= PasswordGenerator.GeneratePassword(6, 6, 2, 2); - var sqlServer = new SqlServerServerResource(name, password); return builder.AddResource(sqlServer) .WithEndpoint(hostPort: port, containerPort: 1433, name: SqlServerServerResource.PrimaryEndpointName) .WithAnnotation(new ContainerImageAnnotation { Registry = "mcr.microsoft.com", Image = "mssql/server", Tag = "2022-latest" }) - .WithDefaultPassword() .WithEnvironment("ACCEPT_EULA", "Y") .WithEnvironment(context => { - context.EnvironmentVariables["MSSQL_SA_PASSWORD"] = context.ExecutionContext.IsPublishMode - ? sqlServer.PasswordInput - : sqlServer.Password; + context.EnvironmentVariables["MSSQL_SA_PASSWORD"] = sqlServer.PasswordInput; }) .PublishAsContainer(); } diff --git a/src/Aspire.Hosting/SqlServer/SqlServerServerResource.cs b/src/Aspire.Hosting/SqlServer/SqlServerServerResource.cs index b04be877f15..ce3bf5c5c55 100644 --- a/src/Aspire.Hosting/SqlServer/SqlServerServerResource.cs +++ b/src/Aspire.Hosting/SqlServer/SqlServerServerResource.cs @@ -8,26 +8,35 @@ namespace Aspire.Hosting.ApplicationModel; /// /// A resource that represents a SQL Server container. /// -/// The name of the resource. -/// The SQL Sever password. -public class SqlServerServerResource(string name, string password) : ContainerResource(name), IResourceWithConnectionString +public class SqlServerServerResource : ContainerResource, IResourceWithConnectionString { internal const string PrimaryEndpointName = "tcp"; - private EndpointReference? _primaryEndpoint; - private InputReference? _passwordInput; + /// + /// Initializes a new instance of the class. + /// + /// The name of the resource. + /// The SQL Sever password, or to generate a random password. + public SqlServerServerResource(string name, string? password = null) : base(name) + { + PrimaryEndpoint = new(this, PrimaryEndpointName); + PasswordInput = new(this, "password"); + + // The password must be at least 8 characters long and contain characters from three of the following four sets: Uppercase letters, Lowercase letters, Base 10 digits, and Symbols + Annotations.Add(InputAnnotation.CreateDefaultPasswordInput(password, minLower: 1, minUpper: 1, minNumeric: 1)); + } /// /// Gets the primary endpoint for the Redis server. /// - public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName); + public EndpointReference PrimaryEndpoint { get; } - internal InputReference PasswordInput => _passwordInput ??= new(this, "password"); + internal InputReference PasswordInput { get; } /// /// Gets the password for the SQL Server container resource. /// - public string Password { get; } = password; + public string Password => PasswordInput.Input.Value ?? throw new InvalidOperationException("Password cannot be null."); /// /// Gets the connection string expression for the SQL Server for the manifest. diff --git a/src/Aspire.Hosting/Utils/PasswordGenerator.cs b/src/Aspire.Hosting/Utils/PasswordGenerator.cs index 11b75f0c329..7643a1ac6c0 100644 --- a/src/Aspire.Hosting/Utils/PasswordGenerator.cs +++ b/src/Aspire.Hosting/Utils/PasswordGenerator.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Runtime.CompilerServices; using System.Security.Cryptography; namespace Aspire.Hosting.Utils; @@ -14,55 +15,94 @@ internal static class PasswordGenerator internal const string LowerCaseChars = "abcdefghjkmnpqrstuvwxyz"; // exclude i,l,o internal const string UpperCaseChars = "ABCDEFGHJKMNPQRSTUVWXYZ"; // exclude I,L,O - internal const string LettersChars = LowerCaseChars + UpperCaseChars; - internal const string DigitChars = "0123456789"; + internal const string NumericChars = "0123456789"; internal const string SpecialChars = "-_.{}~()*+!"; // exclude &<>=;,`'^%$#@/:[] /// - /// Creates a cryptographically random password. + /// Creates a cryptographically random string. /// - /// The number of lowercase chars in the generated password or 0 to ignore this component. - /// The number of uppercase chars in the generated password or 0 to ignore this component. - /// The number of digits in the generated password or 0 to ignore this component. - /// The number of special chars in the generated password or 0 to ignore this component. - /// A cryptographically random password. - /// If lowerCase, upperCase, digit or special is negative or their sum is zero. - public static string GeneratePassword(int lowerCase, int upperCase, int digit, int special) + public static string Generate(int minLength, + bool lower, bool upper, bool numeric, bool special, + int minLower, int minUpper, int minNumeric, int minSpecial) { - ArgumentOutOfRangeException.ThrowIfNegative(lowerCase); - ArgumentOutOfRangeException.ThrowIfNegative(upperCase); - ArgumentOutOfRangeException.ThrowIfNegative(digit); - ArgumentOutOfRangeException.ThrowIfNegative(special); + ArgumentOutOfRangeException.ThrowIfNegative(minLength); + ArgumentOutOfRangeException.ThrowIfGreaterThan(minLength, 128); + ArgumentOutOfRangeException.ThrowIfNegative(minLower); + ArgumentOutOfRangeException.ThrowIfNegative(minUpper); + ArgumentOutOfRangeException.ThrowIfNegative(minNumeric); + ArgumentOutOfRangeException.ThrowIfNegative(minSpecial); + CheckMinZeroWhenDisabled(lower, minLower); + CheckMinZeroWhenDisabled(upper, minUpper); + CheckMinZeroWhenDisabled(numeric, minNumeric); + CheckMinZeroWhenDisabled(special, minSpecial); - var length = lowerCase + upperCase + digit + special; + var requiredMinLength = minLower + minUpper + minNumeric + minSpecial; + var length = Math.Max(minLength, requiredMinLength); - ArgumentOutOfRangeException.ThrowIfZero(length); + Span chars = stackalloc char[length]; - Debug.Assert(length <= 128, "password too long"); + // fill the required characters first + var currentChars = chars; + GenerateRequiredValues(ref currentChars, minLower, LowerCaseChars); + GenerateRequiredValues(ref currentChars, minUpper, UpperCaseChars); + GenerateRequiredValues(ref currentChars, minNumeric, NumericChars); + GenerateRequiredValues(ref currentChars, minSpecial, SpecialChars); - Span chars = stackalloc char[length]; + // fill the rest of the password with random characters from all the available choices + var choices = GetChoices(lower, upper, numeric, special); + RandomNumberGenerator.GetItems(choices, currentChars); - RandomNumberGenerator.GetItems(LowerCaseChars, chars.Slice(0, lowerCase)); - RandomNumberGenerator.GetItems(UpperCaseChars, chars.Slice(lowerCase, upperCase)); - RandomNumberGenerator.GetItems(DigitChars, chars.Slice(lowerCase + upperCase, digit)); - RandomNumberGenerator.GetItems(SpecialChars, chars.Slice(lowerCase + upperCase + digit, special)); RandomNumberGenerator.Shuffle(chars); return new string(chars); } - /// - /// Creates a random string of upper and lower case letters. - /// - public static string GenerateRandomLettersValue(int length) + private static void CheckMinZeroWhenDisabled( + bool enabled, + int minValue, + [CallerArgumentExpression(nameof(enabled))] string? enabledParamName = null, + [CallerArgumentExpression(nameof(minValue))] string? minValueParamName = null) { - ArgumentOutOfRangeException.ThrowIfNegative(length); - ArgumentOutOfRangeException.ThrowIfGreaterThan(length, 128); + if (!enabled && minValue > 0) + { + ThrowArgumentException(); + } - Span chars = stackalloc char[length]; + void ThrowArgumentException() => throw new ArgumentException($"'{minValueParamName}' must be 0 if '{enabledParamName}' is disabled."); + } - RandomNumberGenerator.GetItems(LettersChars, chars); + private static void GenerateRequiredValues(ref Span destination, int minValues, string choices) + { + Debug.Assert(destination.Length >= minValues); - return new string(chars); + if (minValues > 0) + { + RandomNumberGenerator.GetItems(choices, destination.Slice(0, minValues)); + destination = destination.Slice(minValues); + } } + + private static string GetChoices(bool lower, bool upper, bool numeric, bool special) => + (lower, upper, numeric, special) switch + { + (true, true, true, true) => LowerCaseChars + UpperCaseChars + NumericChars + SpecialChars, + (true, true, true, false) => LowerCaseChars + UpperCaseChars + NumericChars, + (true, true, false, true) => LowerCaseChars + UpperCaseChars + SpecialChars, + (true, true, false, false) => LowerCaseChars + UpperCaseChars, + + (true, false, true, true) => LowerCaseChars + NumericChars + SpecialChars, + (true, false, true, false) => LowerCaseChars + NumericChars, + (true, false, false, true) => LowerCaseChars + SpecialChars, + (true, false, false, false) => LowerCaseChars, + + (false, true, true, true) => UpperCaseChars + NumericChars + SpecialChars, + (false, true, true, false) => UpperCaseChars + NumericChars, + (false, true, false, true) => UpperCaseChars + SpecialChars, + (false, true, false, false) => UpperCaseChars, + + (false, false, true, true) => NumericChars + SpecialChars, + (false, false, true, false) => NumericChars, + (false, false, false, true) => SpecialChars, + (false, false, false, false) => throw new ArgumentException("At least one character type must be enabled.") + }; } diff --git a/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs b/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs index 3a89b0abba5..e3f2030fd8e 100644 --- a/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs +++ b/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs @@ -466,7 +466,10 @@ public async void AsAzureSqlDatabaseConstruct() "secret": true, "default": { "generate": { - "minLength": 10 + "minLength": 22, + "minLower": 1, + "minUpper": 1, + "minNumeric": 1 } } } @@ -588,7 +591,7 @@ public async Task PublishAsAzurePostgresFlexibleServer() "secret": true, "default": { "generate": { - "minLength": 10 + "minLength": 22 } } } @@ -625,7 +628,7 @@ public async Task PublishAsAzurePostgresFlexibleServerNoUserPassParams() "secret": true, "default": { "generate": { - "minLength": 10 + "minLength": 22 } } }, @@ -633,7 +636,9 @@ public async Task PublishAsAzurePostgresFlexibleServerNoUserPassParams() "type": "string", "default": { "generate": { - "minLength": 10 + "minLength": 10, + "numeric": false, + "special": false } } } @@ -666,7 +671,7 @@ public async Task PublishAsAzurePostgresFlexibleServerNoUserPassParams() "secret": true, "default": { "generate": { - "minLength": 10 + "minLength": 22 } } } @@ -697,7 +702,7 @@ public async Task PublishAsAzurePostgresFlexibleServerNoUserPassParams() "secret": true, "default": { "generate": { - "minLength": 10 + "minLength": 22 } } }, @@ -705,7 +710,9 @@ public async Task PublishAsAzurePostgresFlexibleServerNoUserPassParams() "type": "string", "default": { "generate": { - "minLength": 10 + "minLength": 10, + "numeric": false, + "special": false } } } diff --git a/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs b/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs index 744fd26000e..9f0845c0d84 100644 --- a/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs +++ b/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs @@ -527,6 +527,55 @@ public void MetadataPropertyCanEmitComplexObjects() Assert.Equal(DateTime.MinValue, nestedComplexValue.GetDateTime()); } + [Fact] + public async Task InputAnnotationDefaultValuesGenerateCorrectly() + { + var appBuilder = DistributedApplication.CreateBuilder(); + var container = appBuilder.AddContainer("container", "image"); + container.WithAnnotation(new InputAnnotation("fake") + { + Default = new GenerateInputDefault() + { + MinLength = 16, + Lower = false, + Upper = false, + Numeric = false, + Special = false, + MinLower = 1, + MinUpper = 2, + MinNumeric = 3, + MinSpecial = 4, + } + }); + + var expectedManifest = """ + { + "type": "container.v0", + "image": "image:latest", + "inputs": { + "fake": { + "type": "string", + "default": { + "generate": { + "minLength": 16, + "lower": false, + "upper": false, + "numeric": false, + "special": false, + "minLower": 1, + "minUpper": 2, + "minNumeric": 3, + "minSpecial": 4 + } + } + } + } + } + """; + + var manifest = await ManifestUtils.GetManifest(container.Resource); + Assert.Equal(expectedManifest, manifest.ToString()); + } private static TestProgram CreateTestProgramJsonDocumentManifestPublisher(bool includeNodeApp = false) { var program = TestProgram.Create(GetManifestArgs(), includeNodeApp: includeNodeApp); diff --git a/tests/Aspire.Hosting.Tests/MySql/AddMySqlTests.cs b/tests/Aspire.Hosting.Tests/MySql/AddMySqlTests.cs index 59e325b68b9..e74f2e177ad 100644 --- a/tests/Aspire.Hosting.Tests/MySql/AddMySqlTests.cs +++ b/tests/Aspire.Hosting.Tests/MySql/AddMySqlTests.cs @@ -176,7 +176,7 @@ public async Task VerifyManifest() "secret": true, "default": { "generate": { - "minLength": 10 + "minLength": 22 } } } diff --git a/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs b/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs index 2ceae4b96e2..5f18ff60037 100644 --- a/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs +++ b/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs @@ -216,7 +216,7 @@ public async Task VerifyManifest() "secret": true, "default": { "generate": { - "minLength": 10 + "minLength": 22 } } } diff --git a/tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs b/tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs index 1b590707767..7d4eb495c71 100644 --- a/tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs +++ b/tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs @@ -243,7 +243,7 @@ public async Task VerifyManifest() "secret": true, "default": { "generate": { - "minLength": 10 + "minLength": 22 } } } diff --git a/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs b/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs index 6b71f8d0eb2..3d31417ef38 100644 --- a/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs +++ b/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs @@ -99,7 +99,8 @@ public async Task VerifyManifest() "secret": true, "default": { "generate": { - "minLength": 10 + "minLength": 22, + "special": false } } } diff --git a/tests/Aspire.Hosting.Tests/SqlServer/AddSqlServerTests.cs b/tests/Aspire.Hosting.Tests/SqlServer/AddSqlServerTests.cs index 91a4052b413..990950bae0b 100644 --- a/tests/Aspire.Hosting.Tests/SqlServer/AddSqlServerTests.cs +++ b/tests/Aspire.Hosting.Tests/SqlServer/AddSqlServerTests.cs @@ -143,7 +143,10 @@ public async Task VerifyManifest() "secret": true, "default": { "generate": { - "minLength": 10 + "minLength": 22, + "minLower": 1, + "minUpper": 1, + "minNumeric": 1 } } } diff --git a/tests/Aspire.Hosting.Tests/Utils/PasswordGeneratorTests.cs b/tests/Aspire.Hosting.Tests/Utils/PasswordGeneratorTests.cs index 4286b246df5..861afe5c459 100644 --- a/tests/Aspire.Hosting.Tests/Utils/PasswordGeneratorTests.cs +++ b/tests/Aspire.Hosting.Tests/Utils/PasswordGeneratorTests.cs @@ -1,8 +1,8 @@ // 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 Xunit; +using static Aspire.Hosting.Utils.PasswordGenerator; namespace Aspire.Hosting.Tests.Utils; @@ -11,87 +11,108 @@ public class PasswordGeneratorTests [Fact] public void ThrowsArgumentOutOfRangeException() { - Assert.Throws(() => PasswordGenerator.GeneratePassword(-1, 0, 0, 0)); - Assert.Throws(() => PasswordGenerator.GeneratePassword(0, -1, 0, 0)); - Assert.Throws(() => PasswordGenerator.GeneratePassword(0, 0, -1, 0)); - Assert.Throws(() => PasswordGenerator.GeneratePassword(0, 0, 0, -1)); - Assert.Throws(() => PasswordGenerator.GeneratePassword(0, 0, 0, 0)); - } - - [Theory] - [InlineData(1)] - [InlineData(3)] - [InlineData(10)] - public void IncludesLowerCase(int length) - { - var password = PasswordGenerator.GeneratePassword(length, 0, 0, 0); + Assert.Throws(() => Generate(-1, true, true, true, true, 0, 0, 0, 0)); + Assert.Throws(() => Generate(129, true, true, true, true, 0, 0, 0, 0)); - Assert.Equal(length, password.Length); - Assert.True(password.All(PasswordGenerator.LowerCaseChars.Contains)); + Assert.Throws(() => Generate(10, true, true, true, true, -1, 0, 0, 0)); + Assert.Throws(() => Generate(10, true, true, true, true, 0, -1, 0, 0)); + Assert.Throws(() => Generate(10, true, true, true, true, 0, 0, -1, 0)); + Assert.Throws(() => Generate(10, true, true, true, true, 0, 0, 0, -1)); } - [Theory] - [InlineData(1)] - [InlineData(3)] - [InlineData(10)] - public void IncludesUpperCase(int length) + [Fact] + public void ThrowsArgumentException() { - var password = PasswordGenerator.GeneratePassword(0, length, 0, 0); + // can't have a minimum requirement when that type is disabled + Assert.Throws(() => Generate(10, false, true, true, true, 1, 0, 0, 0)); + Assert.Throws(() => Generate(10, true, false, true, true, 0, 1, 0, 0)); + Assert.Throws(() => Generate(10, true, true, false, true, 0, 0, 1, 0)); + Assert.Throws(() => Generate(10, true, true, true, false, 0, 0, 0, 1)); - Assert.Equal(length, password.Length); - Assert.True(password.All(PasswordGenerator.UpperCaseChars.Contains)); + Assert.Throws(() => Generate(10, false, false, false, false, 0, 0, 0, 0)); } [Theory] - [InlineData(1)] - [InlineData(3)] - [InlineData(10)] - public void IncludesDigit(int length) + [InlineData(true, true, true, true, LowerCaseChars + UpperCaseChars + NumericChars + SpecialChars, null)] + [InlineData(true, true, true, false, LowerCaseChars + UpperCaseChars + NumericChars, SpecialChars)] + [InlineData(true, true, false, true, LowerCaseChars + UpperCaseChars + SpecialChars, NumericChars)] + [InlineData(true, true, false, false, LowerCaseChars + UpperCaseChars, NumericChars + SpecialChars)] + [InlineData(true, false, true, true, LowerCaseChars + NumericChars + SpecialChars, UpperCaseChars)] + [InlineData(true, false, true, false, LowerCaseChars + NumericChars, UpperCaseChars + SpecialChars)] + [InlineData(true, false, false, true, LowerCaseChars + SpecialChars, UpperCaseChars + NumericChars)] + [InlineData(true, false, false, false, LowerCaseChars, UpperCaseChars + NumericChars + SpecialChars)] + [InlineData(false, true, true, true, UpperCaseChars + NumericChars + SpecialChars, LowerCaseChars)] + [InlineData(false, true, true, false, UpperCaseChars + NumericChars, LowerCaseChars + SpecialChars)] + [InlineData(false, true, false, true, UpperCaseChars + SpecialChars, LowerCaseChars + NumericChars)] + [InlineData(false, true, false, false, UpperCaseChars, LowerCaseChars + NumericChars + SpecialChars)] + [InlineData(false, false, true, true, NumericChars + SpecialChars, LowerCaseChars + UpperCaseChars)] + [InlineData(false, false, true, false, NumericChars, LowerCaseChars + UpperCaseChars + SpecialChars)] + [InlineData(false, false, false, true, SpecialChars, LowerCaseChars + UpperCaseChars + NumericChars)] + // NOTE: all false throws ArgumentException + public void TestGenerate(bool lower, bool upper, bool numeric, bool special, string includes, string? excludes) { - var password = PasswordGenerator.GeneratePassword(0, 0, length, 0); + var password = Generate(10, lower, upper, numeric, special, 0, 0, 0, 0); - Assert.Equal(length, password.Length); - Assert.True(password.All(PasswordGenerator.DigitChars.Contains)); - } + Assert.Equal(10, password.Length); + Assert.True(password.All(includes.Contains)); - [Theory] - [InlineData(1)] - [InlineData(3)] - [InlineData(10)] - public void IncludesSpecial(int length) - { - var password = PasswordGenerator.GeneratePassword(0, 0, 0, length); - - Assert.Equal(length, password.Length); - Assert.True(password.All(PasswordGenerator.SpecialChars.Contains)); + if (excludes is not null) + { + Assert.True(!password.Any(excludes.Contains)); + } } [Theory] - [InlineData(1)] - [InlineData(3)] - [InlineData(10)] - public void IncludesAll(int length) + [InlineData(1, 0, 0, 0)] + [InlineData(0, 1, 0, 0)] + [InlineData(0, 0, 1, 0)] + [InlineData(0, 0, 0, 1)] + [InlineData(0, 2, 1, 0)] + [InlineData(0, 0, 2, 3)] + [InlineData(1, 0, 2, 0)] + [InlineData(5, 1, 1, 1)] + public void TestGenerateMin(int minLower, int minUpper, int minNumeric, int minSpecial) { - var password = PasswordGenerator.GeneratePassword(length, length, length, length); - - Assert.Equal(length * 4, password.Length); - Assert.Equal(length, password.Count(PasswordGenerator.LowerCaseChars.Contains)); - Assert.Equal(length, password.Count(PasswordGenerator.UpperCaseChars.Contains)); - Assert.Equal(length, password.Count(PasswordGenerator.DigitChars.Contains)); - Assert.Equal(length, password.Count(PasswordGenerator.SpecialChars.Contains)); + var password = Generate(10, true, true, true, true, minLower, minUpper, minNumeric, minSpecial); + + Assert.Equal(10, password.Length); + + if (minLower > 0) + { + Assert.True(password.Count(LowerCaseChars.Contains) >= minLower); + } + if (minUpper > 0) + { + Assert.True(password.Count(UpperCaseChars.Contains) >= minUpper); + } + if (minNumeric > 0) + { + Assert.True(password.Count(NumericChars.Contains) >= minNumeric); + } + if (minSpecial > 0) + { + Assert.True(password.Count(SpecialChars.Contains) >= minSpecial); + } } [Theory] [InlineData(1)] - [InlineData(3)] - [InlineData(10)] - public void ValidUriCharacters(int length) + [InlineData(22)] + public void ValidUriCharacters(int minLength) { - var password = PasswordGenerator.GeneratePassword(length, length, length, 0); - password += PasswordGenerator.SpecialChars; + var password = Generate(minLength, true, true, true, false, 0, 0, 0, 0); + password += SpecialChars; Exception exception = Record.Exception(() => new Uri($"https://guest:{password}@localhost:12345")); Assert.True((exception is null), $"Password contains invalid chars: {password}"); } + + [Fact] + public void MinLengthLessThanSumMinTypes() + { + var password = Generate(7, true, true, true, true, 2, 2, 2, 2); + + Assert.Equal(8, password.Length); + } } From 2cd04efc763d476d45ae760977a125e4e15e1025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Ros?= Date: Fri, 8 Mar 2024 10:33:59 -0800 Subject: [PATCH 20/50] OpenAI accepts URIs as connection strings (#2715) Fixes #2652 --- .../AzureOpenAISettings.cs | 29 ++++++++++--------- .../AspireAzureAIOpenAIExtensionsTests.cs | 21 ++++++++++++++ 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/Components/Aspire.Azure.AI.OpenAI/AzureOpenAISettings.cs b/src/Components/Aspire.Azure.AI.OpenAI/AzureOpenAISettings.cs index bbb7de7c1d1..18a3ee50f8d 100644 --- a/src/Components/Aspire.Azure.AI.OpenAI/AzureOpenAISettings.cs +++ b/src/Components/Aspire.Azure.AI.OpenAI/AzureOpenAISettings.cs @@ -49,23 +49,26 @@ public sealed class AzureOpenAISettings : IConnectionStringSettings void IConnectionStringSettings.ParseConnectionString(string? connectionString) { - var connectionBuilder = new DbConnectionStringBuilder - { - ConnectionString = connectionString - }; - - if (connectionBuilder.ContainsKey(ConnectionStringEndpoint) && Uri.TryCreate(connectionBuilder[ConnectionStringEndpoint].ToString(), UriKind.Absolute, out var serviceUri)) - { - Endpoint = serviceUri; - } - else if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri)) + if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri)) { Endpoint = uri; } - - if (connectionBuilder.ContainsKey(ConnectionStringKey)) + else { - Key = connectionBuilder[ConnectionStringKey].ToString(); + var connectionBuilder = new DbConnectionStringBuilder + { + ConnectionString = connectionString + }; + + if (connectionBuilder.ContainsKey(ConnectionStringEndpoint) && Uri.TryCreate(connectionBuilder[ConnectionStringEndpoint].ToString(), UriKind.Absolute, out var serviceUri)) + { + Endpoint = serviceUri; + } + + if (connectionBuilder.ContainsKey(ConnectionStringKey)) + { + Key = connectionBuilder[ConnectionStringKey].ToString(); + } } } } diff --git a/tests/Aspire.Azure.AI.OpenAI.Tests/AspireAzureAIOpenAIExtensionsTests.cs b/tests/Aspire.Azure.AI.OpenAI.Tests/AspireAzureAIOpenAIExtensionsTests.cs index f192b499edb..b1e2f5f3c70 100644 --- a/tests/Aspire.Azure.AI.OpenAI.Tests/AspireAzureAIOpenAIExtensionsTests.cs +++ b/tests/Aspire.Azure.AI.OpenAI.Tests/AspireAzureAIOpenAIExtensionsTests.cs @@ -65,4 +65,25 @@ public void ConnectionStringCanBeSetInCode(bool useKeyed) Assert.NotNull(client); } + + [Theory] + [InlineData("https://yourservice.openai.azure.com/")] + [InlineData("http://domain:12345")] + [InlineData("Endpoint=http://domain.com:12345;Key=abc123")] + [InlineData("Endpoint=http://domain.com:12345")] + public void ReadsFromConnectionStringsFormats(string connectionString) + { + var builder = Host.CreateEmptyApplicationBuilder(null); + builder.Configuration.AddInMemoryCollection([ + new KeyValuePair("ConnectionStrings:openai", connectionString) + ]); + + builder.AddAzureOpenAIClient("openai"); + + var host = builder.Build(); + var client = host.Services.GetRequiredService(); + + Assert.NotNull(client); + } + } From a2a7c31d1e2874074eecd22c6d8af535452b7ad0 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Fri, 8 Mar 2024 17:03:31 -0800 Subject: [PATCH 21/50] Service Discovery: allow multiple schemes to be specified in request (#2719) * Service Discovery: allow multiple schemes to be specified. Add endpoint name to configuration path. * Remove superfluous WriteServiceDiscoveryEnvironmentVariables method * Rename AllocatedEndpointAnnotation to AllocatedEndpoint and make it a property on EndpointAnnotation * Remove unnecessary members * Add endpoint from launch profile * Specify launch profile in test projects --- .../TestShop/ApiGateway/appsettings.json | 2 +- playground/TestShop/AppHost/Program.cs | 2 +- .../TestShop/AppHost/aspire-manifest.json | 15 +- playground/TestShop/MyFrontend/Program.cs | 2 +- ...ointAnnotation.cs => AllocatedEndpoint.cs} | 34 +-- .../ApplicationModel/EndpointAnnotation.cs | 5 + .../ApplicationModel/EndpointReference.cs | 42 ++-- .../EndpointReferenceAnnotation.cs | 2 +- src/Aspire.Hosting/Dcp/ApplicationExecutor.cs | 11 +- .../Extensions/ResourceBuilderExtensions.cs | 49 ++-- .../Extensions/ResourceExtensions.cs | 14 +- .../Publishing/ManifestPublishingContext.cs | 36 --- .../aspire-servicedefaults/Extensions.cs | 7 + ...sions.ServiceDiscovery.Abstractions.csproj | 1 + .../UriEndPoint.cs | 30 +++ .../DnsServiceEndPointResolverProvider.cs | 14 +- .../DnsSrvServiceEndPointResolverProvider.cs | 15 +- ...oft.Extensions.ServiceDiscovery.Dns.csproj | 4 - ...viceDiscoveryForwarderHttpClientFactory.cs | 6 +- ...onfigurationServiceEndPointResolver.Log.cs | 13 +- .../ConfigurationServiceEndPointResolver.cs | 225 +++++++++++------- ...igurationServiceEndPointResolverOptions.cs | 24 +- ...gurationServiceEndPointResolverProvider.cs | 15 +- .../HostingExtensions.cs | 5 + .../Http/HttpClientBuilderExtensions.cs | 6 +- .../Http/ResolvingHttpClientHandler.cs | 6 +- .../Http/ResolvingHttpDelegatingHandler.cs | 94 +++++--- .../ServiceDiscoveryOptionsValidator.cs | 20 ++ .../Internal/ServiceNameParser.cs | 78 ++++++ .../Internal/ServiceNameParts.cs | 82 +------ ...crosoft.Extensions.ServiceDiscovery.csproj | 1 + ...sThroughServiceEndPointResolverProvider.cs | 42 +++- .../ServiceDiscoveryOptions.cs | 29 +++ .../Azure/AzureBicepResourceTests.cs | 10 +- .../Kafka/AddKafkaTests.cs | 8 +- .../MongoDB/AddMongoDBTests.cs | 8 +- .../MySql/AddMySqlTests.cs | 22 +- .../Oracle/AddOracleDatabaseTests.cs | 16 +- .../Postgres/AddPostgresTests.cs | 20 +- .../RabbitMQ/AddRabbitMQTests.cs | 8 +- .../Redis/AddRedisTests.cs | 14 +- .../SlimTestProgramTests.cs | 10 +- .../SqlServer/AddSqlServerTests.cs | 17 +- .../WithEnvironmentTests.cs | 11 +- .../WithReferenceTests.cs | 117 +++------ .../DnsSrvServiceEndPointResolverTests.cs | 4 +- ...nfigurationServiceEndPointResolverTests.cs | 164 ++++++++++++- ...PassThroughServiceEndPointResolverTests.cs | 8 +- .../TestProject.AppHost/TestProgram.cs | 24 +- 49 files changed, 781 insertions(+), 611 deletions(-) rename src/Aspire.Hosting/ApplicationModel/{AllocatedEndpointAnnotation.cs => AllocatedEndpoint.cs} (61%) create mode 100644 src/Microsoft.Extensions.ServiceDiscovery.Abstractions/UriEndPoint.cs create mode 100644 src/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceDiscoveryOptionsValidator.cs create mode 100644 src/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParser.cs create mode 100644 src/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs diff --git a/playground/TestShop/ApiGateway/appsettings.json b/playground/TestShop/ApiGateway/appsettings.json index 9f6414277e9..da0f07ac466 100644 --- a/playground/TestShop/ApiGateway/appsettings.json +++ b/playground/TestShop/ApiGateway/appsettings.json @@ -39,7 +39,7 @@ "basket": { "Destinations": { "basket": { - "Address": "https://basketservice", + "Address": "http://basketservice", "Health": "http://basketservice/readiness" } } diff --git a/playground/TestShop/AppHost/Program.cs b/playground/TestShop/AppHost/Program.cs index f0f52a5411b..27751a48021 100644 --- a/playground/TestShop/AppHost/Program.cs +++ b/playground/TestShop/AppHost/Program.cs @@ -19,7 +19,7 @@ builder.AddProject("frontend") .WithReference(basketService) - .WithReference(catalogService.GetEndpoint("http")); + .WithReference(catalogService); builder.AddProject("orderprocessor", launchProfileName: "OrderProcessor") .WithReference(messaging); diff --git a/playground/TestShop/AppHost/aspire-manifest.json b/playground/TestShop/AppHost/aspire-manifest.json index 9dced06a9fc..ef017945b76 100644 --- a/playground/TestShop/AppHost/aspire-manifest.json +++ b/playground/TestShop/AppHost/aspire-manifest.json @@ -123,9 +123,10 @@ "env": { "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true", "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true", - "services__basketservice__0": "{basketservice.bindings.http.url}", - "services__basketservice__1": "{basketservice.bindings.https.url}", - "services__catalogservice__0": "{catalogservice.bindings.http.url}" + "services__basketservice__http__0": "{basketservice.bindings.http.url}", + "services__basketservice__https__0": "{basketservice.bindings.https.url}", + "services__catalogservice__http__0": "{catalogservice.bindings.http.url}", + "services__catalogservice__https__0": "{catalogservice.bindings.https.url}" }, "bindings": { "http": { @@ -155,10 +156,10 @@ "env": { "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true", "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true", - "services__basketservice__0": "{basketservice.bindings.http.url}", - "services__basketservice__1": "{basketservice.bindings.https.url}", - "services__catalogservice__0": "{catalogservice.bindings.http.url}", - "services__catalogservice__1": "{catalogservice.bindings.https.url}" + "services__basketservice__http__0": "{basketservice.bindings.http.url}", + "services__basketservice__https__0": "{basketservice.bindings.https.url}", + "services__catalogservice__http__0": "{catalogservice.bindings.http.url}", + "services__catalogservice__https__0": "{catalogservice.bindings.https.url}" }, "bindings": { "http": { diff --git a/playground/TestShop/MyFrontend/Program.cs b/playground/TestShop/MyFrontend/Program.cs index 60a412dc838..009a0d4a72b 100644 --- a/playground/TestShop/MyFrontend/Program.cs +++ b/playground/TestShop/MyFrontend/Program.cs @@ -8,7 +8,7 @@ builder.Services.AddHttpForwarderWithServiceDiscovery(); -builder.Services.AddHttpClient(c => c.BaseAddress = new("http://catalogservice")); +builder.Services.AddHttpClient(c => c.BaseAddress = new("https+http://catalogservice")); builder.Services.AddSingleton() .AddGrpcClient(o => o.Address = new("http://basketservice")); diff --git a/src/Aspire.Hosting/ApplicationModel/AllocatedEndpointAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/AllocatedEndpoint.cs similarity index 61% rename from src/Aspire.Hosting/ApplicationModel/AllocatedEndpointAnnotation.cs rename to src/Aspire.Hosting/ApplicationModel/AllocatedEndpoint.cs index 61985d0247d..3e39b262c5d 100644 --- a/src/Aspire.Hosting/ApplicationModel/AllocatedEndpointAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/AllocatedEndpoint.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; -using System.Net.Sockets; namespace Aspire.Hosting.ApplicationModel; @@ -10,39 +9,29 @@ namespace Aspire.Hosting.ApplicationModel; /// Represents an endpoint allocated for a service instance. /// [DebuggerDisplay("Type = {GetType().Name,nq}, Name = {Name}, UriString = {UriString}, EndpointNameQualifiedUriString = {EndpointNameQualifiedUriString}")] -public class AllocatedEndpointAnnotation : IResourceAnnotation +public class AllocatedEndpoint { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// The name of the endpoint. - /// The protocol used by the endpoint. + /// The endpoint. /// The IP address of the endpoint. /// The port number of the endpoint. - /// The URI scheme used by the endpoint. - public AllocatedEndpointAnnotation(string name, ProtocolType protocol, string address, int port, string scheme) + public AllocatedEndpoint(EndpointAnnotation endpoint, string address, int port) { - ArgumentNullException.ThrowIfNullOrEmpty(name); - ArgumentNullException.ThrowIfNullOrEmpty(scheme); + ArgumentNullException.ThrowIfNull(endpoint); ArgumentOutOfRangeException.ThrowIfLessThan(port, 1, nameof(port)); ArgumentOutOfRangeException.ThrowIfGreaterThan(port, 65535, nameof(port)); - Name = name; - Protocol = protocol; + Endpoint = endpoint; Address = address; Port = port; - UriScheme = scheme; } /// - /// Friendly name for the endpoint. + /// Gets the endpoint which this allocation is associated with. /// - public string Name { get; private set; } - - /// - /// The network protocol (TCP and UDP are supported). - /// - public ProtocolType Protocol { get; private set; } + public EndpointAnnotation Endpoint { get; } /// /// The address of the endpoint @@ -57,18 +46,13 @@ public AllocatedEndpointAnnotation(string name, ProtocolType protocol, string ad /// /// For URI-addressed services, contains the scheme part of the address. /// - public string UriScheme { get; private set; } + public string UriScheme => Endpoint.UriScheme; /// /// Endpoint in string representation formatted as "Address:Port". /// public string EndPointString => $"{Address}:{Port}"; - /// - /// URI in string representation specially formatted to be processed by service discovery without ambiguity. - /// - public string EndpointNameQualifiedUriString => $"{Name}://{EndPointString}"; - /// /// URI in string representation. /// diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs index cf3c58bd666..3ed59826397 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs @@ -104,4 +104,9 @@ public string Transport /// /// Defaults to true. public bool IsProxied { get; set; } = true; + + /// + /// Gets or sets the allocated endpoint. + /// + public AllocatedEndpoint? AllocatedEndpoint { get; set; } } diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs index fa4b30ee29e..29a05677585 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs @@ -8,8 +8,8 @@ namespace Aspire.Hosting.ApplicationModel; /// public sealed class EndpointReference : IManifestExpressionProvider, IValueProvider { - // A reference to the allocated endpoint annotation if it exists. - private AllocatedEndpointAnnotation? _allocatedEndpointAnnotation; + // A reference to the endpoint annotation if it exists. + private EndpointAnnotation? _endpointAnnotation; private bool? _isAllocated; /// @@ -25,7 +25,7 @@ public sealed class EndpointReference : IManifestExpressionProvider, IValueProvi /// /// Gets a value indicating whether the endpoint is allocated. /// - public bool IsAllocated => _isAllocated ??= _allocatedEndpointAnnotation is not null || GetAllocatedEndpoint() is not null; + public bool IsAllocated => _isAllocated ??= GetAllocatedEndpoint() is not null; string IManifestExpressionProvider.ValueExpression => GetExpression(); @@ -51,52 +51,54 @@ public string GetExpression(EndpointProperty property = EndpointProperty.Url) /// /// Gets the port for this endpoint. /// - public int Port => AllocatedEndpointAnnotation.Port; + public int Port => AllocatedEndpoint.Port; /// /// Gets the host for this endpoint. /// - public string Host => AllocatedEndpointAnnotation.Address ?? "localhost"; + public string Host => AllocatedEndpoint.Address ?? "localhost"; /// /// Gets the scheme for this endpoint. /// - public string Scheme => AllocatedEndpointAnnotation.UriScheme; + public string Scheme => AllocatedEndpoint.UriScheme; /// /// Gets the URL for this endpoint. /// - public string Url => AllocatedEndpointAnnotation.UriString; + public string Url => AllocatedEndpoint.UriString; - private AllocatedEndpointAnnotation AllocatedEndpointAnnotation => - _allocatedEndpointAnnotation ??= GetAllocatedEndpoint() + private AllocatedEndpoint AllocatedEndpoint => + GetAllocatedEndpoint() ?? throw new InvalidOperationException($"The endpoint `{EndpointName}` is not allocated for the resource `{Owner.Name}`."); - private AllocatedEndpointAnnotation? GetAllocatedEndpoint() => - Owner.Annotations.OfType() - .SingleOrDefault(a => StringComparers.EndpointAnnotationName.Equals(a.Name, EndpointName)); + private AllocatedEndpoint? GetAllocatedEndpoint() + { + var endpoint = _endpointAnnotation ??= Owner.Annotations.OfType().SingleOrDefault(a => StringComparers.EndpointAnnotationName.Equals(a.Name, EndpointName)); + return endpoint?.AllocatedEndpoint; + } /// /// Creates a new instance of with the specified endpoint name. /// /// The resource with endpoints that owns the endpoint reference. - /// The name of the endpoint. - public EndpointReference(IResourceWithEndpoints owner, string endpointName) + /// The endpoint annotation. + public EndpointReference(IResourceWithEndpoints owner, EndpointAnnotation endpoint) { Owner = owner; - EndpointName = endpointName; + EndpointName = endpoint.Name; + _endpointAnnotation = endpoint; } /// - /// Creates a new instance of with the specified allocated endpoint annotation. + /// Creates a new instance of with the specified endpoint name. /// /// The resource with endpoints that owns the endpoint reference. - /// The allocated endpoint annotation. - public EndpointReference(IResourceWithEndpoints owner, AllocatedEndpointAnnotation allocatedEndpointAnnotation) + /// The name of the endpoint. + public EndpointReference(IResourceWithEndpoints owner, string endpointName) { Owner = owner; - EndpointName = allocatedEndpointAnnotation.Name; - _allocatedEndpointAnnotation = allocatedEndpointAnnotation; + EndpointName = endpointName; } } diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointReferenceAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/EndpointReferenceAnnotation.cs index 36ff76f39d0..7f02ca92130 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointReferenceAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointReferenceAnnotation.cs @@ -5,7 +5,7 @@ namespace Aspire.Hosting.ApplicationModel; -[DebuggerDisplay(@"Type = {GetType().Name,nq}, Resource = {Resource.Name}, EndpointNames = {string.Join("", "", EndpointNames)}")] +[DebuggerDisplay(@"Type = {GetType().Name,nq}, Resource = {Resource.Name}, EndpointNames = {UseAllEndpoints ? ""(All)"" : string.Join("", "", EndpointNames)}")] internal sealed class EndpointReferenceAnnotation(IResourceWithEndpoints resource) : IResourceAnnotation { public IResourceWithEndpoints Resource { get; } = resource; diff --git a/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs b/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs index c6b69aa4f7a..4ef46f1c75d 100644 --- a/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs +++ b/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs @@ -309,15 +309,10 @@ private static void AddAllocatedEndpointInfo(IEnumerable resources) throw new InvalidOperationException($"Service '{svc.Metadata.Name}' needs to specify a port for endpoint '{sp.EndpointAnnotation.Name}' since it isn't using a proxy."); } - var a = new AllocatedEndpointAnnotation( - sp.EndpointAnnotation.Name, - PortProtocol.ToProtocolType(svc.Spec.Protocol), + sp.EndpointAnnotation.AllocatedEndpoint = new AllocatedEndpoint( + sp.EndpointAnnotation, sp.EndpointAnnotation.IsProxied ? svc.AllocatedAddress! : "localhost", - (int)svc.AllocatedPort!, - sp.EndpointAnnotation.UriScheme - ); - - appResource.ModelResource.Annotations.Add(a); + (int)svc.AllocatedPort!); } } } diff --git a/src/Aspire.Hosting/Extensions/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/Extensions/ResourceBuilderExtensions.cs index b1b7e72c444..bb67de9c5bd 100644 --- a/src/Aspire.Hosting/Extensions/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/Extensions/ResourceBuilderExtensions.cs @@ -4,7 +4,6 @@ using System.Net.Sockets; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Publishing; -using Aspire.Hosting.Utils; namespace Aspire.Hosting; @@ -136,43 +135,23 @@ public static IResourceBuilder WithConnectionStringRedirection(this IResou return builder.WithAnnotation(new ConnectionStringRedirectAnnotation(resource), ResourceAnnotationMutationBehavior.Replace); } - private static bool ContainsAmbiguousEndpoints(IEnumerable endpoints) - { - // An ambiguous endpoint is where any scheme ( - return endpoints.GroupBy(e => e.UriScheme).Any(g => g.Count() > 1); - } - - private static Action CreateEndpointReferenceEnvironmentPopulationCallback(IResourceBuilder builder, EndpointReferenceAnnotation endpointReferencesAnnotation) - where T : IResourceWithEnvironment + private static Action CreateEndpointReferenceEnvironmentPopulationCallback(EndpointReferenceAnnotation endpointReferencesAnnotation) { return (context) => { - var name = endpointReferencesAnnotation.Resource.Name; - - var allocatedEndPoints = endpointReferencesAnnotation.Resource.Annotations - .OfType() - .Where(a => endpointReferencesAnnotation.UseAllEndpoints || endpointReferencesAnnotation.EndpointNames.Contains(a.Name)); - - var containsAmbiguousEndpoints = ContainsAmbiguousEndpoints(allocatedEndPoints); - - var replaceLocalhostWithContainerHost = builder.Resource is ContainerResource; - var configuration = builder.ApplicationBuilder.Configuration; - - var i = 0; - foreach (var allocatedEndPoint in allocatedEndPoints) + var annotation = endpointReferencesAnnotation; + var serviceName = annotation.Resource.Name; + foreach (var endpoint in annotation.Resource.GetEndpoints()) { - var endpointNameQualifiedUriStringKey = $"services__{name}__{i++}"; - context.EnvironmentVariables[endpointNameQualifiedUriStringKey] = replaceLocalhostWithContainerHost - ? HostNameResolver.ReplaceLocalhostWithContainerHost(allocatedEndPoint.EndpointNameQualifiedUriString, configuration) - : allocatedEndPoint.EndpointNameQualifiedUriString; - - if (!containsAmbiguousEndpoints) + var endpointName = endpoint.EndpointName; + if (!annotation.UseAllEndpoints && !annotation.EndpointNames.Contains(endpointName)) { - var uriStringKey = $"services__{name}__{i++}"; - context.EnvironmentVariables[uriStringKey] = replaceLocalhostWithContainerHost - ? HostNameResolver.ReplaceLocalhostWithContainerHost(allocatedEndPoint.UriString, configuration) - : allocatedEndPoint.UriString; + // Skip this endpoint since it's not in the list of endpoints we want to reference. + continue; } + + // Add the endpoint, rewriting localhost to the container host if necessary. + context.EnvironmentVariables[$"services__{serviceName}__{endpointName}__0"] = endpoint; } }; } @@ -212,7 +191,7 @@ public static IResourceBuilder WithReference(this IR /// /// Injects service discovery information as environment variables from the project resource into the destination resource, using the source resource's name as the service name. - /// Each endpoint defined on the project resource will be injected using the format "services__{sourceResourceName}__{endpointIndex}={endpointNameQualifiedUriString}." + /// Each endpoint defined on the project resource will be injected using the format "services__{sourceResourceName}__{endpointName}__{endpointIndex}={uriString}." /// /// The destination resource. /// The resource where the service discovery information will be injected. @@ -252,7 +231,7 @@ public static IResourceBuilder WithReference(this IR /// /// Injects service discovery information from the specified endpoint into the project resource using the source resource's name as the service name. - /// Each endpoint will be injected using the format "services__{sourceResourceName}__{endpointIndex}={endpointNameQualifiedUriString}." + /// Each endpoint will be injected using the format "services__{sourceResourceName}__{endpointName}__{endpointIndex}={uriString}." /// /// The destination resource. /// The resource where the service discovery information will be injected. @@ -282,7 +261,7 @@ private static void ApplyEndpoints(this IResourceBuilder builder, IResourc endpointReferenceAnnotation = new EndpointReferenceAnnotation(resourceWithEndpoints); builder.WithAnnotation(endpointReferenceAnnotation); - var callback = CreateEndpointReferenceEnvironmentPopulationCallback(builder, endpointReferenceAnnotation); + var callback = CreateEndpointReferenceEnvironmentPopulationCallback(endpointReferenceAnnotation); builder.WithEnvironment(callback); } diff --git a/src/Aspire.Hosting/Extensions/ResourceExtensions.cs b/src/Aspire.Hosting/Extensions/ResourceExtensions.cs index 3935e6a357c..fedc6ac0fa7 100644 --- a/src/Aspire.Hosting/Extensions/ResourceExtensions.cs +++ b/src/Aspire.Hosting/Extensions/ResourceExtensions.cs @@ -87,18 +87,6 @@ public static bool TryGetEndpoints(this IResource resource, [NotNullWhen(true)] return TryGetAnnotationsOfType(resource, out endpoints); } - /// - /// Attempts to get the allocated endpoints for the specified resource. - /// - /// The resource to get the allocated endpoints for. - /// When this method returns, contains the allocated endpoints for the specified resource, if they exist; otherwise, null. - /// true if the allocated endpoints were successfully retrieved; otherwise, false. - [Obsolete("Use GetEndpoints instead.")] - public static bool TryGetAllocatedEndPoints(this IResource resource, [NotNullWhen(true)] out IEnumerable? allocatedEndPoints) - { - return TryGetAnnotationsOfType(resource, out allocatedEndPoints); - } - /// /// Gets the endpoints for the specified resource. /// @@ -106,7 +94,7 @@ public static bool TryGetAllocatedEndPoints(this IResource resource, [NotNullWhe /// public static IEnumerable GetEndpoints(this IResourceWithEndpoints resource) { - if (TryGetAnnotationsOfType(resource, out var endpoints)) + if (TryGetAnnotationsOfType(resource, out var endpoints)) { return endpoints.Select(e => new EndpointReference(resource, e)); } diff --git a/src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs b/src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs index 9aba1f3a2f6..60a2a97edec 100644 --- a/src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs +++ b/src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs @@ -185,8 +185,6 @@ public async Task WriteEnvironmentVariablesAsync(IResource resource) Writer.WriteString(key, valueString); } - WriteServiceDiscoveryEnvironmentVariables(resource); - WritePortBindingEnvironmentVariables(resource); Writer.WriteEndObject(); @@ -227,40 +225,6 @@ public void WriteInputs(IResource resource) } } - /// - /// TODO: Doc Comments - /// - /// - public void WriteServiceDiscoveryEnvironmentVariables(IResource resource) - { - var endpointReferenceAnnotations = resource.Annotations.OfType(); - - if (endpointReferenceAnnotations.Any()) - { - foreach (var endpointReferenceAnnotation in endpointReferenceAnnotations) - { - var endpointNames = endpointReferenceAnnotation.UseAllEndpoints - ? endpointReferenceAnnotation.Resource.Annotations.OfType().Select(sb => sb.Name) - : endpointReferenceAnnotation.EndpointNames; - - var endpointAnnotationsGroupedByScheme = endpointReferenceAnnotation.Resource.Annotations - .OfType() - .Where(sba => endpointNames.Contains(sba.Name, StringComparers.EndpointAnnotationName)) - .GroupBy(sba => sba.UriScheme); - - var i = 0; - foreach (var endpointAnnotationGroupedByScheme in endpointAnnotationsGroupedByScheme) - { - // HACK: For November we are only going to support a single endpoint annotation - // per URI scheme per service reference. - var binding = endpointAnnotationGroupedByScheme.Single(); - - Writer.WriteString($"services__{endpointReferenceAnnotation.Resource.Name}__{i++}", $"{{{endpointReferenceAnnotation.Resource.Name}.bindings.{binding.Name}.url}}"); - } - } - } - } - /// /// TODO: Doc Comments /// diff --git a/src/Aspire.ProjectTemplates/templates/aspire-servicedefaults/Extensions.cs b/src/Aspire.ProjectTemplates/templates/aspire-servicedefaults/Extensions.cs index f44449675cb..81045b729c8 100644 --- a/src/Aspire.ProjectTemplates/templates/aspire-servicedefaults/Extensions.cs +++ b/src/Aspire.ProjectTemplates/templates/aspire-servicedefaults/Extensions.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery; using OpenTelemetry.Logs; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; @@ -28,6 +29,12 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu http.UseServiceDiscovery(); }); + // Uncomment the following to restrict the allowed schemes for service discovery. + // builder.Services.Configure(options => + // { + // options.AllowedSchemes = ["https"]; + // }); + return builder; } diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj index edafcce1914..e98dc409e76 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj +++ b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj @@ -6,6 +6,7 @@ true Provides abstractions for service discovery. Interfaces defined in this package are implemented in Microsoft.Extensions.ServiceDiscovery and other service discovery packages. $(DefaultDotnetIconFullPath) + Microsoft.Extensions.ServiceDiscovery diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/UriEndPoint.cs b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/UriEndPoint.cs new file mode 100644 index 00000000000..6d3132da880 --- /dev/null +++ b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/UriEndPoint.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; + +namespace Microsoft.Extensions.ServiceDiscovery; + +/// +/// An endpoint represented by a . +/// +/// The . +public sealed class UriEndPoint(Uri uri) : EndPoint +{ + /// + /// Gets the associated with this endpoint. + /// + public Uri Uri => uri; + + /// + public override bool Equals(object? obj) + { + return obj is UriEndPoint other && Uri.Equals(other.Uri); + } + + /// + public override int GetHashCode() => Uri.GetHashCode(); + + /// + public override string? ToString() => uri.ToString(); +} diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs b/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs index 04eec3ac52c..8f676327d5f 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs @@ -9,24 +9,16 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; -/// -/// Provides instances which resolve endpoints from DNS. -/// -/// -/// Initializes a new instance. -/// -/// The options. -/// The logger. -/// The time provider. internal sealed partial class DnsServiceEndPointResolverProvider( IOptionsMonitor options, ILogger logger, - TimeProvider timeProvider) : IServiceEndPointResolverProvider + TimeProvider timeProvider, + ServiceNameParser parser) : IServiceEndPointResolverProvider { /// public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) { - if (!ServiceNameParts.TryParse(serviceName, out var parts)) + if (!parser.TryParse(serviceName, out var parts)) { DnsServiceEndPointResolverBase.Log.ServiceNameIsNotUriOrDnsName(logger, serviceName); resolver = default; diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs b/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs index ced6e2c4a5f..bf2f502c97a 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs @@ -10,21 +10,12 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; -/// -/// Provides instances which resolve endpoints from DNS using SRV queries. -/// -/// -/// Initializes a new instance. -/// -/// The options. -/// The logger. -/// The DNS client. -/// The time provider. internal sealed partial class DnsSrvServiceEndPointResolverProvider( IOptionsMonitor options, ILogger logger, IDnsQuery dnsClient, - TimeProvider timeProvider) : IServiceEndPointResolverProvider + TimeProvider timeProvider, + ServiceNameParser parser) : IServiceEndPointResolverProvider { private static readonly string s_serviceAccountPath = Path.Combine($"{Path.DirectorySeparatorChar}var", "run", "secrets", "kubernetes.io", "serviceaccount"); private static readonly string s_serviceAccountNamespacePath = Path.Combine($"{Path.DirectorySeparatorChar}var", "run", "secrets", "kubernetes.io", "serviceaccount", "namespace"); @@ -49,7 +40,7 @@ public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServi return false; } - if (!ServiceNameParts.TryParse(serviceName, out var parts)) + if (!parser.TryParse(serviceName, out var parts)) { DnsServiceEndPointResolverBase.Log.ServiceNameIsNotUriOrDnsName(logger, serviceName); resolver = default; diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj b/src/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj index 7fed469c4d8..0d4c3cbeac2 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj +++ b/src/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj @@ -8,10 +8,6 @@ $(DefaultDotnetIconFullPath) - - - - diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs b/src/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs index 0da7e8b55eb..d37e4f1407d 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.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 Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery.Abstractions; using Microsoft.Extensions.ServiceDiscovery.Http; using Yarp.ReverseProxy.Forwarder; @@ -10,11 +11,12 @@ namespace Microsoft.Extensions.ServiceDiscovery.Yarp; internal sealed class ServiceDiscoveryForwarderHttpClientFactory( TimeProvider timeProvider, IServiceEndPointSelectorProvider selectorProvider, - ServiceEndPointResolverFactory factory) : ForwarderHttpClientFactory + ServiceEndPointResolverFactory factory, + IOptions options) : ForwarderHttpClientFactory { protected override HttpMessageHandler WrapHandler(ForwarderHttpClientContext context, HttpMessageHandler handler) { var registry = new HttpServiceEndPointResolver(factory, selectorProvider, timeProvider); - return new ResolvingHttpDelegatingHandler(registry, handler); + return new ResolvingHttpDelegatingHandler(registry, options, handler); } } diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs b/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs index b7e43172740..5916951c69d 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs @@ -12,7 +12,7 @@ internal sealed partial class ConfigurationServiceEndPointResolver { private sealed partial class Log { - [LoggerMessage(1, LogLevel.Debug, "Skipping endpoint resolution for service '{ServiceName}': '{Reason}'.", EventName = "SkippedResolution")] + [LoggerMessage(1, LogLevel.Debug, "Skipping endpoint resolution for service '{ServiceName}': '{Reason}'.", EventName = "SkippedResolution")] public static partial void SkippedResolution(ILogger logger, string serviceName, string reason); [LoggerMessage(2, LogLevel.Debug, "Matching endpoints using endpoint names for service '{ServiceName}' since endpoint names are specified in configuration.", EventName = "MatchingEndPointNames")] @@ -38,11 +38,11 @@ public static void EndPointNameMatchSelection(ILogger logger, string serviceName } } - [LoggerMessage(4, LogLevel.Debug, "Using configuration from path '{Path}' to resolve endpoints for service '{ServiceName}'.", EventName = "UsingConfigurationPath")] - public static partial void UsingConfigurationPath(ILogger logger, string path, string serviceName); + [LoggerMessage(4, LogLevel.Debug, "Using configuration from path '{Path}' to resolve endpoint '{EndpointName}' for service '{ServiceName}'.", EventName = "UsingConfigurationPath")] + public static partial void UsingConfigurationPath(ILogger logger, string path, string endpointName, string serviceName); - [LoggerMessage(5, LogLevel.Debug, "No endpoints configured for service '{ServiceName}' from path '{Path}'.", EventName = "ConfigurationNotFound")] - internal static partial void ConfigurationNotFound(ILogger logger, string serviceName, string path); + [LoggerMessage(5, LogLevel.Debug, "No valid endpoint configuration was found for service '{ServiceName}' from path '{Path}'.", EventName = "ServiceConfigurationNotFound")] + internal static partial void ServiceConfigurationNotFound(ILogger logger, string serviceName, string path); [LoggerMessage(6, LogLevel.Debug, "Endpoints configured for service '{ServiceName}' from path '{Path}': {ConfiguredEndPoints}.", EventName = "ConfiguredEndPoints")] internal static partial void ConfiguredEndPoints(ILogger logger, string serviceName, string path, string configuredEndPoints); @@ -67,5 +67,8 @@ public static void ConfiguredEndPoints(ILogger logger, string serviceName, strin var configuredEndPoints = endpointValues.ToString(); ConfiguredEndPoints(logger, serviceName, path, configuredEndPoints); } + + [LoggerMessage(7, LogLevel.Debug, "No valid endpoint configuration was found for endpoint '{EndpointName}' on service '{ServiceName}' from path '{Path}'.", EventName = "EndpointConfigurationNotFound")] + internal static partial void EndpointConfigurationNotFound(ILogger logger, string endpointName, string serviceName, string path); } } diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs b/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs index 8ffb3de4039..dae054c9883 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs @@ -1,8 +1,8 @@ // 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.Net; -using System.Text; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -11,12 +11,14 @@ namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; /// -/// A service endpoint resolver that uses configuration to resolve endpoints. +/// A service endpoint resolver that uses configuration to resolve resolved. /// internal sealed partial class ConfigurationServiceEndPointResolver : IServiceEndPointProvider, IHostNameFeature { + private const string DefaultEndPointName = "default"; private readonly string _serviceName; private readonly string? _endpointName; + private readonly string[] _schemes; private readonly IConfiguration _configuration; private readonly ILogger _logger; private readonly IOptions _options; @@ -28,16 +30,19 @@ internal sealed partial class ConfigurationServiceEndPointResolver : IServiceEnd /// The configuration. /// The logger. /// The options. + /// The service name parser. public ConfigurationServiceEndPointResolver( string serviceName, IConfiguration configuration, ILogger logger, - IOptions options) + IOptions options, + ServiceNameParser parser) { - if (ServiceNameParts.TryParse(serviceName, out var parts)) + if (parser.TryParse(serviceName, out var parts)) { _serviceName = parts.Host; _endpointName = parts.EndPointName; + _schemes = parts.Schemes; } else { @@ -59,141 +64,193 @@ public ConfigurationServiceEndPointResolver( private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoints) { - // Only add endpoints to the collection if a previous provider (eg, an override) did not add them. + // Only add resolved to the collection if a previous provider (eg, an override) did not add them. if (endPoints.EndPoints.Count != 0) { Log.SkippedResolution(_logger, _serviceName, "Collection has existing endpoints"); return ResolutionStatus.None; } - var root = _configuration; - var baseSectionName = _options.Value.SectionName; - if (baseSectionName is { Length: > 0 }) - { - root = root.GetSection(baseSectionName); - } - // Get the corresponding config section. - var section = root.GetSection(_serviceName); - var configPath = GetConfigurationPath(baseSectionName); - Log.UsingConfigurationPath(_logger, configPath, _serviceName); + var section = _configuration.GetSection(_options.Value.SectionName).GetSection(_serviceName); if (!section.Exists()) { - return CreateNotFoundResponse(endPoints, configPath); + return CreateNotFoundResponse(endPoints, $"{_options.Value.SectionName}:{_serviceName}"); } - // Read the endpoint from the configuration. - // First check if there is a collection of sections - var children = section.GetChildren(); - if (children.Any()) + endPoints.AddChangeToken(section.GetReloadToken()); + + // Find an appropriate configuration section based on the input. + IConfigurationSection? namedSection = null; + string endpointName; + if (string.IsNullOrWhiteSpace(_endpointName)) { - var values = children.Select(c => c.Value!).Where(s => !string.IsNullOrEmpty(s)).ToList(); - if (values is { Count: > 0 }) + if (_schemes.Length == 0) { - // Use endpoint names if any of the values have an endpoint name set. - var parsedValues = ParseServiceNameParts(values, configPath); - Log.ConfiguredEndPoints(_logger, _serviceName, configPath, parsedValues); - - var matchEndPointNames = !parsedValues.TrueForAll(static uri => string.IsNullOrEmpty(uri.EndPointName)); - Log.EndPointNameMatchSelection(_logger, _serviceName, matchEndPointNames); + // Use the section named "default". + endpointName = DefaultEndPointName; + namedSection = section.GetSection(endpointName); + } + else + { + // Set the ideal endpoint name for error messages. + endpointName = _schemes[0]; - foreach (var uri in parsedValues) + // Treat the scheme as the endpoint name and use the first section with a matching endpoint name which exists + foreach (var scheme in _schemes) { - // If either endpoint names are not in-use or the scheme matches, create an endpoint for this value. - if (!matchEndPointNames || EndPointNamesMatch(_endpointName, uri)) + var candidate = section.GetSection(scheme); + if (candidate.Exists()) { - if (!ServiceNameParts.TryCreateEndPoint(uri, out var endPoint)) - { - return ResolutionStatus.FromException(new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' is invalid.")); - } - - endPoints.EndPoints.Add(CreateEndPoint(endPoint)); + endpointName = scheme; + namedSection = candidate; + break; } } } } - else if (section.Value is { } value && ServiceNameParts.TryParse(value, out var parsed)) + else + { + // Use the section corresponding to the endpoint name. + endpointName = _endpointName; + namedSection = section.GetSection(_endpointName); + } + + var configPath = $"{_options.Value.SectionName}:{_serviceName}:{endpointName}"; + if (!namedSection.Exists()) + { + return CreateNotFoundResponse(endPoints, configPath); + } + + List resolved = []; + Log.UsingConfigurationPath(_logger, configPath, endpointName, _serviceName); + + // Account for both the single and multi-value cases. + if (!string.IsNullOrWhiteSpace(namedSection.Value)) + { + // Single value case. + if (!TryAddEndPoint(resolved, namedSection, endpointName, out var error)) + { + return error; + } + } + else { - if (EndPointNamesMatch(_endpointName, parsed)) + // Multiple value case. + foreach (var child in namedSection.GetChildren()) { - if (!ServiceNameParts.TryCreateEndPoint(parsed, out var endPoint)) + if (!int.TryParse(child.Key, out _)) { - return ResolutionStatus.FromException(new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' is invalid.")); + return ResolutionStatus.FromException(new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' endpoint '{endpointName}' has non-numeric keys.")); } - if (_logger.IsEnabled(LogLevel.Debug)) + if (!TryAddEndPoint(resolved, child, endpointName, out var error)) { - Log.ConfiguredEndPoints(_logger, _serviceName, configPath, [parsed]); + return error; } - - endPoints.EndPoints.Add(CreateEndPoint(endPoint)); } } - if (endPoints.EndPoints.Count == 0) + // Filter the resolved endpoints to only include those which match the specified scheme. + var minIndex = _schemes.Length; + foreach (var ep in resolved) { - Log.ConfigurationNotFound(_logger, _serviceName, configPath); + if (ep.EndPoint is UriEndPoint uri && uri.Uri.Scheme is { } scheme) + { + var index = Array.IndexOf(_schemes, scheme); + if (index >= 0 && index < minIndex) + { + minIndex = index; + } + } } - endPoints.AddChangeToken(section.GetReloadToken()); - return ResolutionStatus.Success; - - static bool EndPointNamesMatch(string? endPointName, ServiceNameParts parts) => - string.IsNullOrEmpty(parts.EndPointName) - || string.IsNullOrEmpty(endPointName) - || MemoryExtensions.Equals(parts.EndPointName, endPointName, StringComparison.OrdinalIgnoreCase); - } + var added = 0; + foreach (var ep in resolved) + { + if (ep.EndPoint is UriEndPoint uri && uri.Uri.Scheme is { } scheme) + { + var index = Array.IndexOf(_schemes, scheme); + if (index >= 0 && index <= minIndex) + { + ++added; + endPoints.EndPoints.Add(ep); + } + } + else + { + ++added; + endPoints.EndPoints.Add(ep); + } + } - private ServiceEndPoint CreateEndPoint(EndPoint endPoint) - { - var serviceEndPoint = ServiceEndPoint.Create(endPoint); - serviceEndPoint.Features.Set(this); - if (_options.Value.ApplyHostNameMetadata(serviceEndPoint)) + if (added == 0) { - serviceEndPoint.Features.Set(this); + return CreateNotFoundResponse(endPoints, configPath); } - return serviceEndPoint; - } + return ResolutionStatus.Success; - private ResolutionStatus CreateNotFoundResponse(ServiceEndPointCollectionSource endPoints, string configPath) - { - endPoints.AddChangeToken(_configuration.GetReloadToken()); - Log.ConfigurationNotFound(_logger, _serviceName, configPath); - return ResolutionStatus.CreateNotFound($"No configuration for the specified path '{configPath}' was found."); } - private string GetConfigurationPath(string? baseSectionName) + private bool TryAddEndPoint(List endPoints, IConfigurationSection section, string endpointName, out ResolutionStatus error) { - var configPath = new StringBuilder(); - if (baseSectionName is { Length: > 0 }) + var value = section.Value; + if (string.IsNullOrWhiteSpace(value) || !TryParseEndPoint(value, out var endPoint)) { - configPath.Append(baseSectionName).Append(':'); + error = ResolutionStatus.FromException(new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' endpoint '{endpointName}' has an invalid value with key '{section.Key}'.")); + return false; } - configPath.Append(_serviceName); - return configPath.ToString(); + endPoints.Add(CreateEndPoint(endPoint)); + error = default; + return true; } - private List ParseServiceNameParts(List input, string configPath) + private static bool TryParseEndPoint(string value, [NotNullWhen(true)] out EndPoint? endPoint) { - var results = new List(input.Count); - for (var i = 0; i < input.Count; ++i) + if (value.IndexOf("://") < 0 && Uri.TryCreate($"fakescheme://{value}", default, out var uri)) { - if (ServiceNameParts.TryParse(input[i], out var value)) + var port = uri.Port > 0 ? uri.Port : 0; + if (IPAddress.TryParse(uri.Host, out var ip)) { - if (!results.Contains(value)) - { - results.Add(value); - } + endPoint = new IPEndPoint(ip, port); } else { - throw new InvalidOperationException($"The endpoint configuration '{input[i]}' from path '{configPath}[{i}]' for service '{_serviceName}' is invalid."); + endPoint = new DnsEndPoint(uri.Host, port); } } + else if (Uri.TryCreate(value, default, out uri)) + { + endPoint = new UriEndPoint(uri); + } + else + { + endPoint = null; + return false; + } + + return true; + } + + private ServiceEndPoint CreateEndPoint(EndPoint endPoint) + { + var serviceEndPoint = ServiceEndPoint.Create(endPoint); + serviceEndPoint.Features.Set(this); + if (_options.Value.ApplyHostNameMetadata(serviceEndPoint)) + { + serviceEndPoint.Features.Set(this); + } + + return serviceEndPoint; + } - return results; + private ResolutionStatus CreateNotFoundResponse(ServiceEndPointCollectionSource endPoints, string configPath) + { + endPoints.AddChangeToken(_configuration.GetReloadToken()); + Log.ServiceConfigurationNotFound(_logger, _serviceName, configPath); + return ResolutionStatus.CreateNotFound($"No valid endpoint configuration was found for service '{_serviceName}' from path '{configPath}'."); } public override string ToString() => "Configuration"; diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs b/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs index 5e67a885ca9..c83589eb268 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs @@ -1,20 +1,40 @@ // 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.Options; + namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; /// /// Options for . /// -public class ConfigurationServiceEndPointResolverOptions +public sealed class ConfigurationServiceEndPointResolverOptions { /// /// The name of the configuration section which contains service endpoints. Defaults to "Services". /// - public string? SectionName { get; set; } = "Services"; + public string SectionName { get; set; } = "Services"; /// /// Gets or sets a delegate used to determine whether to apply host name metadata to each resolved endpoint. Defaults to false. /// public Func ApplyHostNameMetadata { get; set; } = _ => false; } + +internal sealed class ConfigurationServiceEndPointResolverOptionsValidator : IValidateOptions +{ + public ValidateOptionsResult Validate(string? name, ConfigurationServiceEndPointResolverOptions options) + { + if (string.IsNullOrWhiteSpace(options.SectionName)) + { + return ValidateOptionsResult.Fail($"{nameof(options.SectionName)} must not be null or empty."); + } + + if (options.ApplyHostNameMetadata is null) + { + return ValidateOptionsResult.Fail($"{nameof(options.ApplyHostNameMetadata)} must not be null."); + } + + return ValidateOptionsResult.Success; + } +} diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs b/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs index 638c3e6d640..472205f12f9 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs @@ -5,28 +5,23 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.Extensions.ServiceDiscovery.Internal; namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; /// /// implementation that resolves services using . /// -/// The configuration. -/// The options. -/// The logger factory. -public class ConfigurationServiceEndPointResolverProvider( +internal sealed class ConfigurationServiceEndPointResolverProvider( IConfiguration configuration, IOptions options, - ILoggerFactory loggerFactory) : IServiceEndPointResolverProvider + ILogger logger, + ServiceNameParser parser) : IServiceEndPointResolverProvider { - private readonly IConfiguration _configuration = configuration; - private readonly IOptions _options = options; - private readonly ILogger _logger = loggerFactory.CreateLogger(); - /// public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) { - resolver = new ConfigurationServiceEndPointResolver(serviceName, _configuration, _logger, _options); + resolver = new ConfigurationServiceEndPointResolver(serviceName, configuration, logger, options, parser); return true; } } diff --git a/src/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs b/src/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs index 5326d5a2935..a4ba9b63b31 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs @@ -4,8 +4,10 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery; using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Internal; using Microsoft.Extensions.ServiceDiscovery.PassThrough; namespace Microsoft.Extensions.Hosting; @@ -36,6 +38,8 @@ public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection { services.AddOptions(); services.AddLogging(); + services.TryAddSingleton(); + services.TryAddTransient, ServiceDiscoveryOptionsValidator>(); services.TryAddSingleton(static sp => TimeProvider.System); services.TryAddSingleton(); services.TryAddSingleton(); @@ -63,6 +67,7 @@ public static IServiceCollection AddConfigurationServiceEndPointResolver(this IS { services.AddServiceDiscoveryCore(); services.AddSingleton(); + services.AddTransient, ConfigurationServiceEndPointResolverOptionsValidator>(); if (configureOptions is not null) { services.Configure(configureOptions); diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs b/src/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs index 0a507fb2f0b..c1c833de89f 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs @@ -31,7 +31,8 @@ public static IHttpClientBuilder UseServiceDiscovery(this IHttpClientBuilder htt var timeProvider = services.GetService() ?? TimeProvider.System; var resolverProvider = services.GetRequiredService(); var registry = new HttpServiceEndPointResolver(resolverProvider, selectorProvider, timeProvider); - return new ResolvingHttpDelegatingHandler(registry); + var options = services.GetRequiredService>(); + return new ResolvingHttpDelegatingHandler(registry, options); }); // Configure the HttpClient to disable gRPC load balancing. @@ -56,7 +57,8 @@ public static IHttpClientBuilder UseServiceDiscovery(this IHttpClientBuilder htt var selectorProvider = services.GetRequiredService(); var resolverProvider = services.GetRequiredService(); var registry = new HttpServiceEndPointResolver(resolverProvider, selectorProvider, timeProvider); - return new ResolvingHttpDelegatingHandler(registry); + var options = services.GetRequiredService>(); + return new ResolvingHttpDelegatingHandler(registry, options); }); // Configure the HttpClient to disable gRPC load balancing. diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs b/src/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs index 52ff1d4494d..ba32139f6ab 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Internal; +using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Http; @@ -9,9 +10,10 @@ namespace Microsoft.Extensions.ServiceDiscovery.Http; /// /// which resolves endpoints using service discovery. /// -public class ResolvingHttpClientHandler(HttpServiceEndPointResolver resolver) : HttpClientHandler +public class ResolvingHttpClientHandler(HttpServiceEndPointResolver resolver, IOptions options) : HttpClientHandler { private readonly HttpServiceEndPointResolver _resolver = resolver; + private readonly ServiceDiscoveryOptions _options = options.Value; /// protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) @@ -23,7 +25,7 @@ protected override async Task SendAsync(HttpRequestMessage if (originalUri?.Host is not null) { var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); - request.RequestUri = ResolvingHttpDelegatingHandler.GetUriWithEndPoint(originalUri, result); + request.RequestUri = ResolvingHttpDelegatingHandler.GetUriWithEndPoint(originalUri, result, _options); request.Headers.Host ??= result.Features.Get()?.HostName; epHealth = result.Features.Get(); } diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs b/src/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs index 8c0d67f41bd..2ab3c8dc3ec 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs @@ -3,6 +3,7 @@ using System.Net; using Microsoft.Extensions.Internal; +using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Http; @@ -13,24 +14,29 @@ namespace Microsoft.Extensions.ServiceDiscovery.Http; public class ResolvingHttpDelegatingHandler : DelegatingHandler { private readonly HttpServiceEndPointResolver _resolver; + private readonly ServiceDiscoveryOptions _options; /// /// Initializes a new instance. /// /// The endpoint resolver. - public ResolvingHttpDelegatingHandler(HttpServiceEndPointResolver resolver) + /// The service discovery options. + public ResolvingHttpDelegatingHandler(HttpServiceEndPointResolver resolver, IOptions options) { _resolver = resolver; + _options = options.Value; } /// /// Initializes a new instance. /// /// The endpoint resolver. + /// The service discovery options. /// The inner handler. - public ResolvingHttpDelegatingHandler(HttpServiceEndPointResolver resolver, HttpMessageHandler innerHandler) : base(innerHandler) + public ResolvingHttpDelegatingHandler(HttpServiceEndPointResolver resolver, IOptions options, HttpMessageHandler innerHandler) : base(innerHandler) { _resolver = resolver; + _options = options.Value; } /// @@ -43,7 +49,7 @@ protected override async Task SendAsync(HttpRequestMessage if (originalUri?.Host is not null) { var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); - request.RequestUri = GetUriWithEndPoint(originalUri, result); + request.RequestUri = GetUriWithEndPoint(originalUri, result, _options); request.Headers.Host ??= result.Features.Get()?.HostName; epHealth = result.Features.Get(); } @@ -64,37 +70,71 @@ protected override async Task SendAsync(HttpRequestMessage } } - internal static Uri GetUriWithEndPoint(Uri uri, ServiceEndPoint serviceEndPoint) + internal static Uri GetUriWithEndPoint(Uri uri, ServiceEndPoint serviceEndPoint, ServiceDiscoveryOptions options) { var endpoint = serviceEndPoint.EndPoint; - - string host; - int port; - switch (endpoint) + UriBuilder result; + if (endpoint is UriEndPoint { Uri: { } ep }) { - case IPEndPoint ip: - host = ip.Address.ToString(); - port = ip.Port; - break; - case DnsEndPoint dns: - host = dns.Host; - port = dns.Port; - break; - default: - throw new InvalidOperationException($"Endpoints of type {endpoint.GetType()} are not supported"); - } + result = new UriBuilder(uri) + { + Scheme = ep.Scheme, + Host = ep.Host, + }; - var builder = new UriBuilder(uri) - { - Host = host, - }; + if (ep.Port > 0) + { + result.Port = ep.Port; + } - // Default to the default port for the scheme. - if (port > 0) + if (ep.AbsolutePath.Length > 1) + { + result.Path = $"{ep.AbsolutePath.TrimEnd('/')}/{uri.AbsolutePath.TrimStart('/')}"; + } + } + else { - builder.Port = port; + string host; + int port; + switch (endpoint) + { + case IPEndPoint ip: + host = ip.Address.ToString(); + port = ip.Port; + break; + case DnsEndPoint dns: + host = dns.Host; + port = dns.Port; + break; + default: + throw new InvalidOperationException($"Endpoints of type {endpoint.GetType()} are not supported"); + } + + result = new UriBuilder(uri) + { + Host = host, + }; + + // Default to the default port for the scheme. + if (port > 0) + { + result.Port = port; + } + + if (uri.Scheme.IndexOf('+') > 0) + { + var scheme = uri.Scheme.Split('+')[0]; + if (options.AllowedSchemes.Equals(ServiceDiscoveryOptions.AllSchemes) || options.AllowedSchemes.Contains(scheme, StringComparer.OrdinalIgnoreCase)) + { + result.Scheme = scheme; + } + else + { + throw new InvalidOperationException($"The scheme '{scheme}' is not allowed."); + } + } } - return builder.Uri; + return result.Uri; } } diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceDiscoveryOptionsValidator.cs b/src/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceDiscoveryOptionsValidator.cs new file mode 100644 index 00000000000..fae7bd6f4fc --- /dev/null +++ b/src/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceDiscoveryOptionsValidator.cs @@ -0,0 +1,20 @@ +// 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.Options; + +namespace Microsoft.Extensions.ServiceDiscovery.Internal; + +internal sealed class ServiceDiscoveryOptionsValidator : IValidateOptions +{ + public ValidateOptionsResult Validate(string? name, ServiceDiscoveryOptions options) + { + if (options.AllowedSchemes is null) + { + return ValidateOptionsResult.Fail("At least one allowed scheme must be specified."); + } + + return ValidateOptionsResult.Success; + } +} + diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParser.cs b/src/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParser.cs new file mode 100644 index 00000000000..de047481872 --- /dev/null +++ b/src/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParser.cs @@ -0,0 +1,78 @@ +// 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.Options; + +namespace Microsoft.Extensions.ServiceDiscovery.Internal; + +internal sealed class ServiceNameParser(IOptions options) +{ + private readonly string[] _allowedSchemes = options.Value.AllowedSchemes; + + public bool TryParse(string serviceName, [NotNullWhen(true)] out ServiceNameParts parts) + { + if (serviceName.IndexOf("://") < 0 && Uri.TryCreate($"fakescheme://{serviceName}", default, out var uri)) + { + parts = Create(uri, hasScheme: false); + return true; + } + + if (Uri.TryCreate(serviceName, default, out uri)) + { + parts = Create(uri, hasScheme: true); + return true; + } + + parts = default; + return false; + + ServiceNameParts Create(Uri uri, bool hasScheme) + { + var uriHost = uri.Host; + var segmentSeparatorIndex = uriHost.IndexOf('.'); + string host; + string? endPointName = null; + var port = uri.Port > 0 ? uri.Port : 0; + if (uriHost.StartsWith('_') && segmentSeparatorIndex > 1 && uriHost[^1] != '.') + { + endPointName = uriHost[1..segmentSeparatorIndex]; + + // Skip the endpoint name, including its prefix ('_') and suffix ('.'). + host = uriHost[(segmentSeparatorIndex + 1)..]; + } + else + { + host = uriHost; + } + + // Allow multiple schemes to be separated by a '+', eg. "https+http://host:port". + var schemes = hasScheme ? ParseSchemes(uri.Scheme) : []; + return new(schemes, host, endPointName, port); + } + } + + private string[] ParseSchemes(string scheme) + { + if (_allowedSchemes.Equals(ServiceDiscoveryOptions.AllSchemes)) + { + return scheme.Split('+'); + } + + List result = []; + foreach (var s in scheme.Split('+')) + { + foreach (var allowed in _allowedSchemes) + { + if (string.Equals(s, allowed, StringComparison.OrdinalIgnoreCase)) + { + result.Add(s); + break; + } + } + } + + return result.ToArray(); + } +} + diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs b/src/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs index f9ab6ff6c2b..f93729a40ec 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs @@ -1,15 +1,13 @@ // 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.Net; - namespace Microsoft.Extensions.ServiceDiscovery.Internal; internal readonly struct ServiceNameParts : IEquatable { - public ServiceNameParts(string host, string? endPointName, int port) : this() + public ServiceNameParts(string[] schemePriority, string host, string? endPointName, int port) : this() { + Schemes = schemePriority; Host = host; EndPointName = endPointName; Port = port; @@ -17,86 +15,14 @@ public ServiceNameParts(string host, string? endPointName, int port) : this() public string? EndPointName { get; init; } + public string[] Schemes { get; init; } + public string Host { get; init; } public int Port { get; init; } public override string? ToString() => EndPointName is not null ? $"EndPointName: {EndPointName}, Host: {Host}, Port: {Port}" : $"Host: {Host}, Port: {Port}"; - public static bool TryParse(string serviceName, [NotNullWhen(true)] out ServiceNameParts parts) - { - if (serviceName.IndexOf("://") < 0 && Uri.TryCreate($"fakescheme://{serviceName}", default, out var uri)) - { - parts = Create(uri, hasScheme: false); - return true; - } - - if (Uri.TryCreate(serviceName, default, out uri)) - { - parts = Create(uri, hasScheme: true); - return true; - } - - parts = default; - return false; - - static ServiceNameParts Create(Uri uri, bool hasScheme) - { - var uriHost = uri.Host; - var segmentSeparatorIndex = uriHost.IndexOf('.'); - string host; - string? endPointName = null; - var port = uri.Port > 0 ? uri.Port : 0; - if (uriHost.StartsWith('_') && segmentSeparatorIndex > 1 && uriHost[^1] != '.') - { - endPointName = uriHost[1..segmentSeparatorIndex]; - - // Skip the endpoint name, including its prefix ('_') and suffix ('.'). - host = uriHost[(segmentSeparatorIndex + 1)..]; - } - else - { - host = uriHost; - if (hasScheme) - { - endPointName = uri.Scheme; - } - } - - return new(host, endPointName, port); - } - } - - public static bool TryCreateEndPoint(ServiceNameParts parts, [NotNullWhen(true)] out EndPoint? endPoint) - { - if (IPAddress.TryParse(parts.Host, out var ip)) - { - endPoint = new IPEndPoint(ip, parts.Port); - } - else if (!string.IsNullOrEmpty(parts.Host)) - { - endPoint = new DnsEndPoint(parts.Host, parts.Port); - } - else - { - endPoint = null; - return false; - } - - return true; - } - - public static bool TryCreateEndPoint(string serviceName, [NotNullWhen(true)] out EndPoint? serviceEndPoint) - { - if (TryParse(serviceName, out var parts)) - { - return TryCreateEndPoint(parts, out serviceEndPoint); - } - - serviceEndPoint = null; - return false; - } - public override bool Equals(object? obj) => obj is ServiceNameParts other && Equals(other); public override int GetHashCode() => HashCode.Combine(EndPointName, Host, Port); diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index 1fa156be1d4..60ec55fe9df 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs b/src/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs index 32a64b8d350..b3a326010bb 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs @@ -5,7 +5,6 @@ using System.Net; using Microsoft.Extensions.Logging; using Microsoft.Extensions.ServiceDiscovery.Abstractions; -using Microsoft.Extensions.ServiceDiscovery.Internal; namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; @@ -17,7 +16,7 @@ internal sealed class PassThroughServiceEndPointResolverProvider(ILogger public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) { - if (!ServiceNameParts.TryCreateEndPoint(serviceName, out var endPoint)) + if (!TryCreateEndPoint(serviceName, out var endPoint)) { // Propagate the value through regardless, leaving it to the caller to interpret it. endPoint = new DnsEndPoint(serviceName, 0); @@ -26,4 +25,43 @@ public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServi resolver = new PassThroughServiceEndPointResolver(logger, serviceName, endPoint); return true; } + + private static bool TryCreateEndPoint(string serviceName, [NotNullWhen(true)] out EndPoint? serviceEndPoint) + { + if ((serviceName.Contains("://", StringComparison.Ordinal) || !Uri.TryCreate($"fakescheme://{serviceName}", default, out var uri)) && !Uri.TryCreate(serviceName, default, out uri)) + { + serviceEndPoint = null; + return false; + } + + var uriHost = uri.Host; + var segmentSeparatorIndex = uriHost.IndexOf('.'); + string host; + if (uriHost.StartsWith('_') && segmentSeparatorIndex > 1 && uriHost[^1] != '.') + { + // Skip the endpoint name, including its prefix ('_') and suffix ('.'). + host = uriHost[(segmentSeparatorIndex + 1)..]; + } + else + { + host = uriHost; + } + + var port = uri.Port > 0 ? uri.Port : 0; + if (IPAddress.TryParse(host, out var ip)) + { + serviceEndPoint = new IPEndPoint(ip, port); + } + else if (!string.IsNullOrEmpty(host)) + { + serviceEndPoint = new DnsEndPoint(host, port); + } + else + { + serviceEndPoint = null; + return false; + } + + return true; + } } diff --git a/src/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs b/src/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs new file mode 100644 index 00000000000..d9510a3cf22 --- /dev/null +++ b/src/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.ServiceDiscovery; + +/// +/// Options for configuring service discovery. +/// +public sealed class ServiceDiscoveryOptions +{ + /// + /// The value for which indicates that all schemes are allowed. + /// +#pragma warning disable IDE0300 // Simplify collection initialization +#pragma warning disable CA1825 // Avoid zero-length array allocations + public static readonly string[] AllSchemes = new string[0]; +#pragma warning restore CA1825 // Avoid zero-length array allocations +#pragma warning restore IDE0300 // Simplify collection initialization + + /// + /// Gets or sets the collection of allowed URI schemes for URIs resolved by the service discovery system when multiple schemes are specified, for example "https+http://_endpoint.service". + /// + /// + /// When set to , all schemes are allowed. + /// Schemes are not case-sensitive. + /// + public string[] AllowedSchemes { get; set; } = AllSchemes; +} + diff --git a/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs b/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs index e3f2030fd8e..66b9b7e40e5 100644 --- a/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs +++ b/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.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.Net.Sockets; using System.Text.Json.Nodes; using Aspire.Hosting.Azure; using Aspire.Hosting.Tests.Utils; @@ -317,7 +316,7 @@ public async Task PublishAsRedisPublishesRedisAsAzureRedis() var builder = DistributedApplication.CreateBuilder(); var redis = builder.AddRedis("cache") - .WithAnnotation(new AllocatedEndpointAnnotation("tcp", ProtocolType.Tcp, "localhost", 12455, "tcp")) + .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 12455)) .PublishAsAzureRedis(); Assert.True(redis.Resource.IsContainer()); @@ -336,7 +335,7 @@ public async Task PublishAsRedisPublishesRedisAsAzureRedisConstruct() var builder = DistributedApplication.CreateBuilder(); var redis = builder.AddRedis("cache") - .WithAnnotation(new AllocatedEndpointAnnotation("tcp", ProtocolType.Tcp, "localhost", 12455, "tcp")) + .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 12455)) .PublishAsAzureRedisConstruct(useProvisioner: false); // Resolving abiguity due to InternalsVisibleTo Assert.True(redis.Resource.IsContainer()); @@ -562,9 +561,8 @@ public async Task PublishAsAzurePostgresFlexibleServer() // Verify that when PublishAs variant is used, connection string acquisition // still uses the local endpoint. - var endpointAnnotation = new AllocatedEndpointAnnotation(PostgresServerResource.PrimaryEndpointName, System.Net.Sockets.ProtocolType.Tcp, "localhost", 1234, "tcp"); - postgres.WithAnnotation(endpointAnnotation); - var expectedConnectionString = $"Host={endpointAnnotation.Address};Port={endpointAnnotation.Port};Username=postgres;Password={PasswordUtil.EscapePassword(postgres.Resource.Password)}"; + postgres.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 1234)); + var expectedConnectionString = $"Host=localhost;Port=1234;Username=postgres;Password={PasswordUtil.EscapePassword(postgres.Resource.Password)}"; Assert.Equal(expectedConnectionString, postgres.Resource.GetConnectionString()); Assert.Equal("{postgres.secretOutputs.connectionString}", azurePostgres.Resource.ConnectionStringExpression); diff --git a/tests/Aspire.Hosting.Tests/Kafka/AddKafkaTests.cs b/tests/Aspire.Hosting.Tests/Kafka/AddKafkaTests.cs index 59a8f9c5faf..a4775b3fbea 100644 --- a/tests/Aspire.Hosting.Tests/Kafka/AddKafkaTests.cs +++ b/tests/Aspire.Hosting.Tests/Kafka/AddKafkaTests.cs @@ -47,13 +47,7 @@ public void KafkaCreatesConnectionString() var appBuilder = DistributedApplication.CreateBuilder(); appBuilder .AddKafka("kafka") - .WithAnnotation( - new AllocatedEndpointAnnotation(KafkaServerResource.PrimaryEndpointName, - ProtocolType.Tcp, - "localhost", - 27017, - "tcp" - )); + .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 27017)); using var app = appBuilder.Build(); diff --git a/tests/Aspire.Hosting.Tests/MongoDB/AddMongoDBTests.cs b/tests/Aspire.Hosting.Tests/MongoDB/AddMongoDBTests.cs index faceb405e4f..bbff7a1c5cf 100644 --- a/tests/Aspire.Hosting.Tests/MongoDB/AddMongoDBTests.cs +++ b/tests/Aspire.Hosting.Tests/MongoDB/AddMongoDBTests.cs @@ -80,13 +80,7 @@ public void MongoDBCreatesConnectionString() var appBuilder = DistributedApplication.CreateBuilder(); appBuilder .AddMongoDB("mongodb") - .WithAnnotation( - new AllocatedEndpointAnnotation("tcp", - ProtocolType.Tcp, - "localhost", - 27017, - "https" - )) + .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 27017)) .AddDatabase("mydatabase"); using var app = appBuilder.Build(); diff --git a/tests/Aspire.Hosting.Tests/MySql/AddMySqlTests.cs b/tests/Aspire.Hosting.Tests/MySql/AddMySqlTests.cs index e74f2e177ad..bbeaa6974e0 100644 --- a/tests/Aspire.Hosting.Tests/MySql/AddMySqlTests.cs +++ b/tests/Aspire.Hosting.Tests/MySql/AddMySqlTests.cs @@ -98,13 +98,7 @@ public void MySqlCreatesConnectionString() { var appBuilder = DistributedApplication.CreateBuilder(); appBuilder.AddMySql("mysql") - .WithAnnotation( - new AllocatedEndpointAnnotation(MySqlServerResource.PrimaryEndpointName, - ProtocolType.Tcp, - "localhost", - 2000, - "https" - )); + .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000)); using var app = appBuilder.Build(); @@ -122,13 +116,7 @@ public void MySqlCreatesConnectionStringWithDatabase() { var appBuilder = DistributedApplication.CreateBuilder(); appBuilder.AddMySql("mysql") - .WithAnnotation( - new AllocatedEndpointAnnotation(MySqlServerResource.PrimaryEndpointName, - ProtocolType.Tcp, - "localhost", - 2000, - "https" - )) + .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000)) .AddDatabase("db"); using var app = appBuilder.Build(); @@ -212,7 +200,7 @@ public async Task SingleMySqlInstanceProducesCorrectMySqlHostsVariable() using var app = builder.Build(); // Add fake allocated endpoints. - mysql.WithAnnotation(new AllocatedEndpointAnnotation(MySqlServerResource.PrimaryEndpointName, ProtocolType.Tcp, "host.docker.internal", 5001, "tcp")); + mysql.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "host.docker.internal", 5001)); var model = app.Services.GetRequiredService(); var hook = new PhpMyAdminConfigWriterHook(); @@ -248,8 +236,8 @@ public void WithPhpMyAdminProducesValidServerConfigFile() var mysql2 = builder.AddMySql("mysql2").WithPhpMyAdmin(8081); // Add fake allocated endpoints. - mysql1.WithAnnotation(new AllocatedEndpointAnnotation(MySqlServerResource.PrimaryEndpointName, ProtocolType.Tcp, "host.docker.internal", 5001, "tcp")); - mysql2.WithAnnotation(new AllocatedEndpointAnnotation(MySqlServerResource.PrimaryEndpointName, ProtocolType.Tcp, "host.docker.internal", 5002, "tcp")); + mysql1.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "host.docker.internal", 5001)); + mysql2.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "host.docker.internal", 5002)); var myAdmin = builder.Resources.Single(r => r.Name.EndsWith("-phpmyadmin")); var volume = myAdmin.Annotations.OfType().Single(); diff --git a/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs b/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs index 5f18ff60037..224ed1ecddc 100644 --- a/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs +++ b/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs @@ -96,13 +96,7 @@ public void OracleCreatesConnectionString() { var appBuilder = DistributedApplication.CreateBuilder(); appBuilder.AddOracle("orcl") - .WithAnnotation( - new AllocatedEndpointAnnotation(OracleDatabaseServerResource.PrimaryEndpointName, - ProtocolType.Tcp, - "localhost", - 2000, - "https" - )); + .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000)); using var app = appBuilder.Build(); @@ -121,13 +115,7 @@ public void OracleCreatesConnectionStringWithDatabase() { var appBuilder = DistributedApplication.CreateBuilder(); appBuilder.AddOracle("orcl") - .WithAnnotation( - new AllocatedEndpointAnnotation(OracleDatabaseServerResource.PrimaryEndpointName, - ProtocolType.Tcp, - "localhost", - 2000, - "https" - )) + .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000)) .AddDatabase("db"); using var app = appBuilder.Build(); diff --git a/tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs b/tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs index 7d4eb495c71..e846bb31d59 100644 --- a/tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs +++ b/tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs @@ -118,13 +118,7 @@ public void PostgresCreatesConnectionString() { var appBuilder = DistributedApplication.CreateBuilder(); var postgres = appBuilder.AddPostgres("postgres") - .WithAnnotation( - new AllocatedEndpointAnnotation(PostgresServerResource.PrimaryEndpointName, - ProtocolType.Tcp, - "localhost", - 2000, - "https" - )); + .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000)); var connectionString = postgres.Resource.GetConnectionString(); Assert.Equal("Host={postgres.bindings.tcp.host};Port={postgres.bindings.tcp.port};Username=postgres;Password={postgres.inputs.password}", postgres.Resource.ConnectionStringExpression); @@ -136,13 +130,7 @@ public void PostgresCreatesConnectionStringWithDatabase() { var appBuilder = DistributedApplication.CreateBuilder(); appBuilder.AddPostgres("postgres") - .WithAnnotation( - new AllocatedEndpointAnnotation(PostgresServerResource.PrimaryEndpointName, - ProtocolType.Tcp, - "localhost", - 2000, - "https" - )) + .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000)) .AddDatabase("db"); using var app = appBuilder.Build(); @@ -292,8 +280,8 @@ public void WithPostgresProducesValidServersJsonFile() var pg2 = builder.AddPostgres("mypostgres2").WithPgAdmin(8081); // Add fake allocated endpoints. - pg1.WithAnnotation(new AllocatedEndpointAnnotation("tcp", ProtocolType.Tcp, "host.docker.internal", 5001, "tcp")); - pg2.WithAnnotation(new AllocatedEndpointAnnotation("tcp", ProtocolType.Tcp, "host.docker.internal", 5002, "tcp")); + pg1.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "host.docker.internal", 5001)); + pg2.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "host.docker.internal", 5002)); var pgadmin = builder.Resources.Single(r => r.Name.EndsWith("-pgadmin")); var volume = pgadmin.Annotations.OfType().Single(); diff --git a/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs b/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs index 3d31417ef38..6947fde7d3a 100644 --- a/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs +++ b/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs @@ -48,13 +48,7 @@ public void RabbitMQCreatesConnectionString() var appBuilder = DistributedApplication.CreateBuilder(); appBuilder .AddRabbitMQ("rabbit") - .WithAnnotation( - new AllocatedEndpointAnnotation(RabbitMQServerResource.PrimaryEndpointName, - ProtocolType.Tcp, - "localhost", - 27011, - "tcp" - )); + .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 27011)); using var app = appBuilder.Build(); diff --git a/tests/Aspire.Hosting.Tests/Redis/AddRedisTests.cs b/tests/Aspire.Hosting.Tests/Redis/AddRedisTests.cs index c0c536e972b..751eea4fd5b 100644 --- a/tests/Aspire.Hosting.Tests/Redis/AddRedisTests.cs +++ b/tests/Aspire.Hosting.Tests/Redis/AddRedisTests.cs @@ -79,13 +79,7 @@ public void RedisCreatesConnectionString() { var appBuilder = DistributedApplication.CreateBuilder(); appBuilder.AddRedis("myRedis") - .WithAnnotation( - new AllocatedEndpointAnnotation(RedisResource.PrimaryEndpointName, - ProtocolType.Tcp, - "localhost", - 2000, - "tcp" - )); + .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000)); using var app = appBuilder.Build(); @@ -141,7 +135,7 @@ public async Task SingleRedisInstanceProducesCorrectRedisHostsVariable() using var app = builder.Build(); // Add fake allocated endpoints. - redis.WithAnnotation(new AllocatedEndpointAnnotation("tcp", ProtocolType.Tcp, "host.docker.internal", 5001, "tcp")); + redis.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "host.docker.internal", 5001)); var model = app.Services.GetRequiredService(); var hook = new RedisCommanderConfigWriterHook(); @@ -163,8 +157,8 @@ public async Task MultipleRedisInstanceProducesCorrectRedisHostsVariable() using var app = builder.Build(); // Add fake allocated endpoints. - redis1.WithAnnotation(new AllocatedEndpointAnnotation("tcp", ProtocolType.Tcp, "host.docker.internal", 5001, "tcp")); - redis2.WithAnnotation(new AllocatedEndpointAnnotation("tcp", ProtocolType.Tcp, "host.docker.internal", 5002, "tcp")); + redis1.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "host.docker.internal", 5001)); + redis2.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "host.docker.internal", 5002)); var model = app.Services.GetRequiredService(); var hook = new RedisCommanderConfigWriterHook(); diff --git a/tests/Aspire.Hosting.Tests/SlimTestProgramTests.cs b/tests/Aspire.Hosting.Tests/SlimTestProgramTests.cs index 0c06778ebdd..9560e682085 100644 --- a/tests/Aspire.Hosting.Tests/SlimTestProgramTests.cs +++ b/tests/Aspire.Hosting.Tests/SlimTestProgramTests.cs @@ -46,9 +46,8 @@ public async Task TestPortOnEndpointAnnotationAndAllocatedEndpointAnnotationMatc foreach (var projectBuilders in testProgram.ServiceProjectBuilders) { var endpoint = projectBuilders.Resource.Annotations.OfType().Single(); - var allocatedEndpoint = projectBuilders.Resource.Annotations.OfType().Single(); - - Assert.Equal(endpoint.Port, allocatedEndpoint.Port); + Assert.NotNull(endpoint.AllocatedEndpoint); + Assert.Equal(endpoint.Port, endpoint.AllocatedEndpoint.Port); } } @@ -68,9 +67,8 @@ public async Task TestPortOnEndpointAnnotationAndAllocatedEndpointAnnotationMatc foreach (var projectBuilders in testProgram.ServiceProjectBuilders) { var endpoint = projectBuilders.Resource.Annotations.OfType().Single(); - var allocatedEndpoint = projectBuilders.Resource.Annotations.OfType().Single(); - - Assert.Equal(endpoint.Port, allocatedEndpoint.Port); + Assert.NotNull(endpoint.AllocatedEndpoint); + Assert.Equal(endpoint.Port, endpoint.AllocatedEndpoint.Port); } } } diff --git a/tests/Aspire.Hosting.Tests/SqlServer/AddSqlServerTests.cs b/tests/Aspire.Hosting.Tests/SqlServer/AddSqlServerTests.cs index 990950bae0b..a0aaed675da 100644 --- a/tests/Aspire.Hosting.Tests/SqlServer/AddSqlServerTests.cs +++ b/tests/Aspire.Hosting.Tests/SqlServer/AddSqlServerTests.cs @@ -64,13 +64,7 @@ public void SqlServerCreatesConnectionString() var appBuilder = DistributedApplication.CreateBuilder(); appBuilder .AddSqlServer("sqlserver") - .WithAnnotation( - new AllocatedEndpointAnnotation(SqlServerServerResource.PrimaryEndpointName, - ProtocolType.Tcp, - "localhost", - 1433, - "tcp" - )); + .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 1433)); using var app = appBuilder.Build(); @@ -90,13 +84,8 @@ public void SqlServerDatabaseCreatesConnectionString() var appBuilder = DistributedApplication.CreateBuilder(); appBuilder .AddSqlServer("sqlserver") - .WithAnnotation( - new AllocatedEndpointAnnotation(SqlServerServerResource.PrimaryEndpointName, - ProtocolType.Tcp, - "localhost", - 1433, - "tcp" - )).AddDatabase("mydb"); + .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 1433)) + .AddDatabase("mydb"); using var app = appBuilder.Build(); diff --git a/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs b/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs index f4d936f96a5..9a185e06d75 100644 --- a/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs +++ b/tests/Aspire.Hosting.Tests/WithEnvironmentTests.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.Net.Sockets; using Aspire.Hosting.Tests.Utils; using Xunit; @@ -16,13 +15,9 @@ public async Task EnvironmentReferencingEndpointPopulatesWithBindingUrl() // Create a binding and its metching annotation (simulating DCP behavior) testProgram.ServiceABuilder.WithHttpsEndpoint(1000, 2000, "mybinding"); - testProgram.ServiceABuilder.WithAnnotation( - new AllocatedEndpointAnnotation("mybinding", - ProtocolType.Tcp, - "localhost", - 2000, - "https" - )); + testProgram.ServiceABuilder.WithEndpoint( + "mybinding", + e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000)); testProgram.ServiceBBuilder.WithEnvironment("myName", testProgram.ServiceABuilder.GetEndpoint("mybinding")); diff --git a/tests/Aspire.Hosting.Tests/WithReferenceTests.cs b/tests/Aspire.Hosting.Tests/WithReferenceTests.cs index 97c9e9f7131..95201b5a290 100644 --- a/tests/Aspire.Hosting.Tests/WithReferenceTests.cs +++ b/tests/Aspire.Hosting.Tests/WithReferenceTests.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.Net.Sockets; using Aspire.Hosting.Tests.Utils; using Xunit; @@ -16,15 +15,9 @@ public async Task ResourceWithSingleEndpointProducesSimplifiedEnvironmentVariabl { using var testProgram = CreateTestProgram(); - // Create a binding and its metching annotation (simulating DCP behavior) + // Create a binding and its matching annotation (simulating DCP behavior) testProgram.ServiceABuilder.WithHttpsEndpoint(1000, 2000, "mybinding"); - testProgram.ServiceABuilder.WithAnnotation( - new AllocatedEndpointAnnotation("mybinding", - ProtocolType.Tcp, - "localhost", - 2000, - "https" - )); + testProgram.ServiceABuilder.WithEndpoint("mybinding", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000)); // Get the service provider. testProgram.ServiceBBuilder.WithReference(testProgram.ServiceABuilder.GetEndpoint(endpointName)); @@ -34,9 +27,8 @@ public async Task ResourceWithSingleEndpointProducesSimplifiedEnvironmentVariabl var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(testProgram.ServiceBBuilder.Resource); var servicesKeysCount = config.Keys.Count(k => k.StartsWith("services__")); - Assert.Equal(2, servicesKeysCount); - Assert.Contains(config, kvp => kvp.Key == "services__servicea__0" && kvp.Value == "mybinding://localhost:2000"); - Assert.Contains(config, kvp => kvp.Key == "services__servicea__1" && kvp.Value == "https://localhost:2000"); + Assert.Equal(1, servicesKeysCount); + Assert.Contains(config, kvp => kvp.Key == "services__servicea__mybinding__0" && kvp.Value == "https://localhost:2000"); } [Fact] @@ -46,25 +38,12 @@ public async Task ResourceWithConflictingEndpointsProducesFullyScopedEnvironment // Create a binding and its matching annotation (simulating DCP behavior) testProgram.ServiceABuilder.WithHttpsEndpoint(1000, 2000, "mybinding"); - testProgram.ServiceABuilder.WithAnnotation( - new AllocatedEndpointAnnotation("mybinding", - ProtocolType.Tcp, - "localhost", - 2000, - "https" - )); + testProgram.ServiceABuilder.WithEndpoint("mybinding", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000)); // Create a binding and its matching annotation (simulating DCP behavior) - HOWEVER // this binding conflicts with the earlier because they have the same scheme. testProgram.ServiceABuilder.WithHttpsEndpoint(1000, 3000, "myconflictingbinding"); - testProgram.ServiceABuilder.WithAnnotation( - new AllocatedEndpointAnnotation("myconflictingbinding", - ProtocolType.Tcp, - "localhost", - 3000, - "https" - )); - + testProgram.ServiceABuilder.WithEndpoint("myconflictingbinding", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 3000)); testProgram.ServiceBBuilder.WithReference(testProgram.ServiceABuilder.GetEndpoint("mybinding")); testProgram.ServiceBBuilder.WithReference(testProgram.ServiceABuilder.GetEndpoint("myconflictingbinding")); @@ -76,8 +55,8 @@ public async Task ResourceWithConflictingEndpointsProducesFullyScopedEnvironment var servicesKeysCount = config.Keys.Count(k => k.StartsWith("services__")); Assert.Equal(2, servicesKeysCount); - Assert.Contains(config, kvp => kvp.Key == "services__servicea__0" && kvp.Value == "mybinding://localhost:2000"); - Assert.Contains(config, kvp => kvp.Key == "services__servicea__1" && kvp.Value == "myconflictingbinding://localhost:3000"); + Assert.Contains(config, kvp => kvp.Key == "services__servicea__mybinding__0" && kvp.Value == "https://localhost:2000"); + Assert.Contains(config, kvp => kvp.Key == "services__servicea__myconflictingbinding__0" && kvp.Value == "https://localhost:3000"); } [Fact] @@ -87,24 +66,12 @@ public async Task ResourceWithNonConflictingEndpointsProducesAllVariantsOfEnviro // Create a binding and its matching annotation (simulating DCP behavior) testProgram.ServiceABuilder.WithHttpsEndpoint(1000, 2000, "mybinding"); - testProgram.ServiceABuilder.WithAnnotation( - new AllocatedEndpointAnnotation("mybinding", - ProtocolType.Tcp, - "localhost", - 2000, - "https" - )); + testProgram.ServiceABuilder.WithEndpoint("mybinding", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000)); // Create a binding and its matching annotation (simulating DCP behavior) - not // conflicting because the scheme is different to the first binding. testProgram.ServiceABuilder.WithHttpEndpoint(1000, 3000, "mynonconflictingbinding"); - testProgram.ServiceABuilder.WithAnnotation( - new AllocatedEndpointAnnotation("mynonconflictingbinding", - ProtocolType.Tcp, - "localhost", - 3000, - "http" - )); + testProgram.ServiceABuilder.WithEndpoint("mynonconflictingbinding", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 3000)); testProgram.ServiceBBuilder.WithReference(testProgram.ServiceABuilder.GetEndpoint("mybinding")); testProgram.ServiceBBuilder.WithReference(testProgram.ServiceABuilder.GetEndpoint("mynonconflictingbinding")); @@ -116,11 +83,9 @@ public async Task ResourceWithNonConflictingEndpointsProducesAllVariantsOfEnviro var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(testProgram.ServiceBBuilder.Resource); var servicesKeysCount = config.Keys.Count(k => k.StartsWith("services__")); - Assert.Equal(4, servicesKeysCount); - Assert.Contains(config, kvp => kvp.Key == "services__servicea__0" && kvp.Value == "mybinding://localhost:2000"); - Assert.Contains(config, kvp => kvp.Key == "services__servicea__1" && kvp.Value == "https://localhost:2000"); - Assert.Contains(config, kvp => kvp.Key == "services__servicea__2" && kvp.Value == "mynonconflictingbinding://localhost:3000"); - Assert.Contains(config, kvp => kvp.Key == "services__servicea__3" && kvp.Value == "http://localhost:3000"); + Assert.Equal(2, servicesKeysCount); + Assert.Contains(config, kvp => kvp.Key == "services__servicea__mybinding__0" && kvp.Value == "https://localhost:2000"); + Assert.Contains(config, kvp => kvp.Key == "services__servicea__mynonconflictingbinding__0" && kvp.Value == "http://localhost:3000"); } [Fact] @@ -128,24 +93,15 @@ public async Task ResourceWithConflictingEndpointsProducesAllEnvironmentVariable { using var testProgram = CreateTestProgram(); - // Create a binding and its metching annotation (simulating DCP behavior) + // Create a binding and its matching annotation (simulating DCP behavior) testProgram.ServiceABuilder.WithHttpsEndpoint(1000, 2000, "mybinding"); - testProgram.ServiceABuilder.WithAnnotation( - new AllocatedEndpointAnnotation("mybinding", - ProtocolType.Tcp, - "localhost", - 2000, - "https" - )); + testProgram.ServiceABuilder.WithEndpoint("mybinding", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000)); testProgram.ServiceABuilder.WithHttpsEndpoint(1000, 3000, "mybinding2"); - testProgram.ServiceABuilder.WithAnnotation( - new AllocatedEndpointAnnotation("mybinding2", - ProtocolType.Tcp, - "localhost", - 3000, - "https" - )); + testProgram.ServiceABuilder.WithEndpoint("mybinding2", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 3000)); + + // The launch profile adds an "http" endpoint + testProgram.ServiceABuilder.WithEndpoint("http", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 4000)); // Get the service provider. testProgram.ServiceBBuilder.WithReference(testProgram.ServiceABuilder); @@ -155,9 +111,10 @@ public async Task ResourceWithConflictingEndpointsProducesAllEnvironmentVariable var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(testProgram.ServiceBBuilder.Resource); var servicesKeysCount = config.Keys.Count(k => k.StartsWith("services__")); - Assert.Equal(2, servicesKeysCount); - Assert.Contains(config, kvp => kvp.Key == "services__servicea__0" && kvp.Value == "mybinding://localhost:2000"); - Assert.Contains(config, kvp => kvp.Key == "services__servicea__1" && kvp.Value == "mybinding2://localhost:3000"); + Assert.Equal(3, servicesKeysCount); + Assert.Contains(config, kvp => kvp.Key == "services__servicea__mybinding__0" && kvp.Value == "https://localhost:2000"); + Assert.Contains(config, kvp => kvp.Key == "services__servicea__mybinding2__0" && kvp.Value == "https://localhost:3000"); + Assert.Contains(config, kvp => kvp.Key == "services__servicea__http__0" && kvp.Value == "http://localhost:4000"); } [Fact] @@ -167,22 +124,13 @@ public async Task ResourceWithEndpointsProducesAllEnvironmentVariables() // Create a binding and its metching annotation (simulating DCP behavior) testProgram.ServiceABuilder.WithHttpsEndpoint(1000, 2000, "mybinding"); - testProgram.ServiceABuilder.WithAnnotation( - new AllocatedEndpointAnnotation("mybinding", - ProtocolType.Tcp, - "localhost", - 2000, - "https" - )); + testProgram.ServiceABuilder.WithEndpoint("mybinding", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000)); testProgram.ServiceABuilder.WithHttpEndpoint(1000, 3000, "mybinding2"); - testProgram.ServiceABuilder.WithAnnotation( - new AllocatedEndpointAnnotation("mybinding2", - ProtocolType.Tcp, - "localhost", - 3000, - "http" - )); + testProgram.ServiceABuilder.WithEndpoint("mybinding2", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 3000)); + + // The launch profile adds an "http" endpoint + testProgram.ServiceABuilder.WithEndpoint("http", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 4000)); // Get the service provider. testProgram.ServiceBBuilder.WithReference(testProgram.ServiceABuilder); @@ -192,11 +140,10 @@ public async Task ResourceWithEndpointsProducesAllEnvironmentVariables() var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(testProgram.ServiceBBuilder.Resource); var servicesKeysCount = config.Keys.Count(k => k.StartsWith("services__")); - Assert.Equal(4, servicesKeysCount); - Assert.Contains(config, kvp => kvp.Key == "services__servicea__0" && kvp.Value == "mybinding://localhost:2000"); - Assert.Contains(config, kvp => kvp.Key == "services__servicea__1" && kvp.Value == "https://localhost:2000"); - Assert.Contains(config, kvp => kvp.Key == "services__servicea__2" && kvp.Value == "mybinding2://localhost:3000"); - Assert.Contains(config, kvp => kvp.Key == "services__servicea__3" && kvp.Value == "http://localhost:3000"); + Assert.Equal(3, servicesKeysCount); + Assert.Contains(config, kvp => kvp.Key == "services__servicea__mybinding__0" && kvp.Value == "https://localhost:2000"); + Assert.Contains(config, kvp => kvp.Key == "services__servicea__mybinding2__0" && kvp.Value == "http://localhost:3000"); + Assert.Contains(config, kvp => kvp.Key == "services__servicea__http__0" && kvp.Value == "http://localhost:4000"); } [Fact] diff --git a/tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs b/tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs index bd69969199d..7e2ef478ef7 100644 --- a/tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs +++ b/tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs @@ -164,8 +164,8 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo { InitialData = new Dictionary { - ["services:basket:0"] = "localhost:8080", - ["services:basket:1"] = "remotehost:9090", + ["services:basket:http:0"] = "localhost:8080", + ["services:basket:http:1"] = "remotehost:9090", } }; var config = new ConfigurationBuilder().Add(configSource); diff --git a/tests/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs b/tests/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs index 66bc1ab8d1b..f35ffa2026c 100644 --- a/tests/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs +++ b/tests/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs @@ -22,7 +22,7 @@ public async Task ResolveServiceEndPoint_Configuration_SingleResult() { var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary { - ["services:basket"] = "localhost:8080", + ["services:basket:http"] = "localhost:8080", }); var services = new ServiceCollection() .AddSingleton(config.Build()) @@ -52,6 +52,72 @@ public async Task ResolveServiceEndPoint_Configuration_SingleResult() } } + [Fact] + public async Task ResolveServiceEndPoint_Configuration_DisallowedScheme() + { + // Try to resolve an http endpoint when only https is allowed. + var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary + { + ["services:basket:foo:0"] = "http://localhost:8080", + ["services:basket:foo:1"] = "https://localhost", + }); + var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .AddConfigurationServiceEndPointResolver() + .Configure(o => o.AllowedSchemes = ["https"]) + .BuildServiceProvider(); + var resolverFactory = services.GetRequiredService(); + ServiceEndPointWatcher resolver; + + // Explicitly specifying http. + // We should get no endpoint back because http is not allowed by configuration. + await using ((resolver = resolverFactory.CreateResolver("http://_foo.basket")).ConfigureAwait(false)) + { + Assert.NotNull(resolver); + var tcs = new TaskCompletionSource(); + resolver.OnEndPointsUpdated = tcs.SetResult; + resolver.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + Assert.Equal(ResolutionStatus.Success, initialResult.Status); + Assert.Empty(initialResult.EndPoints); + } + + // Specifying either https or http. + // The result should be that we only get the http endpoint back. + await using ((resolver = resolverFactory.CreateResolver("https+http://_foo.basket")).ConfigureAwait(false)) + { + Assert.NotNull(resolver); + var tcs = new TaskCompletionSource(); + resolver.OnEndPointsUpdated = tcs.SetResult; + resolver.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + Assert.Equal(ResolutionStatus.Success, initialResult.Status); + var ep = Assert.Single(initialResult.EndPoints); + Assert.Equal(new UriEndPoint(new Uri("https://localhost")), ep.EndPoint); + } + + // Specifying either https or http, but in reverse. + // The result should be that we only get the http endpoint back. + await using ((resolver = resolverFactory.CreateResolver("http+https://_foo.basket")).ConfigureAwait(false)) + { + Assert.NotNull(resolver); + var tcs = new TaskCompletionSource(); + resolver.OnEndPointsUpdated = tcs.SetResult; + resolver.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + Assert.Equal(ResolutionStatus.Success, initialResult.Status); + var ep = Assert.Single(initialResult.EndPoints); + Assert.Equal(new UriEndPoint(new Uri("https://localhost")), ep.EndPoint); + } + } + [Fact] public async Task ResolveServiceEndPoint_Configuration_MultipleResults() { @@ -59,8 +125,8 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() { InitialData = new Dictionary { - ["services:basket:0"] = "http://localhost:8080", - ["services:basket:1"] = "http://remotehost:9090", + ["services:basket:http:0"] = "http://localhost:8080", + ["services:basket:http:1"] = "http://remotehost:9090", } }; var config = new ConfigurationBuilder().Add(configSource); @@ -82,8 +148,31 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() Assert.True(initialResult.ResolvedSuccessfully); Assert.Equal(ResolutionStatus.Success, initialResult.Status); Assert.Equal(2, initialResult.EndPoints.Count); - Assert.Equal(new DnsEndPoint("localhost", 8080), initialResult.EndPoints[0].EndPoint); - Assert.Equal(new DnsEndPoint("remotehost", 9090), initialResult.EndPoints[1].EndPoint); + Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPoints[0].EndPoint); + Assert.Equal(new UriEndPoint(new Uri("http://remotehost:9090")), initialResult.EndPoints[1].EndPoint); + + Assert.All(initialResult.EndPoints, ep => + { + var hostNameFeature = ep.Features.Get(); + Assert.NotNull(hostNameFeature); + Assert.Equal("basket", hostNameFeature.HostName); + }); + } + + // Request either https or http. Since there are only http endpoints, we should get only http endpoints back. + await using ((resolver = resolverFactory.CreateResolver("https+http://basket")).ConfigureAwait(false)) + { + Assert.NotNull(resolver); + var tcs = new TaskCompletionSource(); + resolver.OnEndPointsUpdated = tcs.SetResult; + resolver.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + Assert.Equal(ResolutionStatus.Success, initialResult.Status); + Assert.Equal(2, initialResult.EndPoints.Count); + Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPoints[0].EndPoint); + Assert.Equal(new UriEndPoint(new Uri("http://remotehost:9090")), initialResult.EndPoints[1].EndPoint); Assert.All(initialResult.EndPoints, ep => { @@ -101,10 +190,12 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols() { InitialData = new Dictionary { - ["services:basket:0"] = "http://localhost:8080", - ["services:basket:1"] = "http://remotehost:9090", - ["services:basket:2"] = "http://_grpc.localhost:2222", - ["services:basket:3"] = "grpc://remotehost:2222", + ["services:basket:http:0"] = "http://localhost:8080", + ["services:basket:https:1"] = "https://remotehost:9090", + ["services:basket:grpc:0"] = "localhost:2222", + ["services:basket:grpc:1"] = "127.0.0.1:3333", + ["services:basket:grpc:2"] = "http://remotehost:4444", + ["services:basket:grpc:3"] = "https://remotehost:5555", } }; var config = new ConfigurationBuilder().Add(configSource); @@ -125,9 +216,60 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols() Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); Assert.Equal(ResolutionStatus.Success, initialResult.Status); - Assert.Equal(2, initialResult.EndPoints.Count); + Assert.Equal(3, initialResult.EndPoints.Count); Assert.Equal(new DnsEndPoint("localhost", 2222), initialResult.EndPoints[0].EndPoint); - Assert.Equal(new DnsEndPoint("remotehost", 2222), initialResult.EndPoints[1].EndPoint); + Assert.Equal(new IPEndPoint(IPAddress.Loopback, 3333), initialResult.EndPoints[1].EndPoint); + Assert.Equal(new UriEndPoint(new Uri("http://remotehost:4444")), initialResult.EndPoints[2].EndPoint); + + Assert.All(initialResult.EndPoints, ep => + { + var hostNameFeature = ep.Features.Get(); + Assert.Null(hostNameFeature); + }); + } + } + + [Fact] + public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols_WithSpecificationByConsumer() + { + var configSource = new MemoryConfigurationSource + { + InitialData = new Dictionary + { + ["services:basket:default:0"] = "http://localhost:8080", + ["services:basket:default:1"] = "remotehost:9090", + ["services:basket:grpc:0"] = "localhost:2222", + ["services:basket:grpc:1"] = "127.0.0.1:3333", + ["services:basket:grpc:2"] = "http://remotehost:4444", + ["services:basket:grpc:3"] = "https://remotehost:5555", + } + }; + var config = new ConfigurationBuilder().Add(configSource); + var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .AddConfigurationServiceEndPointResolver() + .BuildServiceProvider(); + var resolverFactory = services.GetRequiredService(); + ServiceEndPointWatcher resolver; + await using ((resolver = resolverFactory.CreateResolver("https+http://_grpc.basket")).ConfigureAwait(false)) + { + Assert.NotNull(resolver); + var tcs = new TaskCompletionSource(); + resolver.OnEndPointsUpdated = tcs.SetResult; + resolver.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + Assert.Equal(ResolutionStatus.Success, initialResult.Status); + Assert.Equal(3, initialResult.EndPoints.Count); + + // These must be treated as HTTPS by the HttpClient middleware, but that is not the responsibility of the resolver. + Assert.Equal(new DnsEndPoint("localhost", 2222), initialResult.EndPoints[0].EndPoint); + Assert.Equal(new IPEndPoint(IPAddress.Loopback, 3333), initialResult.EndPoints[1].EndPoint); + + // We expect the HTTPS endpoint back but not the HTTP one. + Assert.Equal(new UriEndPoint(new Uri("https://remotehost:5555")), initialResult.EndPoints[2].EndPoint); Assert.All(initialResult.EndPoints, ep => { diff --git a/tests/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs b/tests/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs index 696061949d9..d8adcbca529 100644 --- a/tests/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs +++ b/tests/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs @@ -49,7 +49,7 @@ public async Task ResolveServiceEndPoint_Superseded() { InitialData = new Dictionary { - ["services:basket:0"] = "http://localhost:8080", + ["services:basket:http:0"] = "http://localhost:8080", } }; var config = new ConfigurationBuilder().Add(configSource); @@ -72,7 +72,7 @@ public async Task ResolveServiceEndPoint_Superseded() // We expect the basket service to be resolved from Configuration, not the pass-through provider. Assert.Single(initialResult.EndPoints); - Assert.Equal(new DnsEndPoint("localhost", 8080), initialResult.EndPoints[0].EndPoint); + Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPoints[0].EndPoint); } } @@ -83,7 +83,7 @@ public async Task ResolveServiceEndPoint_Fallback() { InitialData = new Dictionary { - ["services:basket:0"] = "http://localhost:8080", + ["services:basket:default:0"] = "http://localhost:8080", } }; var config = new ConfigurationBuilder().Add(configSource); @@ -118,7 +118,7 @@ public async Task ResolveServiceEndPoint_Fallback_NoScheme() { InitialData = new Dictionary { - ["services:basket:0"] = "http://localhost:8080", + ["services:basket:default:0"] = "http://localhost:8080", } }; var config = new ConfigurationBuilder().Add(configSource); diff --git a/tests/testproject/TestProject.AppHost/TestProgram.cs b/tests/testproject/TestProject.AppHost/TestProgram.cs index 8fefdf93f68..9efd9b56e08 100644 --- a/tests/testproject/TestProject.AppHost/TestProgram.cs +++ b/tests/testproject/TestProject.AppHost/TestProgram.cs @@ -37,9 +37,9 @@ private TestProgram(string[] args, Assembly assembly, bool includeIntegrationSer var serviceAPath = Path.Combine(Projects.TestProject_AppHost.ProjectPath, @"..\TestProject.ServiceA\TestProject.ServiceA.csproj"); - ServiceABuilder = AppBuilder.AddProject("servicea", serviceAPath); - ServiceBBuilder = AppBuilder.AddProject("serviceb"); - ServiceCBuilder = AppBuilder.AddProject("servicec"); + ServiceABuilder = AppBuilder.AddProject("servicea", serviceAPath, launchProfileName: "http"); + ServiceBBuilder = AppBuilder.AddProject("serviceb", launchProfileName: "http"); + ServiceCBuilder = AppBuilder.AddProject("servicec", launchProfileName: "http"); WorkerABuilder = AppBuilder.AddProject("workera"); if (includeNodeApp) @@ -158,7 +158,7 @@ public DistributedApplication Build() public void Dispose() => App?.Dispose(); /// - /// Writes the allocated endpoints to the console in JSON format. + /// Writes the allocatedEndpoint endpoints to the console in JSON format. /// This allows for easier consumption by the external test process. /// private sealed class EndPointWriterHook : IDistributedApplicationLifecycleHook @@ -174,11 +174,19 @@ public async Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appMo var endpointsJsonArray = new JsonArray(); projectJson["Endpoints"] = endpointsJsonArray; - foreach (var endpoint in project.Annotations.OfType()) + foreach (var endpoint in project.Annotations.OfType()) { - var endpointJsonObject = new JsonObject(); - endpointJsonObject["Name"] = endpoint.Name; - endpointJsonObject["Uri"] = endpoint.UriString; + var allocatedEndpoint = endpoint.AllocatedEndpoint; + if (allocatedEndpoint is null) + { + continue; + } + + var endpointJsonObject = new JsonObject + { + ["Name"] = endpoint.Name, + ["Uri"] = allocatedEndpoint.UriString + }; endpointsJsonArray.Add(endpointJsonObject); } } From 1f38210a50c9aa92be1ba9dc15a9cd87fca6ab15 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Fri, 8 Mar 2024 17:31:08 -0800 Subject: [PATCH 22/50] Write details of container volumes to the publishing manifest (#2742) * Write container volume details to publishing manifest * Add a test for writing volumes to the manifest --- .../AppHost/Properties/launchSettings.json | 5 +-- .../Publishing/ManifestPublishingContext.cs | 35 ++++++++++++++++ .../ManifestGenerationTests.cs | 41 +++++++++++++++++++ 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/playground/TestShop/AppHost/Properties/launchSettings.json b/playground/TestShop/AppHost/Properties/launchSettings.json index f607729fb5b..df947e1f104 100644 --- a/playground/TestShop/AppHost/Properties/launchSettings.json +++ b/playground/TestShop/AppHost/Properties/launchSettings.json @@ -14,14 +14,11 @@ }, "generate-manifest": { "commandName": "Project", - "launchBrowser": true, "dotnetRunMessages": true, "commandLineArgs": "--publisher manifest --output-path aspire-manifest.json", - "applicationUrl": "http://localhost:15888", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", - "DOTNET_ENVIRONMENT": "Development", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16031" + "DOTNET_ENVIRONMENT": "Development" } } }, diff --git a/src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs b/src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs index 60a2a97edec..dbf24d981e4 100644 --- a/src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs +++ b/src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs @@ -81,6 +81,7 @@ public async Task WriteContainerAsync(ContainerResource container) Writer.WriteString("entrypoint", container.Entrypoint); } + // Write args if they are present if (container.TryGetAnnotationsOfType(out var argsCallback)) { var args = new List(); @@ -104,6 +105,40 @@ public async Task WriteContainerAsync(ContainerResource container) } } + // Write volume details + if (container.TryGetAnnotationsOfType(out var mounts)) + { + var volumes = mounts.Where(mounts => mounts.Type == ContainerMountType.Named).ToList(); + + // Only write out details for volumes (no bind mounts) + if (volumes.Count > 0) + { + // Volumes are written as an array of objects as anonymous volumes do not have a name + Writer.WriteStartArray("volumes"); + + foreach (var volume in volumes) + { + Writer.WriteStartObject(); + + // This can be null for anonymous volumes + if (volume.Source is not null) + { + Writer.WritePropertyName("name"); + Writer.WriteStringValue(volume.Source); + } + + Writer.WritePropertyName("target"); + Writer.WriteStringValue(volume.Target); + + Writer.WriteBoolean("readOnly", volume.IsReadOnly); + + Writer.WriteEndObject(); + } + + Writer.WriteEndArray(); + } + } + await WriteEnvironmentVariablesAsync(container).ConfigureAwait(false); WriteBindings(container, emitContainerPort: true); WriteInputs(container); diff --git a/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs b/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs index 9f0845c0d84..801806d9a41 100644 --- a/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs +++ b/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs @@ -183,6 +183,47 @@ public void EnsureContainerWithArgsEmitsContainerArgs() arg => Assert.Equal("more", arg.GetString())); } + [Fact] + public async Task EnsureContainerWithVolumesEmitsVolumes() + { + using var program = CreateTestProgramJsonDocumentManifestPublisher(); + + var container = program.AppBuilder.AddContainer("containerwithvolumes", "image/name") + .WithVolume("myvolume", "/mount/here") + .WithBindMount("./some/source", "/bound") // This should be ignored and not written to the manifest + .WithVolume("myreadonlyvolume", "/mount/there", isReadOnly: true) + .WithVolume(null! /* anonymous volume */, "/mount/everywhere"); + + program.Build(); + + var manifest = await ManifestUtils.GetManifest(container.Resource); + + var expectedManifest = """ + { + "type": "container.v0", + "image": "image/name:latest", + "volumes": [ + { + "name": "myvolume", + "target": "/mount/here", + "readOnly": false + }, + { + "name": "myreadonlyvolume", + "target": "/mount/there", + "readOnly": true + }, + { + "target": "/mount/everywhere", + "readOnly": false + } + ] + } + """; + + Assert.Equal(expectedManifest, manifest.ToString()); + } + [Theory] [InlineData(new string[] { "args1", "args2" }, new string[] { "withArgs1", "withArgs2" })] [InlineData(new string[] { }, new string[] { "withArgs1", "withArgs2" })] From e5461637799ffd98253f193de827ce24bcfd25b6 Mon Sep 17 00:00:00 2001 From: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com> Date: Fri, 8 Mar 2024 18:14:29 -0800 Subject: [PATCH 23/50] Add ServiceBus support using CDK (#2741) * Add ServiceBus support using CDK * remove commented code * Add sku parameter and fix tests * Fix sku param and ref docs --- Directory.Packages.props | 2 +- .../CdkSample.ApiService.csproj | 1 + .../cdk/CdkSample.ApiService/Program.cs | 14 ++- playground/cdk/CdkSample.AppHost/Program.cs | 35 +++++- .../CdkSample.AppHost/aspire-manifest.json | 25 +++- .../CdkSample.AppHost/servicebus.module.bicep | 104 ++++++++++++++++ .../AzureServiceBusResource.cs | 45 +++++++ .../Extensions/AzureServiceBusExtensions.cs | 111 +++++++++++++++++- .../Azure/AzureBicepResourceTests.cs | 34 ++++++ 9 files changed, 356 insertions(+), 15 deletions(-) create mode 100644 playground/cdk/CdkSample.AppHost/servicebus.module.bicep diff --git a/Directory.Packages.props b/Directory.Packages.props index 87801159540..0e86f5bddf2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -25,7 +25,7 @@ - + diff --git a/playground/cdk/CdkSample.ApiService/CdkSample.ApiService.csproj b/playground/cdk/CdkSample.ApiService/CdkSample.ApiService.csproj index bd02a6cc98b..5827bb1d34f 100644 --- a/playground/cdk/CdkSample.ApiService/CdkSample.ApiService.csproj +++ b/playground/cdk/CdkSample.ApiService/CdkSample.ApiService.csproj @@ -7,6 +7,7 @@ + diff --git a/playground/cdk/CdkSample.ApiService/Program.cs b/playground/cdk/CdkSample.ApiService/Program.cs index 43561e8bd21..25ecc4a555c 100644 --- a/playground/cdk/CdkSample.ApiService/Program.cs +++ b/playground/cdk/CdkSample.ApiService/Program.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text.Json; +using Azure.Messaging.ServiceBus; using Azure.Security.KeyVault.Secrets; using Azure.Storage.Blobs; using Microsoft.EntityFrameworkCore; @@ -17,10 +18,11 @@ builder.AddRedisClient("cache"); builder.AddCosmosDbContext("cosmos", "cosmosdb"); builder.AddNpgsqlDbContext("pgsqldb"); +builder.AddAzureServiceBusClient("sb"); var app = builder.Build(); -app.MapGet("/", async (BlobServiceClient bsc, SqlContext sqlContext, SecretClient sc, IConnectionMultiplexer connection, CosmosContext cosmosContext, NpgsqlContext npgsqlContext) => +app.MapGet("/", async (BlobServiceClient bsc, SqlContext sqlContext, SecretClient sc, IConnectionMultiplexer connection, CosmosContext cosmosContext, NpgsqlContext npgsqlContext, ServiceBusClient sbc) => { return new { @@ -30,6 +32,7 @@ blobFiles = await TestBlobStorageAsync(bsc), sqlRows = await TestSqlServerAsync(sqlContext), npgsqlRows = await TestNpgsqlAsync(npgsqlContext), + serviceBus = await TestServiceBusAsync(sbc) }; }); app.Run(); @@ -81,6 +84,15 @@ static async Task> TestBlobStorageAsync(BlobServiceClient bsc) return blobNames; } +static async Task TestServiceBusAsync(ServiceBusClient sbc) +{ + await using var sender = sbc.CreateSender("myqueue"); + await sender.SendMessageAsync(new ServiceBusMessage("Hello, World!")); + + await using var receiver = sbc.CreateReceiver("myqueue"); + return await receiver.ReceiveMessageAsync(); +} + static async Task> TestSqlServerAsync(SqlContext context) { await context.Database.EnsureCreatedAsync(); diff --git a/playground/cdk/CdkSample.AppHost/Program.cs b/playground/cdk/CdkSample.AppHost/Program.cs index 578a3fe1c75..c024bd183c6 100644 --- a/playground/cdk/CdkSample.AppHost/Program.cs +++ b/playground/cdk/CdkSample.AppHost/Program.cs @@ -37,13 +37,36 @@ var pgsql2 = builder.AddPostgres("pgsql2").AsAzurePostgresFlexibleServerConstruct(); +var sb = builder.AddAzureServiceBusConstruct("servicebus") + .AddQueue("queue1", + (construct, queue) => + { + queue.Properties.MaxDeliveryCount = 5; + queue.Properties.LockDuration = TimeSpan.FromMinutes(5); + }) + .AddTopic("topic1", + (construct, topic) => + { + topic.Properties.EnablePartitioning = true; + }) + .AddTopic("topic2") + .AddSubscription("topic1", "subscription1", + (construct, subscription) => + { + subscription.Properties.LockDuration = TimeSpan.FromMinutes(5); + subscription.Properties.RequiresSession = true; + }) + .AddSubscription("topic1", "subscription2") + .AddTopic("topic3", new[] { "sub1", "sub2" }); + builder.AddProject("api") - .WithReference(blobs) - .WithReference(sqldb) - .WithReference(keyvault) - .WithReference(cache) - .WithReference(cosmosdb) - .WithReference(pgsqldb); + .WithReference(blobs) + .WithReference(sqldb) + .WithReference(keyvault) + .WithReference(cache) + .WithReference(cosmosdb) + .WithReference(pgsqldb) + .WithReference(sb); // This project is only added in playground projects to support development/debugging // of the dashboard. It is not required in end developer code. Comment out this code diff --git a/playground/cdk/CdkSample.AppHost/aspire-manifest.json b/playground/cdk/CdkSample.AppHost/aspire-manifest.json index 52d29bf99b3..554d2427367 100644 --- a/playground/cdk/CdkSample.AppHost/aspire-manifest.json +++ b/playground/cdk/CdkSample.AppHost/aspire-manifest.json @@ -54,7 +54,10 @@ "secret": true, "default": { "generate": { - "minLength": 10 + "minLength": 22, + "minLower": 1, + "minUpper": 1, + "minNumeric": 1 } } } @@ -127,7 +130,7 @@ "secret": true, "default": { "generate": { - "minLength": 10 + "minLength": 22 } } } @@ -153,7 +156,7 @@ "secret": true, "default": { "generate": { - "minLength": 10 + "minLength": 22 } } }, @@ -161,12 +164,23 @@ "type": "string", "default": { "generate": { - "minLength": 10 + "minLength": 10, + "numeric": false, + "special": false } } } } }, + "servicebus": { + "type": "azure.bicep.v0", + "connectionString": "{servicebus.outputs.serviceBusEndpoint}", + "path": "servicebus.module.bicep", + "params": { + "principalId": "", + "principalType": "" + } + }, "api": { "type": "project.v0", "path": "../CdkSample.ApiService/CdkSample.ApiService.csproj", @@ -178,7 +192,8 @@ "ConnectionStrings__mykv": "{mykv.connectionString}", "ConnectionStrings__cache": "{cache.connectionString}", "ConnectionStrings__cosmos": "{cosmos.connectionString}", - "ConnectionStrings__pgsqldb": "{pgsqldb.connectionString}" + "ConnectionStrings__pgsqldb": "{pgsqldb.connectionString}", + "ConnectionStrings__servicebus": "{servicebus.connectionString}" }, "bindings": { "http": { diff --git a/playground/cdk/CdkSample.AppHost/servicebus.module.bicep b/playground/cdk/CdkSample.AppHost/servicebus.module.bicep new file mode 100644 index 00000000000..60e9e751626 --- /dev/null +++ b/playground/cdk/CdkSample.AppHost/servicebus.module.bicep @@ -0,0 +1,104 @@ +targetScope = 'resourceGroup' + +@description('') +param location string = resourceGroup().location + +@description('') +param sku string = 'Standard' + +@description('') +param principalId string + +@description('') +param principalType string + + +resource serviceBusNamespace_amM9gJ0Ya 'Microsoft.ServiceBus/namespaces@2021-11-01' = { + name: toLower(take(concat('servicebus', uniqueString(resourceGroup().id)), 24)) + location: location + sku: sku + properties: { + minimumTlsVersion: '1.2' + } +} + +resource roleAssignment_O68yhHszw 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: serviceBusNamespace_amM9gJ0Ya + name: guid(serviceBusNamespace_amM9gJ0Ya.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '090c5cfd-751d-490a-894a-3ce6f1109419')) + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '090c5cfd-751d-490a-894a-3ce6f1109419') + principalId: principalId + principalType: principalType + } +} + +resource serviceBusQueue_wJ6B0eQwN 'Microsoft.ServiceBus/namespaces/queues@2021-11-01' = { + parent: serviceBusNamespace_amM9gJ0Ya + name: 'queue1' + location: location + properties: { + lockDuration: 'PT5M' + maxDeliveryCount: 5 + } +} + +resource serviceBusTopic_Rr0YFQpE9 'Microsoft.ServiceBus/namespaces/topics@2021-11-01' = { + parent: serviceBusNamespace_amM9gJ0Ya + name: 'topic1' + location: location + properties: { + enablePartitioning: true + } +} + +resource serviceBusSubscription_SysEikGPG 'Microsoft.ServiceBus/namespaces/topics/subscriptions@2021-11-01' = { + parent: serviceBusTopic_Rr0YFQpE9 + name: 'subscription1' + location: location + properties: { + lockDuration: 'PT5M' + requiresSession: true + } +} + +resource serviceBusSubscription_5hExkZHCU 'Microsoft.ServiceBus/namespaces/topics/subscriptions@2021-11-01' = { + parent: serviceBusTopic_Rr0YFQpE9 + name: 'subscription2' + location: location + properties: { + } +} + +resource serviceBusTopic_cKuAI6Z4Z 'Microsoft.ServiceBus/namespaces/topics@2021-11-01' = { + parent: serviceBusNamespace_amM9gJ0Ya + name: 'topic2' + location: location + properties: { + } +} + +resource serviceBusTopic_cRWE7uNBs 'Microsoft.ServiceBus/namespaces/topics@2021-11-01' = { + parent: serviceBusNamespace_amM9gJ0Ya + name: 'topic3' + location: location + properties: { + } +} + +resource serviceBusSubscription_bhjaa0Rpf 'Microsoft.ServiceBus/namespaces/topics/subscriptions@2021-11-01' = { + parent: serviceBusTopic_cRWE7uNBs + name: 'sub1' + location: location + properties: { + } +} + +resource serviceBusSubscription_l4R4UcHly 'Microsoft.ServiceBus/namespaces/topics/subscriptions@2021-11-01' = { + parent: serviceBusTopic_cRWE7uNBs + name: 'sub2' + location: location + properties: { + } +} + +output serviceBusEndpoint string = serviceBusNamespace_amM9gJ0Ya.properties.serviceBusEndpoint diff --git a/src/Aspire.Hosting.Azure/AzureServiceBusResource.cs b/src/Aspire.Hosting.Azure/AzureServiceBusResource.cs index e8fcd21d439..9e3f8edaf05 100644 --- a/src/Aspire.Hosting.Azure/AzureServiceBusResource.cs +++ b/src/Aspire.Hosting.Azure/AzureServiceBusResource.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Hosting.ApplicationModel; +using Azure.Provisioning.ServiceBus; namespace Aspire.Hosting.Azure; @@ -47,3 +48,47 @@ public class AzureServiceBusResource(string name) : return GetConnectionString(); } } + +/// +/// Represents an Azure Service Bus resource. +/// +/// The name of the resource. +/// Callback to configure the Azure Service Bus resource. +public class AzureServiceBusConstructResource(string name, Action configureConstruct) + : AzureConstructResource(name, configureConstruct), IResourceWithConnectionString +{ + internal List<(string Name, Action? Configure)> Queues { get; } = []; + internal List<(string Name, Action? Configure)> Topics { get; } = []; + internal List<(string TopicName, string Name, Action? Configure)> Subscriptions { get; } = []; + + /// + /// Gets the "serviceBusEndpoint" output reference from the bicep template for the Azure Storage resource. + /// + public BicepOutputReference ServiceBusEndpoint => new("serviceBusEndpoint", this); + + /// + /// Gets the connection string template for the manifest for the Azure Service Bus endpoint. + /// + public string ConnectionStringExpression => ServiceBusEndpoint.ValueExpression; + + /// + /// Gets the connection string for the Azure Service Bus endpoint. + /// + /// The connection string for the Azure Service Bus endpoint. + public string? GetConnectionString() => ServiceBusEndpoint.Value; + + /// + /// Gets the connection string for the Azure Service Bus endpoint. + /// + /// A to observe while waiting for the task to complete. + /// The connection string for the Azure Service Bus endpoint. + public async ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) + { + if (ProvisioningTaskCompletionSource is not null) + { + await ProvisioningTaskCompletionSource.Task.WaitAsync(cancellationToken).ConfigureAwait(false); + } + + return GetConnectionString(); + } +} diff --git a/src/Aspire.Hosting.Azure/Extensions/AzureServiceBusExtensions.cs b/src/Aspire.Hosting.Azure/Extensions/AzureServiceBusExtensions.cs index b8cd8d6d1be..5074180f9d4 100644 --- a/src/Aspire.Hosting.Azure/Extensions/AzureServiceBusExtensions.cs +++ b/src/Aspire.Hosting.Azure/Extensions/AzureServiceBusExtensions.cs @@ -4,6 +4,9 @@ using System.Text.Json.Nodes; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Azure; +using Azure.Provisioning; +using Azure.Provisioning.Authorization; +using Azure.Provisioning.ServiceBus; namespace Aspire.Hosting; @@ -40,7 +43,58 @@ public static IResourceBuilder AddAzureServiceBus(this } /// - /// Adds a Azure Service Bus Queue resource to the application model. This resource requires an to be added to the application model. + /// Adds an Azure Service Bus Namespace resource to the application model. This resource can be used to create queue, topic, and subscription resources. + /// + /// The builder for the distributed application. + /// The name of the resource. + /// Optional callback to configure the Service Bus namespace. + /// + public static IResourceBuilder AddAzureServiceBusConstruct(this IDistributedApplicationBuilder builder, string name, Action? configureResource = null) + { + var configureConstruct = (ResourceModuleConstruct construct) => + { + var serviceBusNamespace = new ServiceBusNamespace(construct, name: name); + + serviceBusNamespace.AssignProperty(p => p.Sku.Name, new Parameter("sku", defaultValue: "Standard")); + + var serviceBusDataOwnerRole = serviceBusNamespace.AssignRole(RoleDefinition.ServiceBusDataOwner); + serviceBusDataOwnerRole.AssignProperty(p => p.PrincipalType, construct.PrincipalTypeParameter); + + serviceBusNamespace.AddOutput(sa => sa.ServiceBusEndpoint, "serviceBusEndpoint"); + + configureResource?.Invoke(construct, serviceBusNamespace); + + var azureResource = (AzureServiceBusConstructResource)construct.Resource; + foreach (var queue in azureResource.Queues) + { + var queueResource = new ServiceBusQueue(construct, name: queue.Name, parent: serviceBusNamespace); + queue.Configure?.Invoke(construct, queueResource); + } + var topicDictionary = new Dictionary(); + foreach (var topic in azureResource.Topics) + { + var topicResource = new ServiceBusTopic(construct, name: topic.Name, parent: serviceBusNamespace); + topicDictionary.Add(topic.Name, topicResource); + topic.Configure?.Invoke(construct, topicResource); + } + foreach (var subscription in azureResource.Subscriptions) + { + var topic = topicDictionary[subscription.TopicName]; + var subscriptionResource = new ServiceBusSubscription(construct, name: subscription.Name, parent: topic); + subscription.Configure?.Invoke(construct, subscriptionResource); + } + }; + var resource = new AzureServiceBusConstructResource(name, configureConstruct); + + return builder.AddResource(resource) + // These ambient parameters are only available in development time. + .WithParameter(AzureBicepResource.KnownParameters.PrincipalId) + .WithParameter(AzureBicepResource.KnownParameters.PrincipalType) + .WithManifestPublishingCallback(resource.WriteToManifest); + } + + /// + /// Adds an Azure Service Bus Queue resource to the application model. This resource requires an to be added to the application model. /// /// The Azure Service Bus resource builder. /// The name of the queue. @@ -52,7 +106,7 @@ public static IResourceBuilder AddQueue(this IResourceB } /// - /// Adds a Azure Service Bus Topic resource to the application model. This resource requires an to be added to the application model. + /// Adds an Azure Service Bus Topic resource to the application model. This resource requires an to be added to the application model. /// /// The Azure Service Bus resource builder. /// The name of the topic. @@ -63,4 +117,57 @@ public static IResourceBuilder AddTopic(this IResourceB return builder; } + + /// + /// Adds an Azure Service Bus Topic resource to the application model. This resource requires an to be added to the application model. + /// + /// The Azure Service Bus resource builder. + /// The name of the topic. + /// The name of the subscriptions. + public static IResourceBuilder AddTopic(this IResourceBuilder builder, string name, string[] subscriptions) + { + builder.Resource.Topics.Add((name, null)); + foreach (var subscription in subscriptions) + { + builder.Resource.Subscriptions.Add((name, subscription, null)); + } + return builder; + } + + /// + /// Adds an Azure Service Bus Queue resource to the application model. This resource requires an to be added to the application model. + /// + /// The Azure Service Bus resource builder. + /// The name of the queue. + /// Optional callback to customize the queue. + public static IResourceBuilder AddQueue(this IResourceBuilder builder, string name, Action? configureQueue = default) + { + builder.Resource.Queues.Add((name, configureQueue)); + return builder; + } + + /// + /// Adds an Azure Service Bus Topic resource to the application model. This resource requires an to be added to the application model. + /// + /// The Azure Service Bus resource builder. + /// The name of the topic. + /// Optional callback to customize the topic. + public static IResourceBuilder AddTopic(this IResourceBuilder builder, string name, Action? configureTopic = default) + { + builder.Resource.Topics.Add((name, configureTopic)); + return builder; + } + + /// + /// Adds an Azure Service Bus Topic resource to the application model. This resource requires an to be added to the application model. + /// + /// The Azure Service Bus resource builder. + /// The name of the topic. + /// The name of the subscription. + /// Optional callback to customize the subscription. + public static IResourceBuilder AddSubscription(this IResourceBuilder builder, string topicName, string subscriptionName, Action? configureSubscription = default) + { + builder.Resource.Subscriptions.Add((topicName, subscriptionName, configureSubscription)); + return builder; + } } diff --git a/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs b/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs index 66b9b7e40e5..33b5c419c22 100644 --- a/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs +++ b/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs @@ -753,6 +753,40 @@ public void AddAzureServiceBus() Assert.Equal("{sb.outputs.serviceBusEndpoint}", serviceBus.Resource.ConnectionStringExpression); } + [Fact] + public async Task AddAzureServiceBusConstruct() + { + var builder = DistributedApplication.CreateBuilder(); + var serviceBus = builder.AddAzureServiceBusConstruct("sb"); + + serviceBus + .AddQueue("queue1") + .AddQueue("queue2") + .AddTopic("t1") + .AddTopic("t2") + .AddSubscription("t1", "s3"); + + serviceBus.Resource.Outputs["serviceBusEndpoint"] = "mynamespaceEndpoint"; + + Assert.Equal("sb", serviceBus.Resource.Name); + Assert.Equal("mynamespaceEndpoint", serviceBus.Resource.GetConnectionString()); + Assert.Equal("{sb.outputs.serviceBusEndpoint}", serviceBus.Resource.ConnectionStringExpression); + + var manifest = await ManifestUtils.GetManifest(serviceBus.Resource); + var expected = """ + { + "type": "azure.bicep.v0", + "connectionString": "{sb.outputs.serviceBusEndpoint}", + "path": "sb.module.bicep", + "params": { + "principalId": "", + "principalType": "" + } + } + """; + Assert.Equal(expected, manifest.ToString()); + } + [Fact] public async Task AddAzureStorage() { From 493f631075a04382bef6d312945ff60a58f30975 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sat, 9 Mar 2024 22:27:30 +0800 Subject: [PATCH 24/50] Fix console logs JS error (#2749) --- .../Components/Controls/ResourceSelect.razor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Aspire.Dashboard/Components/Controls/ResourceSelect.razor.cs b/src/Aspire.Dashboard/Components/Controls/ResourceSelect.razor.cs index e373cecdc40..9d9fb77c1f4 100644 --- a/src/Aspire.Dashboard/Components/Controls/ResourceSelect.razor.cs +++ b/src/Aspire.Dashboard/Components/Controls/ResourceSelect.razor.cs @@ -43,7 +43,7 @@ public ValueTask UpdateDisplayValueAsync() return ValueTask.CompletedTask; } - return JSRuntime.InvokeVoidAsync("updateSelectDisplayValue", _resourceSelectComponent.Element); + return JSRuntime.InvokeVoidAsync("updateFluentSelectDisplayValue", _resourceSelectComponent.Element); } private string? GetPopupHeight() From cb1e177340951b2ba0fd69eda6ec7896172af1ac Mon Sep 17 00:00:00 2001 From: Mouaad Gssair Date: Sat, 9 Mar 2024 22:40:26 +0100 Subject: [PATCH 25/50] Added Fix and tests for calling WithImage (#2580) (#2588) * Make `WithImageTag(..)` throw an appropriate exception and add overload with tag to `WithImage(..)` --- .../ContainerResourceBuilderExtensions.cs | 27 +++++++++++---- .../ContainerResourceBuilderTests.cs | 33 +++++++++++++++++++ 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/src/Aspire.Hosting/Extensions/ContainerResourceBuilderExtensions.cs b/src/Aspire.Hosting/Extensions/ContainerResourceBuilderExtensions.cs index 04e2ba89364..832a0939568 100644 --- a/src/Aspire.Hosting/Extensions/ContainerResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/Extensions/ContainerResourceBuilderExtensions.cs @@ -105,9 +105,13 @@ public static IResourceBuilder WithEntrypoint(this IResourceBuilder bui /// public static IResourceBuilder WithImageTag(this IResourceBuilder builder, string tag) where T : ContainerResource { - var containerImageAnnotation = builder.Resource.Annotations.OfType().Single(); - containerImageAnnotation.Tag = tag; - return builder; + if (builder.Resource.Annotations.OfType().LastOrDefault() is { } existingImageAnnotation) + { + existingImageAnnotation.Tag = tag; + return builder; + } + + throw new InvalidOperationException($"The resource '{builder.Resource.Name}' does not have a container image specified. Use WithImage to specify the container image and tag."); } /// @@ -129,12 +133,21 @@ public static IResourceBuilder WithImageRegistry(this IResourceBuilder /// /// Type of container resource. /// Builder for the container resource. - /// Registry value. + /// Image value. + /// Tag value. /// - public static IResourceBuilder WithImage(this IResourceBuilder builder, string image) where T : ContainerResource + public static IResourceBuilder WithImage(this IResourceBuilder builder, string image, string tag = "latest") where T : ContainerResource { - var containerImageAnnotation = builder.Resource.Annotations.OfType().Single(); - containerImageAnnotation.Image = image; + if (builder.Resource.Annotations.OfType().LastOrDefault() is { } existingImageAnnotation) + { + existingImageAnnotation.Image = image; + existingImageAnnotation.Tag = tag; + return builder; + } + + // if the annotation doesn't exist, create it with the given image and add it to the collection + var containerImageAnnotation = new ContainerImageAnnotation() { Image = image, Tag = tag }; + builder.Resource.Annotations.Add(containerImageAnnotation); return builder; } diff --git a/tests/Aspire.Hosting.Tests/ContainerResourceBuilderTests.cs b/tests/Aspire.Hosting.Tests/ContainerResourceBuilderTests.cs index 8699038c063..d26700e340a 100644 --- a/tests/Aspire.Hosting.Tests/ContainerResourceBuilderTests.cs +++ b/tests/Aspire.Hosting.Tests/ContainerResourceBuilderTests.cs @@ -15,6 +15,39 @@ public void WithImageMutatesImageName() Assert.Equal("redis-stack", redis.Resource.Annotations.OfType().Single().Image); } + [Fact] + public void WithImageMutatesImageNameAndTag() + { + var builder = DistributedApplication.CreateBuilder(); + var redis = builder.AddRedis("redis").WithImage("redis-stack", "1.0.0"); + Assert.Equal("redis-stack", redis.Resource.Annotations.OfType().Single().Image); + Assert.Equal("1.0.0", redis.Resource.Annotations.OfType().Single().Tag); + } + + [Fact] + public void WithImageAddsAnnotationIfNotExistingAndMutatesImageName() + { + var builder = DistributedApplication.CreateBuilder(); + var container = builder.AddContainer("app", "some-image"); + container.Resource.Annotations.RemoveAt(0); + + container.WithImage("new-image"); + Assert.Equal("new-image", container.Resource.Annotations.OfType().Single().Image); + Assert.Equal("latest", container.Resource.Annotations.OfType().Single().Tag); + } + + [Fact] + public void WithImageMutatesImageNameOfLastAnnotation() + { + var builder = DistributedApplication.CreateBuilder(); + var container = builder.AddContainer("app", "some-image"); + container.Resource.Annotations.Add(new ContainerImageAnnotation { Image = "another-image" } ); + + container.WithImage("new-image"); + Assert.Equal("new-image", container.Resource.Annotations.OfType().Last().Image); + Assert.Equal("latest", container.Resource.Annotations.OfType().Last().Tag); + } + [Fact] public void WithImageTagMutatesImageTag() { From a7eadaa04fa11a34699b20b1af2416efc9503db6 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Sat, 9 Mar 2024 15:59:49 -0800 Subject: [PATCH 26/50] Enable forwarded headers middleware when publishing projects by default (#2745) * Enable forwarded headers middleware when publishing projects by default Fixes #290 --- .../DisableForwardedHeadersAnnotation.cs | 15 +++++++ .../ApplicationModel/EndpointAnnotation.cs | 2 +- .../ProjectResourceBuilderExtensions.cs | 29 ++++++++++++- .../ProjectResourceTests.cs | 42 +++++++++++++++---- 4 files changed, 78 insertions(+), 10 deletions(-) create mode 100644 src/Aspire.Hosting/ApplicationModel/DisableForwardedHeadersAnnotation.cs diff --git a/src/Aspire.Hosting/ApplicationModel/DisableForwardedHeadersAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/DisableForwardedHeadersAnnotation.cs new file mode 100644 index 00000000000..9bdaa7597a5 --- /dev/null +++ b/src/Aspire.Hosting/ApplicationModel/DisableForwardedHeadersAnnotation.cs @@ -0,0 +1,15 @@ +// 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; + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// Represents an annotation that disables enabling forwarded headers on ASP.NET Core projects on publish. +/// +[DebuggerDisplay("Type = {GetType().Name,nq}")] +public sealed class DisableForwardedHeadersAnnotation : IResourceAnnotation +{ + +} diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs index 3ed59826397..cfa7739b767 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs @@ -7,7 +7,7 @@ namespace Aspire.Hosting.ApplicationModel; /// -/// Represents a endpoint annotation that describes how a service should be bound to a network. +/// Represents an endpoint annotation that describes how a service should be bound to a network. /// /// /// This class is used to specify the network protocol, port, URI scheme, transport, and other details for a service. diff --git a/src/Aspire.Hosting/Extensions/ProjectResourceBuilderExtensions.cs b/src/Aspire.Hosting/Extensions/ProjectResourceBuilderExtensions.cs index 3bf42ad93e9..092605299c7 100644 --- a/src/Aspire.Hosting/Extensions/ProjectResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/Extensions/ProjectResourceBuilderExtensions.cs @@ -13,6 +13,8 @@ namespace Aspire.Hosting; /// public static class ProjectResourceBuilderExtensions { + private const string AspNetCoreForwaredHeadersEnabledVariableName = "ASPNETCORE_FORWARDEDHEADERS_ENABLED"; + /// /// Adds a .NET project to the application model. By default, this will exist in a Projects namespace. e.g. Projects.MyProject. /// If the project is not in a Projects namespace, make sure a project reference is added from the AppHost project to the target project. @@ -92,6 +94,20 @@ private static IResourceBuilder WithProjectDefaults(this IResou builder.WithOtlpExporter(); builder.ConfigureConsoleLogs(); + var projectResource = builder.Resource; + + if (builder.ApplicationBuilder.ExecutionContext.IsPublishMode) + { + builder.WithEnvironment(context => + { + // If we have any endpoints & the forwarded headers wasn't disabled then add it + if (projectResource.GetEndpoints().Any() && !projectResource.Annotations.OfType().Any()) + { + context.EnvironmentVariables[AspNetCoreForwaredHeadersEnabledVariableName] = "true"; + } + }); + } + if (excludeLaunchProfile) { builder.WithAnnotation(new ExcludeLaunchProfileAnnotation()); @@ -103,8 +119,6 @@ private static IResourceBuilder WithProjectDefaults(this IResou builder.WithAnnotation(new LaunchProfileAnnotation(launchProfileName)); } - var projectResource = builder.Resource; - if (builder.ApplicationBuilder.ExecutionContext.IsRunMode) { // Automatically add EndpointAnnotation to project resources based on ApplicationUrl set in the launch profile. @@ -208,6 +222,17 @@ public static IResourceBuilder ExcludeLaunchProfile(this IResou throw new InvalidOperationException("This API is replaced by the AddProject overload that accepts a launchProfileName. Null means exclude launch profile. Method will be removed by GA."); } + /// + /// Configures the project to disable forwarded headers when being published. + /// + /// The project resource builder. + /// A reference to the . + public static IResourceBuilder DisableForwadedHeaders(this IResourceBuilder builder) + { + builder.WithAnnotation(ResourceAnnotationMutationBehavior.Replace); + return builder; + } + private static bool IsKestrelHttp2ConfigurationPresent(ProjectResource projectResource) { var projectMetadata = projectResource.GetProjectMetadata(); diff --git a/tests/Aspire.Hosting.Tests/ProjectResourceTests.cs b/tests/Aspire.Hosting.Tests/ProjectResourceTests.cs index b7e15f8a05e..2f13f3f252e 100644 --- a/tests/Aspire.Hosting.Tests/ProjectResourceTests.cs +++ b/tests/Aspire.Hosting.Tests/ProjectResourceTests.cs @@ -25,7 +25,7 @@ public async Task AddProjectAddsEnvironmentVariablesAndServiceMetadata() var resource = Assert.Single(projectResources); Assert.Equal("projectName", resource.Name); - Assert.Equal(6, resource.Annotations.Count); + Assert.Equal(7, resource.Annotations.Count); var serviceMetadata = Assert.Single(resource.Annotations.OfType()); Assert.IsType(serviceMetadata); @@ -172,16 +172,39 @@ public void ExcludeLaunchProfileAddsAnnotationToProject() var projectResources = appModel.GetProjectResources(); var resource = Assert.Single(projectResources); - // ExcludeLaunchProfileAnnotation isn't public, so we just check the type name - Assert.Contains(resource.Annotations, a => a.GetType().Name == "ExcludeLaunchProfileAnnotation"); + + Assert.Contains(resource.Annotations, a => a is ExcludeLaunchProfileAnnotation); } [Fact] - public async Task VerifyManifest() + public void DisabledForwadedHeadersAddsAnnotationToProject() { var appBuilder = CreateBuilder(); - appBuilder.AddProject("projectName"); + appBuilder.AddProject("projectName").DisableForwadedHeaders(); + using var app = appBuilder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var projectResources = appModel.GetProjectResources(); + + var resource = Assert.Single(projectResources); + + Assert.Contains(resource.Annotations, a => a is DisableForwardedHeadersAnnotation); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task VerifyManifest(bool disableForwardedHeaders) + { + var appBuilder = CreateBuilder(); + + var project = appBuilder.AddProject("projectName"); + if (disableForwardedHeaders) + { + project.DisableForwadedHeaders(); + } using var app = appBuilder.Build(); @@ -193,13 +216,17 @@ public async Task VerifyManifest() var manifest = await ManifestUtils.GetManifest(resource); - var expectedManifest = """ + var fordwardedHeadersEnvVar = disableForwardedHeaders + ? "" + : $",{Environment.NewLine} \"ASPNETCORE_FORWARDEDHEADERS_ENABLED\": \"true\""; + + var expectedManifest = $$""" { "type": "project.v0", "path": "net8.0/another-path", "env": { "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true", - "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true" + "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true"{{fordwardedHeadersEnvVar}} }, "bindings": { "http": { @@ -215,6 +242,7 @@ public async Task VerifyManifest() } } """; + Assert.Equal(expectedManifest, manifest.ToString()); } From d75d2c0abafdd1394591325992f6fae4c961d482 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Sun, 10 Mar 2024 13:57:33 -0700 Subject: [PATCH 27/50] Represent expressions made up of multiple properties of other resources. (#2759) * Introduce expressions made up of multiple properties of other resources. - Re-implement GetConnectionString to use ReferenceExpressions - Centralize hard coding of host.docker.internal - Fixed tests - Added suport for reference expressions in environment variables. * Clean up commented out code --- .../AzureAppConfigurationResource.cs | 17 +-- .../AzureApplicationInsightsResource.cs | 17 +-- .../AzureBlobStorageResource.cs | 16 +-- .../AzureCosmosDBResource.cs | 40 +----- .../AzureKeyVaultResource.cs | 30 +---- .../AzureOpenAIResource.cs | 3 +- .../AzurePostgresResource.cs | 30 +---- .../AzureQueueStorageResource.cs | 16 +-- .../AzureRedisResource.cs | 30 +---- .../AzureSearchResource.cs | 15 +-- .../AzureServiceBusResource.cs | 30 +---- .../AzureSignalRResource.cs | 20 +-- .../AzureSqlServerResource.cs | 48 ++----- .../AzureTableStorageResource.cs | 16 +-- .../DistributedApplicationExtensions.cs | 5 +- .../DistributedApplicationFactoryOfT.cs | 4 +- .../ApplicationModel/AllocatedEndpoint.cs | 9 +- .../ApplicationModel/EndpointReference.cs | 65 ++++++++- .../IResourceWithConnectionString.cs | 8 +- .../ApplicationModel/ReferenceExpression.cs | 124 ++++++++++++++++++ .../ResourceWithConnectionStringSurrogate.cs | 4 +- src/Aspire.Hosting/Dcp/ApplicationExecutor.cs | 7 +- .../Extensions/ResourceBuilderExtensions.cs | 21 ++- .../Kafka/KafkaServerResource.cs | 12 +- .../MongoDB/MongoDBBuilderExtensions.cs | 2 +- .../MongoDB/MongoDBDatabaseResource.cs | 4 +- .../MongoDB/MongoDBServerResource.cs | 18 ++- .../MySql/MySqlDatabaseResource.cs | 4 +- .../MySql/MySqlServerResource.cs | 17 +-- .../MySql/PhpMyAdminConfigWriterHook.cs | 4 +- src/Aspire.Hosting/Nats/NatsServerResource.cs | 15 ++- .../Oracle/OracleDatabaseResource.cs | 4 +- .../Oracle/OracleDatabaseServerResource.cs | 14 +- .../Postgres/PgAdminConfigWriterHook.cs | 2 +- .../Postgres/PostgresDatabaseResource.cs | 16 --- .../Postgres/PostgresServerResource.cs | 24 +--- .../RabbitMQ/RabbitMQServerResource.cs | 12 +- .../Redis/RedisCommanderConfigWriterHook.cs | 2 +- src/Aspire.Hosting/Redis/RedisResource.cs | 22 +--- src/Aspire.Hosting/Seq/SeqResource.cs | 11 +- .../SqlServer/SqlServerDatabaseResource.cs | 17 --- .../SqlServer/SqlServerServerResource.cs | 26 +--- .../TestingBuilderTests.cs | 2 +- .../TestingFactoryTests.cs | 4 +- .../Azure/AzureBicepProvisionerTests.cs | 2 +- .../Azure/AzureBicepResourceTests.cs | 56 ++++---- .../Kafka/AddKafkaTests.cs | 4 +- .../MongoDB/AddMongoDBTests.cs | 6 +- .../MySql/AddMySqlTests.cs | 10 +- .../Oracle/AddOracleDatabaseTests.cs | 14 +- .../Postgres/AddPostgresTests.cs | 10 +- .../RabbitMQ/AddRabbitMQTests.cs | 4 +- .../Redis/AddRedisTests.cs | 4 +- .../ResourceNotificationTests.cs | 2 +- .../SqlServer/AddSqlServerTests.cs | 8 +- .../WithEnvironmentTests.cs | 44 +++++++ .../WithReferenceTests.cs | 4 +- 57 files changed, 475 insertions(+), 500 deletions(-) create mode 100644 src/Aspire.Hosting/ApplicationModel/ReferenceExpression.cs diff --git a/src/Aspire.Hosting.Azure/AzureAppConfigurationResource.cs b/src/Aspire.Hosting.Azure/AzureAppConfigurationResource.cs index f80bdd12624..cffc55e05f4 100644 --- a/src/Aspire.Hosting.Azure/AzureAppConfigurationResource.cs +++ b/src/Aspire.Hosting.Azure/AzureAppConfigurationResource.cs @@ -23,24 +23,11 @@ public class AzureAppConfigurationResource(string name) : /// public string ConnectionStringExpression => Endpoint.ValueExpression; - /// - /// Gets the connection string for the Azure App Configuration resource. - /// - /// The connection string for the Azure App Configuration resource. - public string? GetConnectionString() => Endpoint.Value; - /// /// Gets the connection string for the Azure App Configuration resource. /// /// A to observe while waiting for the task to complete. /// The connection string for the Azure App Configuration resource. - public async ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) - { - if (ProvisioningTaskCompletionSource is not null) - { - await ProvisioningTaskCompletionSource.Task.WaitAsync(cancellationToken).ConfigureAwait(false); - } - - return GetConnectionString(); - } + public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken) + => Endpoint.GetValueAsync(cancellationToken); } diff --git a/src/Aspire.Hosting.Azure/AzureApplicationInsightsResource.cs b/src/Aspire.Hosting.Azure/AzureApplicationInsightsResource.cs index 380d00f8dd3..9c2a7acc141 100644 --- a/src/Aspire.Hosting.Azure/AzureApplicationInsightsResource.cs +++ b/src/Aspire.Hosting.Azure/AzureApplicationInsightsResource.cs @@ -23,26 +23,13 @@ public class AzureApplicationInsightsResource(string name) : /// public string ConnectionStringExpression => ConnectionString.ValueExpression; - /// - /// Gets the connection string for the Azure Application Insights resource. - /// - /// The connection string for the Azure Application Insights resource. - public string? GetConnectionString() => ConnectionString.Value; - /// /// Gets the connection string for the Azure Application Insights resource. /// /// A to observe while waiting for the task to complete. /// The connection string for the Azure Application Insights resource. - public async ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) - { - if (ProvisioningTaskCompletionSource is not null) - { - await ProvisioningTaskCompletionSource.Task.WaitAsync(cancellationToken).ConfigureAwait(false); - } - - return GetConnectionString(); - } + public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) + => ConnectionString.GetValueAsync(cancellationToken); // UseAzureMonitor is looks for this specific environment variable name. string IResourceWithConnectionString.ConnectionStringEnvironmentVariable => "APPLICATIONINSIGHTS_CONNECTION_STRING"; diff --git a/src/Aspire.Hosting.Azure/AzureBlobStorageResource.cs b/src/Aspire.Hosting.Azure/AzureBlobStorageResource.cs index 84ee7eab4fe..54a0d2c0760 100644 --- a/src/Aspire.Hosting.Azure/AzureBlobStorageResource.cs +++ b/src/Aspire.Hosting.Azure/AzureBlobStorageResource.cs @@ -25,12 +25,6 @@ public class AzureBlobStorageResource(string name, AzureStorageResource storage) /// public string ConnectionStringExpression => Parent.BlobEndpoint.ValueExpression; - /// - /// Gets the connection string for the Azure Blob Storage resource. - /// - /// The connection string for the Azure Blob Storage resource. - public string? GetConnectionString() => Parent.GetBlobConnectionString(); - /// /// Gets the connection string for the Azure Blob Storage resource. /// @@ -43,7 +37,7 @@ public class AzureBlobStorageResource(string name, AzureStorageResource storage) await Parent.ProvisioningTaskCompletionSource.Task.WaitAsync(cancellationToken).ConfigureAwait(false); } - return GetConnectionString(); + return Parent.GetBlobConnectionString(); } /// @@ -76,12 +70,6 @@ public class AzureBlobStorageConstructResource(string name, AzureStorageConstruc /// public string ConnectionStringExpression => Parent.BlobEndpoint.ValueExpression; - /// - /// Gets the connection string for the Azure Blob Storage resource. - /// - /// The connection string for the Azure Blob Storage resource. - public string? GetConnectionString() => Parent.GetBlobConnectionString(); - /// /// Gets the connection string for the Azure Blob Storage resource. /// @@ -94,7 +82,7 @@ public class AzureBlobStorageConstructResource(string name, AzureStorageConstruc await Parent.ProvisioningTaskCompletionSource.Task.WaitAsync(cancellationToken).ConfigureAwait(false); } - return GetConnectionString(); + return Parent.GetBlobConnectionString(); } /// diff --git a/src/Aspire.Hosting.Azure/AzureCosmosDBResource.cs b/src/Aspire.Hosting.Azure/AzureCosmosDBResource.cs index 0e69e447fb4..653812ec306 100644 --- a/src/Aspire.Hosting.Azure/AzureCosmosDBResource.cs +++ b/src/Aspire.Hosting.Azure/AzureCosmosDBResource.cs @@ -43,28 +43,14 @@ public class AzureCosmosDBResource(string name) : /// /// A to observe while waiting for the task to complete. /// The connection string to use for this database. - public async ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) - { - if (ProvisioningTaskCompletionSource is not null) - { - await ProvisioningTaskCompletionSource.Task.WaitAsync(cancellationToken).ConfigureAwait(false); - } - - return GetConnectionString(); - } - - /// - /// Gets the connection string to use for this database. - /// - /// The connection string to use for this database. - public string? GetConnectionString() + public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) { if (IsEmulator) { - return AzureCosmosDBEmulatorConnectionString.Create(EmulatorEndpoint.Port); + return new(AzureCosmosDBEmulatorConnectionString.Create(EmulatorEndpoint.Port)); } - return ConnectionString.Value; + return ConnectionString.GetValueAsync(cancellationToken); } } @@ -100,28 +86,14 @@ public class AzureCosmosDBConstructResource(string name, Action /// A to observe while waiting for the task to complete. /// The connection string to use for this database. - public async ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) - { - if (ProvisioningTaskCompletionSource is not null) - { - await ProvisioningTaskCompletionSource.Task.WaitAsync(cancellationToken).ConfigureAwait(false); - } - - return GetConnectionString(); - } - - /// - /// Gets the connection string to use for this database. - /// - /// The connection string to use for this database. - public string? GetConnectionString() + public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) { if (IsEmulator) { - return AzureCosmosDBEmulatorConnectionString.Create(EmulatorEndpoint.Port); + return new(AzureCosmosDBEmulatorConnectionString.Create(EmulatorEndpoint.Port)); } - return ConnectionString.Value; + return ConnectionString.GetValueAsync(cancellationToken); } } diff --git a/src/Aspire.Hosting.Azure/AzureKeyVaultResource.cs b/src/Aspire.Hosting.Azure/AzureKeyVaultResource.cs index a87a09089a1..9353e946002 100644 --- a/src/Aspire.Hosting.Azure/AzureKeyVaultResource.cs +++ b/src/Aspire.Hosting.Azure/AzureKeyVaultResource.cs @@ -23,25 +23,14 @@ public class AzureKeyVaultResource(string name) : /// public string ConnectionStringExpression => VaultUri.ValueExpression; - /// - /// Gets the connection string for the Azure Key Vault resource. - /// - /// The connection string for the Azure Key Vault resource. - public string? GetConnectionString() => VaultUri.Value; - /// /// Gets the connection string for the Azure Key Vault resource. /// /// A to observe while waiting for the task to complete. /// The connection string for the Azure Key Vault resource. - public async ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) + public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) { - if (ProvisioningTaskCompletionSource is not null) - { - await ProvisioningTaskCompletionSource.Task.WaitAsync(cancellationToken).ConfigureAwait(false); - } - - return new(GetConnectionString()); + return VaultUri.GetValueAsync(cancellationToken); } } @@ -62,24 +51,13 @@ public class AzureKeyVaultConstructResource(string name, Action public string ConnectionStringExpression => VaultUri.ValueExpression; - /// - /// Gets the connection string for the Azure Key Vault resource. - /// - /// The connection string for the Azure Key Vault resource. - public string? GetConnectionString() => VaultUri.Value; - /// /// Gets the connection string for the Azure Key Vault resource. /// /// A to observe while waiting for the task to complete. /// The connection string for the Azure Key Vault resource. - public async ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) + public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) { - if (ProvisioningTaskCompletionSource is not null) - { - await ProvisioningTaskCompletionSource.Task.WaitAsync(cancellationToken).ConfigureAwait(false); - } - - return new(GetConnectionString()); + return VaultUri.GetValueAsync(cancellationToken); } } diff --git a/src/Aspire.Hosting.Azure/AzureOpenAIResource.cs b/src/Aspire.Hosting.Azure/AzureOpenAIResource.cs index 88daa7cd31a..cafca733b79 100644 --- a/src/Aspire.Hosting.Azure/AzureOpenAIResource.cs +++ b/src/Aspire.Hosting.Azure/AzureOpenAIResource.cs @@ -28,7 +28,8 @@ public class AzureOpenAIResource(string name) : /// Gets the connection string for the resource. /// /// The connection string for the resource. - public string? GetConnectionString() => ConnectionString.Value; + public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken) + => ConnectionString.GetValueAsync(cancellationToken); /// /// Gets the list of deployments of the Azure OpenAI resource. diff --git a/src/Aspire.Hosting.Azure/AzurePostgresResource.cs b/src/Aspire.Hosting.Azure/AzurePostgresResource.cs index c39ff98f5f1..d581fda54c8 100644 --- a/src/Aspire.Hosting.Azure/AzurePostgresResource.cs +++ b/src/Aspire.Hosting.Azure/AzurePostgresResource.cs @@ -23,25 +23,14 @@ public class AzurePostgresResource(PostgresServerResource innerResource) : /// public string ConnectionStringExpression => ConnectionString.ValueExpression; - /// - /// Gets the connection string for the Azure Postgres Flexible Server. - /// - /// The connection string. - public string? GetConnectionString() => ConnectionString.Value; - /// /// Gets the connection string for the Azure Postgres Flexible Server. /// /// A to observe while waiting for the task to complete. /// The connection string. - public async ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) + public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) { - if (ProvisioningTaskCompletionSource is not null) - { - await ProvisioningTaskCompletionSource.Task.WaitAsync(cancellationToken).ConfigureAwait(false); - } - - return GetConnectionString(); + return ConnectionString.GetValueAsync(cancellationToken); } /// @@ -70,25 +59,14 @@ public class AzurePostgresConstructResource(PostgresServerResource innerResource /// public string ConnectionStringExpression => ConnectionString.ValueExpression; - /// - /// Gets the connection string for the Azure Postgres Flexible Server. - /// - /// The connection string. - public string? GetConnectionString() => ConnectionString.Value; - /// /// Gets the connection string for the Azure Postgres Flexible Server. /// /// A to observe while waiting for the task to complete. /// The connection string. - public async ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) + public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) { - if (ProvisioningTaskCompletionSource is not null) - { - await ProvisioningTaskCompletionSource.Task.WaitAsync(cancellationToken).ConfigureAwait(false); - } - - return GetConnectionString(); + return ConnectionString.GetValueAsync(cancellationToken); } /// diff --git a/src/Aspire.Hosting.Azure/AzureQueueStorageResource.cs b/src/Aspire.Hosting.Azure/AzureQueueStorageResource.cs index 360cb6240c9..06499f2bfbe 100644 --- a/src/Aspire.Hosting.Azure/AzureQueueStorageResource.cs +++ b/src/Aspire.Hosting.Azure/AzureQueueStorageResource.cs @@ -25,12 +25,6 @@ public class AzureQueueStorageResource(string name, AzureStorageResource storage /// public string ConnectionStringExpression => Parent.QueueEndpoint.ValueExpression; - /// - /// Gets the connection string for the Azure Queue Storage resource. - /// - /// The connection string for the Azure Queue Storage resource. - public string? GetConnectionString() => Parent.GetQueueConnectionString(); - /// /// Gets the connection string for the Azure Queue Storage resource. /// @@ -43,7 +37,7 @@ public class AzureQueueStorageResource(string name, AzureStorageResource storage await Parent.ProvisioningTaskCompletionSource.Task.WaitAsync(cancellationToken).ConfigureAwait(false); } - return GetConnectionString(); + return Parent.GetQueueConnectionString(); } internal void WriteToManifest(ManifestPublishingContext context) @@ -72,12 +66,6 @@ public class AzureQueueStorageConstructResource(string name, AzureStorageConstru /// public string ConnectionStringExpression => Parent.QueueEndpoint.ValueExpression; - /// - /// Gets the connection string for the Azure Queue Storage resource. - /// - /// The connection string for the Azure Queue Storage resource. - public string? GetConnectionString() => Parent.GetQueueConnectionString(); - /// /// Gets the connection string for the Azure Blob Storage resource. /// @@ -90,7 +78,7 @@ public class AzureQueueStorageConstructResource(string name, AzureStorageConstru await Parent.ProvisioningTaskCompletionSource.Task.WaitAsync(cancellationToken).ConfigureAwait(false); } - return GetConnectionString(); + return Parent.GetQueueConnectionString(); } internal void WriteToManifest(ManifestPublishingContext context) diff --git a/src/Aspire.Hosting.Azure/AzureRedisResource.cs b/src/Aspire.Hosting.Azure/AzureRedisResource.cs index 396887f4c7e..55fd5b6338d 100644 --- a/src/Aspire.Hosting.Azure/AzureRedisResource.cs +++ b/src/Aspire.Hosting.Azure/AzureRedisResource.cs @@ -23,25 +23,14 @@ public class AzureRedisResource(RedisResource innerResource) : /// public string ConnectionStringExpression => ConnectionString.ValueExpression; - /// - /// Gets the connection string for the Azure Redis resource. - /// - /// The connection string for the Azure Redis resource. - public string? GetConnectionString() => ConnectionString.Value; - /// /// Gets the connection string for the Azure Redis resource. /// /// A to observe while waiting for the task to complete. /// The connection string for the Azure Redis resource. - public async ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) + public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) { - if (ProvisioningTaskCompletionSource is not null) - { - await ProvisioningTaskCompletionSource.Task.WaitAsync(cancellationToken).ConfigureAwait(false); - } - - return GetConnectionString(); + return ConnectionString.GetValueAsync(cancellationToken); } /// @@ -70,25 +59,14 @@ public class AzureRedisConstructResource(RedisResource innerResource, Action public string ConnectionStringExpression => ConnectionString.ValueExpression; - /// - /// Gets the connection string for the Azure Redis resource. - /// - /// The connection string for the Azure Redis resource. - public string? GetConnectionString() => ConnectionString.Value; - /// /// Gets the connection string for the Azure Redis resource. /// /// A to observe while waiting for the task to complete. /// The connection string for the Azure Redis resource. - public async ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) + public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) { - if (ProvisioningTaskCompletionSource is not null) - { - await ProvisioningTaskCompletionSource.Task.WaitAsync(cancellationToken).ConfigureAwait(false); - } - - return GetConnectionString(); + return ConnectionString.GetValueAsync(cancellationToken); } /// diff --git a/src/Aspire.Hosting.Azure/AzureSearchResource.cs b/src/Aspire.Hosting.Azure/AzureSearchResource.cs index 6507f6a2ecd..e35945aad46 100644 --- a/src/Aspire.Hosting.Azure/AzureSearchResource.cs +++ b/src/Aspire.Hosting.Azure/AzureSearchResource.cs @@ -23,25 +23,14 @@ public class AzureSearchResource(string name) : /// public string ConnectionStringExpression => ConnectionString.ValueExpression; - /// - /// Gets the connection string for the azure search resource. - /// - /// The connection string for the resource. - public string? GetConnectionString() => ConnectionString.Value; - /// /// Gets the connection string for the azure search resource. /// /// A to observe while waiting for the task to complete. /// The connection string for the resource. - public async ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) + public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) { - if (ProvisioningTaskCompletionSource is not null) - { - await ProvisioningTaskCompletionSource.Task.WaitAsync(cancellationToken).ConfigureAwait(false); - } - - return GetConnectionString(); + return ConnectionString.GetValueAsync(cancellationToken); } } diff --git a/src/Aspire.Hosting.Azure/AzureServiceBusResource.cs b/src/Aspire.Hosting.Azure/AzureServiceBusResource.cs index 9e3f8edaf05..6ceca5bf17e 100644 --- a/src/Aspire.Hosting.Azure/AzureServiceBusResource.cs +++ b/src/Aspire.Hosting.Azure/AzureServiceBusResource.cs @@ -27,25 +27,14 @@ public class AzureServiceBusResource(string name) : /// public string ConnectionStringExpression => ServiceBusEndpoint.ValueExpression; - /// - /// Gets the connection string for the Azure Service Bus endpoint. - /// - /// The connection string for the Azure Service Bus endpoint. - public string? GetConnectionString() => ServiceBusEndpoint.Value; - /// /// Gets the connection string for the Azure Service Bus endpoint. /// /// A to observe while waiting for the task to complete. /// The connection string for the Azure Service Bus endpoint. - public async ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) + public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) { - if (ProvisioningTaskCompletionSource is not null) - { - await ProvisioningTaskCompletionSource.Task.WaitAsync(cancellationToken).ConfigureAwait(false); - } - - return GetConnectionString(); + return ServiceBusEndpoint.GetValueAsync(cancellationToken); } } @@ -71,24 +60,13 @@ public class AzureServiceBusConstructResource(string name, Action public string ConnectionStringExpression => ServiceBusEndpoint.ValueExpression; - /// - /// Gets the connection string for the Azure Service Bus endpoint. - /// - /// The connection string for the Azure Service Bus endpoint. - public string? GetConnectionString() => ServiceBusEndpoint.Value; - /// /// Gets the connection string for the Azure Service Bus endpoint. /// /// A to observe while waiting for the task to complete. /// The connection string for the Azure Service Bus endpoint. - public async ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) + public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) { - if (ProvisioningTaskCompletionSource is not null) - { - await ProvisioningTaskCompletionSource.Task.WaitAsync(cancellationToken).ConfigureAwait(false); - } - - return GetConnectionString(); + return ServiceBusEndpoint.GetValueAsync(cancellationToken); } } diff --git a/src/Aspire.Hosting.Azure/AzureSignalRResource.cs b/src/Aspire.Hosting.Azure/AzureSignalRResource.cs index e6f075ff79d..76b14ae5181 100644 --- a/src/Aspire.Hosting.Azure/AzureSignalRResource.cs +++ b/src/Aspire.Hosting.Azure/AzureSignalRResource.cs @@ -18,29 +18,21 @@ public class AzureSignalRResource(string name) : /// public BicepOutputReference HostName => new("hostName", this); - /// - /// Gets the connection string template for the manifest for Azure SignalR. - /// - public string ConnectionStringExpression => $"Endpoint=https://{HostName.ValueExpression};AuthType=azure"; + private ReferenceExpression ConnectionString + => ReferenceExpression.Create($"Endpoint=https://{HostName};AuthType=azure"); /// - /// Gets the connection string for Azure SignalR. + /// Gets the connection string template for the manifest for Azure SignalR. /// - /// The connection string for Azure SignalR. - public string? GetConnectionString() => $"Endpoint=https://{HostName.Value};AuthType=azure"; + public string ConnectionStringExpression => ConnectionString.ValueExpression; /// /// Gets the connection string for Azure SignalR. /// /// A to observe while waiting for the task to complete. /// The connection string for Azure SignalR. - public async ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) + public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) { - if (ProvisioningTaskCompletionSource is not null) - { - await ProvisioningTaskCompletionSource.Task.WaitAsync(cancellationToken).ConfigureAwait(false); - } - - return GetConnectionString(); + return ConnectionString.GetValueAsync(cancellationToken); } } diff --git a/src/Aspire.Hosting.Azure/AzureSqlServerResource.cs b/src/Aspire.Hosting.Azure/AzureSqlServerResource.cs index f78d8fa00ae..fc7e3b8b64d 100644 --- a/src/Aspire.Hosting.Azure/AzureSqlServerResource.cs +++ b/src/Aspire.Hosting.Azure/AzureSqlServerResource.cs @@ -18,34 +18,24 @@ public class AzureSqlServerResource(SqlServerServerResource innerResource) : /// public BicepOutputReference FullyQualifiedDomainName => new("sqlServerFqdn", this); + private ReferenceExpression ConnectionString => + ReferenceExpression.Create( + $"Server=tcp:{FullyQualifiedDomainName},1433;Encrypt=True;Authentication=\"Active Directory Default\""); + /// /// Gets the connection template for the manifest for the Azure SQL Server resource. /// public string ConnectionStringExpression => - $"Server=tcp:{FullyQualifiedDomainName.ValueExpression},1433;Encrypt=True;Authentication=\"Active Directory Default\""; - - /// - /// Gets the connection string for the Azure SQL Server resource. - /// - /// The connection string for the Azure SQL Server resource. - public string? GetConnectionString() - { - return $"Server=tcp:{FullyQualifiedDomainName.Value},1433;Encrypt=True;Authentication=\"Active Directory Default\""; - } + ConnectionString.ValueExpression; /// /// Gets the connection string for the Azure SQL Server resource. /// /// A to observe while waiting for the task to complete. /// The connection string for the Azure SQL Server resource. - public async ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) + public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) { - if (ProvisioningTaskCompletionSource is not null) - { - await ProvisioningTaskCompletionSource.Task.WaitAsync(cancellationToken).ConfigureAwait(false); - } - - return GetConnectionString(); + return ConnectionString.GetValueAsync(cancellationToken); } /// @@ -67,34 +57,24 @@ public class AzureSqlServerConstructResource(SqlServerServerResource innerResour /// public BicepOutputReference FullyQualifiedDomainName => new("sqlServerFqdn", this); + private ReferenceExpression ConnectionString => + ReferenceExpression.Create( + $"Server=tcp:{FullyQualifiedDomainName},1433;Encrypt=True;Authentication=\"Active Directory Default\""); + /// /// Gets the connection template for the manifest for the Azure SQL Server resource. /// public string ConnectionStringExpression => - $"Server=tcp:{FullyQualifiedDomainName.ValueExpression},1433;Encrypt=True;Authentication=\"Active Directory Default\""; - - /// - /// Gets the connection string for the Azure SQL Server resource. - /// - /// The connection string for the Azure SQL Server resource. - public string? GetConnectionString() - { - return $"Server=tcp:{FullyQualifiedDomainName.Value},1433;Encrypt=True;Authentication=\"Active Directory Default\""; - } + ConnectionString.ValueExpression; /// /// Gets the connection string for the Azure SQL Server resource. /// /// A to observe while waiting for the task to complete. /// The connection string for the Azure SQL Server resource. - public async ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) + public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) { - if (ProvisioningTaskCompletionSource is not null) - { - await ProvisioningTaskCompletionSource.Task.WaitAsync(cancellationToken).ConfigureAwait(false); - } - - return GetConnectionString(); + return ConnectionString.GetValueAsync(cancellationToken); } /// diff --git a/src/Aspire.Hosting.Azure/AzureTableStorageResource.cs b/src/Aspire.Hosting.Azure/AzureTableStorageResource.cs index 47e0a32c152..ff7eef2bbdb 100644 --- a/src/Aspire.Hosting.Azure/AzureTableStorageResource.cs +++ b/src/Aspire.Hosting.Azure/AzureTableStorageResource.cs @@ -25,12 +25,6 @@ public class AzureTableStorageResource(string name, AzureStorageResource storage /// public string ConnectionStringExpression => Parent.TableEndpoint.ValueExpression; - /// - /// Gets the connection string for the Azure Table Storage resource. - /// - /// The connection string for the Azure Table Storage resource. - public string? GetConnectionString() => Parent.GetTableConnectionString(); - /// /// Gets the connection string for the Azure Table Storage resource. /// @@ -43,7 +37,7 @@ public class AzureTableStorageResource(string name, AzureStorageResource storage await Parent.ProvisioningTaskCompletionSource.Task.WaitAsync(cancellationToken).ConfigureAwait(false); } - return GetConnectionString(); + return Parent.GetTableConnectionString(); } internal void WriteToManifest(ManifestPublishingContext context) @@ -72,12 +66,6 @@ public class AzureTableStorageConstructResource(string name, AzureStorageConstru /// public string ConnectionStringExpression => Parent.TableEndpoint.ValueExpression; - /// - /// Gets the connection string for the Azure Table Storage resource. - /// - /// The connection string for the Azure Table Storage resource. - public string? GetConnectionString() => Parent.GetTableConnectionString(); - /// /// Gets the connection string for the Azure Blob Storage resource. /// @@ -90,7 +78,7 @@ public class AzureTableStorageConstructResource(string name, AzureStorageConstru await Parent.ProvisioningTaskCompletionSource.Task.WaitAsync(cancellationToken).ConfigureAwait(false); } - return GetConnectionString(); + return Parent.GetTableConnectionString(); } internal void WriteToManifest(ManifestPublishingContext context) diff --git a/src/Aspire.Hosting.Testing/DistributedApplicationExtensions.cs b/src/Aspire.Hosting.Testing/DistributedApplicationExtensions.cs index 53437a8c08f..cfe44274fa7 100644 --- a/src/Aspire.Hosting.Testing/DistributedApplicationExtensions.cs +++ b/src/Aspire.Hosting.Testing/DistributedApplicationExtensions.cs @@ -32,9 +32,10 @@ public static HttpClient CreateHttpClient(this DistributedApplication app, strin /// /// The application. /// The resource name. + /// The cancellationToken to observe while waiting for the task to complete. /// The connection string for the specified resource. /// The resource was not found or does not expose a connection string. - public static string? GetConnectionString(this DistributedApplication app, string resourceName) + public static ValueTask GetConnectionStringAsync(this DistributedApplication app, string resourceName, CancellationToken cancellationToken = default) { var resource = GetResource(app, resourceName); if (resource is not IResourceWithConnectionString resourceWithConnectionString) @@ -42,7 +43,7 @@ public static HttpClient CreateHttpClient(this DistributedApplication app, strin throw new ArgumentException($"Resource '{resourceName}' does not expose a connection string.", nameof(resourceName)); } - return resourceWithConnectionString.GetConnectionString(); + return resourceWithConnectionString.GetConnectionStringAsync(cancellationToken); } /// diff --git a/src/Aspire.Hosting.Testing/DistributedApplicationFactoryOfT.cs b/src/Aspire.Hosting.Testing/DistributedApplicationFactoryOfT.cs index e3305f7dd5d..0e4d63d6016 100644 --- a/src/Aspire.Hosting.Testing/DistributedApplicationFactoryOfT.cs +++ b/src/Aspire.Hosting.Testing/DistributedApplicationFactoryOfT.cs @@ -72,9 +72,9 @@ public HttpClient CreateHttpClient(string resourceName, string? endpointName = d /// The resource name. /// The connection string for the specified resource. /// The resource was not found or does not expose a connection string. - public string? GetConnectionString(string resourceName) + public ValueTask GetConnectionString(string resourceName) { - return GetStartedApplication().GetConnectionString(resourceName); + return GetStartedApplication().GetConnectionStringAsync(resourceName); } /// diff --git a/src/Aspire.Hosting/ApplicationModel/AllocatedEndpoint.cs b/src/Aspire.Hosting/ApplicationModel/AllocatedEndpoint.cs index 3e39b262c5d..946ab536d40 100644 --- a/src/Aspire.Hosting/ApplicationModel/AllocatedEndpoint.cs +++ b/src/Aspire.Hosting/ApplicationModel/AllocatedEndpoint.cs @@ -16,8 +16,9 @@ public class AllocatedEndpoint /// /// The endpoint. /// The IP address of the endpoint. + /// The address of the container host. /// The port number of the endpoint. - public AllocatedEndpoint(EndpointAnnotation endpoint, string address, int port) + public AllocatedEndpoint(EndpointAnnotation endpoint, string address, int port, string containerHostAddress = "host.docker.internal") { ArgumentNullException.ThrowIfNull(endpoint); ArgumentOutOfRangeException.ThrowIfLessThan(port, 1, nameof(port)); @@ -25,6 +26,7 @@ public AllocatedEndpoint(EndpointAnnotation endpoint, string address, int port) Endpoint = endpoint; Address = address; + ContainerHostAddress = containerHostAddress; Port = port; } @@ -38,6 +40,11 @@ public AllocatedEndpoint(EndpointAnnotation endpoint, string address, int port) /// public string Address { get; private set; } + /// + /// The address of the container host. + /// + public string ContainerHostAddress { get; private set; } + /// /// The port used by the endpoint /// diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs index 29a05677585..14aa823b174 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Globalization; + namespace Aspire.Hosting.ApplicationModel; /// @@ -34,12 +36,12 @@ public sealed class EndpointReference : IManifestExpressionProvider, IValueProvi /// /// Gets the specified property expression of the endpoint. Defaults to the URL if no property is specified. /// - public string GetExpression(EndpointProperty property = EndpointProperty.Url) + internal string GetExpression(EndpointProperty property = EndpointProperty.Url) { var prop = property switch { EndpointProperty.Url => "url", - EndpointProperty.Host => "host", + EndpointProperty.Host or EndpointProperty.IPV4Host => "host", EndpointProperty.Port => "port", EndpointProperty.Scheme => "scheme", _ => throw new InvalidOperationException($"The property '{property}' is not supported for the endpoint '{EndpointName}'.") @@ -48,6 +50,16 @@ public string GetExpression(EndpointProperty property = EndpointProperty.Url) return $"{{{Owner.Name}.bindings.{EndpointName}.{prop}}}"; } + /// + /// Gets the specified property expression of the endpoint. Defaults to the URL if no property is specified. + /// + /// + /// + public EndpointReferenceExpression Property(EndpointProperty property) + { + return new(this, property); + } + /// /// Gets the port for this endpoint. /// @@ -58,6 +70,11 @@ public string GetExpression(EndpointProperty property = EndpointProperty.Url) /// public string Host => AllocatedEndpoint.Address ?? "localhost"; + /// + /// Gets the container host for this endpoint. + /// + public string ContainerHost => AllocatedEndpoint.ContainerHostAddress; + /// /// Gets the scheme for this endpoint. /// @@ -102,6 +119,46 @@ public EndpointReference(IResourceWithEndpoints owner, string endpointName) } } +/// +/// Represents a property expression for an endpoint reference. +/// +/// The endpoint reference. +/// The property of the endpoint. +public class EndpointReferenceExpression(EndpointReference endpointReference, EndpointProperty property) : IValueProvider, IManifestExpressionProvider +{ + /// + /// Gets the . + /// + public EndpointReference Owner { get; } = endpointReference; + + /// + /// Gets the for the property expression. + /// + public EndpointProperty Property { get; } = property; + + /// + /// Gets the expression of the property of the endpoint. + /// + public string ValueExpression => + Owner.GetExpression(Property); + + /// + /// Gets the value of the property of the endpoint. + /// + /// + /// + /// + public ValueTask GetValueAsync(CancellationToken cancellationToken) => Property switch + { + EndpointProperty.Url => new(Owner.Url), + EndpointProperty.Host => new(Owner.Host), + EndpointProperty.IPV4Host => new("127.0.0.1"), + EndpointProperty.Port => new(Owner.Port.ToString(CultureInfo.InvariantCulture)), + EndpointProperty.Scheme => new(Owner.Scheme), + _ => throw new InvalidOperationException($"The property '{Property}' is not supported for the endpoint '{Owner.EndpointName}'.") + }; +} + /// /// Represents the properties of an endpoint that can be referenced. /// @@ -116,6 +173,10 @@ public enum EndpointProperty /// Host, /// + /// The IPv4 address of the endpoint. + /// + IPV4Host, + /// /// The port of the endpoint. /// Port, diff --git a/src/Aspire.Hosting/ApplicationModel/IResourceWithConnectionString.cs b/src/Aspire.Hosting/ApplicationModel/IResourceWithConnectionString.cs index 58a45769c60..876a03f54fd 100644 --- a/src/Aspire.Hosting/ApplicationModel/IResourceWithConnectionString.cs +++ b/src/Aspire.Hosting/ApplicationModel/IResourceWithConnectionString.cs @@ -8,18 +8,12 @@ namespace Aspire.Hosting.ApplicationModel; /// public interface IResourceWithConnectionString : IResource, IManifestExpressionProvider, IValueProvider { - /// - /// Gets the connection string associated with the resource. - /// - /// The connection string associated with the resource, when one is available. - public string? GetConnectionString(); - /// /// Gets the connection string associated with the resource. /// /// A to observe while waiting for the task to complete. /// The connection string associated with the resource, when one is available. - public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) => new(GetConnectionString()); + public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default); string IManifestExpressionProvider.ValueExpression => ConnectionStringReferenceExpression; diff --git a/src/Aspire.Hosting/ApplicationModel/ReferenceExpression.cs b/src/Aspire.Hosting/ApplicationModel/ReferenceExpression.cs new file mode 100644 index 00000000000..1a340a0d3f7 --- /dev/null +++ b/src/Aspire.Hosting/ApplicationModel/ReferenceExpression.cs @@ -0,0 +1,124 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Text; + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// Represents an expression that might be made up of multiple resource properties. For example, +/// a connection string might be made up of a host, port, and password from different endpoints. +/// +public class ReferenceExpression : IValueProvider, IManifestExpressionProvider +{ + private readonly string[] _manifestExpressions; + + private ReferenceExpression(string format, IValueProvider[] valueProviders, string[] manifestExpressions) + { + Format = format; + ValueProviders = valueProviders; + _manifestExpressions = manifestExpressions; + } + + /// + /// The format string for this expression. + /// + public string Format { get; } + + /// + /// The manifest expressions for the parameters for the format string. + /// + public IReadOnlyList ManifestExpressions => _manifestExpressions; + + /// + /// The list of that will be used to resolve parameters for the format string. + /// + public IReadOnlyList ValueProviders { get; } + + /// + /// The value expression for the format string. + /// + public string ValueExpression => + string.Format(CultureInfo.InvariantCulture, Format, _manifestExpressions); + + /// + /// Gets the value of the expression. The final string value after evaluating the format string and its parameters. + /// + /// + /// + public async ValueTask GetValueAsync(CancellationToken cancellationToken) + { + var args = new object?[ValueProviders.Count]; + for (var i = 0; i < ValueProviders.Count; i++) + { + args[i] = await ValueProviders[i].GetValueAsync(cancellationToken).ConfigureAwait(false); + } + + return string.Format(CultureInfo.InvariantCulture, Format, args); + } + + internal static ReferenceExpression Create(string format, IValueProvider[] valueProviders, string[] manifestExpressions) + { + return new(format, valueProviders, manifestExpressions); + } + + /// + /// Creates a new instance of with the specified format and value providers. + /// + /// The handler that contains the format and value providers. + /// A new instance of with the specified format and value providers. + public static ReferenceExpression Create(in ExpressionInterpolatedStringHandler handler) + { + return handler.GetExpression(); + } +} + +/// +/// Represents a handler for interpolated strings that contain expressions. Those expressions will either be literal strings or +/// instances of types that implement both and . +/// +/// The length of the literal part of the interpolated string. +/// The number of formatted parts in the interpolated string. +[InterpolatedStringHandler] +public ref struct ExpressionInterpolatedStringHandler(int literalLength, int formattedCount) +{ + private readonly StringBuilder _builder = new(literalLength * 2); + private readonly List _valueProviders = new(formattedCount); + private readonly List _manifestExpressions = new(formattedCount); + + /// + /// Appends a literal value to the expression. + /// + /// + public readonly void AppendLiteral(string value) + { + _builder.Append(value); + } + + /// + /// Appends a formatted value to the expression. + /// + /// + public readonly void AppendFormatted(string value) + { + _builder.Append(value); + } + + /// + /// Appends a formatted value to the expression. The value must implement and . + /// + /// + /// + public void AppendFormatted(T valueProvider) where T : IValueProvider, IManifestExpressionProvider + { + var index = _valueProviders.Count; + _builder.Append(CultureInfo.InvariantCulture, $"{{{index}}}"); + + _valueProviders.Add(valueProvider); + _manifestExpressions.Add(valueProvider.ValueExpression); + } + + internal readonly ReferenceExpression GetExpression() => + ReferenceExpression.Create(_builder.ToString(), [.. _valueProviders], [.. _manifestExpressions]); +} diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceWithConnectionStringSurrogate.cs b/src/Aspire.Hosting/ApplicationModel/ResourceWithConnectionStringSurrogate.cs index 8a00a61db62..73911b37aa5 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceWithConnectionStringSurrogate.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceWithConnectionStringSurrogate.cs @@ -9,9 +9,9 @@ internal sealed class ResourceWithConnectionStringSurrogate(IResource innerResou public ResourceAnnotationCollection Annotations => innerResource.Annotations; - public string? GetConnectionString() + public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken) { - return callback(); + return new(callback()); } public string? ConnectionStringEnvironmentVariable => environmentVariableName; diff --git a/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs b/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs index 4ef46f1c75d..b1c4169f959 100644 --- a/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs +++ b/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs @@ -290,8 +290,10 @@ private async Task CreateContainersAndExecutablesAsync(CancellationToken cancell await Task.WhenAll(containersTask, executablesTask).ConfigureAwait(false); } - private static void AddAllocatedEndpointInfo(IEnumerable resources) + private void AddAllocatedEndpointInfo(IEnumerable resources) { + var containerHost = HostNameResolver.ReplaceLocalhostWithContainerHost("localhost", configuration); + foreach (var appResource in resources) { foreach (var sp in appResource.ServicesProduced) @@ -312,7 +314,8 @@ private static void AddAllocatedEndpointInfo(IEnumerable resources) sp.EndpointAnnotation.AllocatedEndpoint = new AllocatedEndpoint( sp.EndpointAnnotation, sp.EndpointAnnotation.IsProxied ? svc.AllocatedAddress! : "localhost", - (int)svc.AllocatedPort!); + (int)svc.AllocatedPort!, + containerHostAddress: containerHost); } } } diff --git a/src/Aspire.Hosting/Extensions/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/Extensions/ResourceBuilderExtensions.cs index bb67de9c5bd..b2001bd87a6 100644 --- a/src/Aspire.Hosting/Extensions/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/Extensions/ResourceBuilderExtensions.cs @@ -27,6 +27,25 @@ public static IResourceBuilder WithEnvironment(this IResourceBuilder bu return builder.WithAnnotation(new EnvironmentAnnotation(name, value ?? string.Empty)); } + /// + /// Adds an environment variable to the resource. + /// + /// The resource type. + /// The resource builder. + /// The name of the environment variable. + /// The value of the environment variable. + /// A resource configured with the specified environment variable. + public static IResourceBuilder WithEnvironment(this IResourceBuilder builder, string name, in ExpressionInterpolatedStringHandler value) + where T : IResourceWithEnvironment + { + var expression = value.GetExpression(); + + return builder.WithEnvironment(context => + { + context.EnvironmentVariables[name] = expression; + }); + } + /// /// Adds an environment variable to the resource. /// @@ -161,7 +180,7 @@ private static Action CreateEndpointReferenceEnviron /// The format of the environment variable will be "ConnectionStrings__{sourceResourceName}={connectionString}." /// /// Each resource defines the format of the connection string value. The - /// underlying connection string value can be retrieved using . + /// underlying connection string value can be retrieved using . /// /// /// Connection strings are also resolved by the configuration system (appSettings.json in the AppHost project, or environment variables). If a connection string is not found on the resource, the configuration system will be queried for a connection string diff --git a/src/Aspire.Hosting/Kafka/KafkaServerResource.cs b/src/Aspire.Hosting/Kafka/KafkaServerResource.cs index 80321405d8e..3847aed425c 100644 --- a/src/Aspire.Hosting/Kafka/KafkaServerResource.cs +++ b/src/Aspire.Hosting/Kafka/KafkaServerResource.cs @@ -20,18 +20,20 @@ public class KafkaServerResource(string name) : ContainerResource(name), IResour /// public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName); + private ReferenceExpression ConnectionString => + ReferenceExpression.Create( + $"{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}"); + /// /// Gets the connection string expression for Kafka broker for the manifest. /// public string ConnectionStringExpression => - $"{PrimaryEndpoint.GetExpression(EndpointProperty.Host)}:{PrimaryEndpoint.GetExpression(EndpointProperty.Port)}"; + ConnectionString.ValueExpression; /// /// Gets the connection string for Kafka broker. /// /// A connection string for the Kafka in the form "host:port" to be passed as BootstrapServers. - public string? GetConnectionString() - { - return $"{PrimaryEndpoint.Host}:{PrimaryEndpoint.Port}"; - } + public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken) => + ConnectionString.GetValueAsync(cancellationToken); } diff --git a/src/Aspire.Hosting/MongoDB/MongoDBBuilderExtensions.cs b/src/Aspire.Hosting/MongoDB/MongoDBBuilderExtensions.cs index 55244076d01..3fc2c0c2950 100644 --- a/src/Aspire.Hosting/MongoDB/MongoDBBuilderExtensions.cs +++ b/src/Aspire.Hosting/MongoDB/MongoDBBuilderExtensions.cs @@ -75,7 +75,7 @@ public static IResourceBuilder WithMongoExpress(this IResourceBuilder b private static void ConfigureMongoExpressContainer(EnvironmentCallbackContext context, MongoDBServerResource resource) { - context.EnvironmentVariables.Add("ME_CONFIG_MONGODB_URL", $"mongodb://host.docker.internal:{resource.PrimaryEndpoint.Port}/?directConnection=true"); + context.EnvironmentVariables.Add("ME_CONFIG_MONGODB_URL", $"mongodb://{resource.PrimaryEndpoint.ContainerHost}:{resource.PrimaryEndpoint.Port}/?directConnection=true"); context.EnvironmentVariables.Add("ME_CONFIG_BASICAUTH", "false"); } } diff --git a/src/Aspire.Hosting/MongoDB/MongoDBDatabaseResource.cs b/src/Aspire.Hosting/MongoDB/MongoDBDatabaseResource.cs index 656c8588e3e..93dda489abb 100644 --- a/src/Aspire.Hosting/MongoDB/MongoDBDatabaseResource.cs +++ b/src/Aspire.Hosting/MongoDB/MongoDBDatabaseResource.cs @@ -28,9 +28,9 @@ public string ConnectionStringExpression /// Gets the connection string for the MongoDB database. /// /// A connection string for the MongoDB database. - public string? GetConnectionString() + public async ValueTask GetConnectionStringAsync(CancellationToken cancellationToken) { - if (Parent.GetConnectionString() is { } connectionString) + if (await Parent.GetConnectionStringAsync(cancellationToken).ConfigureAwait(false) is { } connectionString) { return connectionString.EndsWith('/') ? $"{connectionString}{DatabaseName}" : diff --git a/src/Aspire.Hosting/MongoDB/MongoDBServerResource.cs b/src/Aspire.Hosting/MongoDB/MongoDBServerResource.cs index 5059366dffc..b68ac3c688d 100644 --- a/src/Aspire.Hosting/MongoDB/MongoDBServerResource.cs +++ b/src/Aspire.Hosting/MongoDB/MongoDBServerResource.cs @@ -1,8 +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 Aspire.Hosting.MongoDB; - namespace Aspire.Hosting.ApplicationModel; /// @@ -20,23 +18,23 @@ public class MongoDBServerResource(string name) : ContainerResource(name), IReso /// public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName); + private ReferenceExpression ConnectionString => + ReferenceExpression.Create( + $"mongodb://{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}"); + /// /// Gets the connection string for the MongoDB server. /// public string ConnectionStringExpression => - $"mongodb://{PrimaryEndpoint.GetExpression(EndpointProperty.Host)}:{PrimaryEndpoint.GetExpression(EndpointProperty.Port)}"; + ConnectionString.ValueExpression; /// /// Gets the connection string for the MongoDB server. /// + /// Cancellation token. /// A connection string for the MongoDB server in the form "mongodb://host:port". - public string? GetConnectionString() - { - return new MongoDBConnectionStringBuilder() - .WithServer(PrimaryEndpoint.Host) - .WithPort(PrimaryEndpoint.Port) - .Build(); - } + public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken) => + ConnectionString.GetValueAsync(cancellationToken); private readonly Dictionary _databases = new Dictionary(StringComparers.ResourceName); diff --git a/src/Aspire.Hosting/MySql/MySqlDatabaseResource.cs b/src/Aspire.Hosting/MySql/MySqlDatabaseResource.cs index 55d7e6cb83b..45cff50fcd7 100644 --- a/src/Aspire.Hosting/MySql/MySqlDatabaseResource.cs +++ b/src/Aspire.Hosting/MySql/MySqlDatabaseResource.cs @@ -28,9 +28,9 @@ public class MySqlDatabaseResource(string name, string databaseName, MySqlServer /// Gets the connection string for the MySQL database. /// /// A connection string for the MySQL database. - public string? GetConnectionString() + public async ValueTask GetConnectionStringAsync(CancellationToken cancellationToken) { - if (Parent.GetConnectionString() is { } connectionString) + if (await Parent.GetConnectionStringAsync(cancellationToken).ConfigureAwait(false) is { } connectionString) { return $"{connectionString};Database={DatabaseName}"; } diff --git a/src/Aspire.Hosting/MySql/MySqlServerResource.cs b/src/Aspire.Hosting/MySql/MySqlServerResource.cs index 972269d79f2..0b6c89839c5 100644 --- a/src/Aspire.Hosting/MySql/MySqlServerResource.cs +++ b/src/Aspire.Hosting/MySql/MySqlServerResource.cs @@ -1,8 +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 Aspire.Hosting.Utils; - namespace Aspire.Hosting.ApplicationModel; /// @@ -37,20 +35,23 @@ public MySqlServerResource(string name, string? password = null) : base(name) /// public string Password => PasswordInput.Input.Value ?? throw new InvalidOperationException("Password cannot be null."); + private ReferenceExpression ConnectionString => + ReferenceExpression.Create( + $"Server={PrimaryEndpoint.Property(EndpointProperty.Host)};Port={PrimaryEndpoint.Property(EndpointProperty.Port)};User ID=root;Password={PasswordInput}"); + /// /// Gets the connection string expression for the MySQL server. /// public string ConnectionStringExpression => - $"Server={PrimaryEndpoint.GetExpression(EndpointProperty.Host)};Port={PrimaryEndpoint.GetExpression(EndpointProperty.Port)};User ID=root;Password={PasswordInput.ValueExpression}"; + ConnectionString.ValueExpression; /// /// Gets the connection string for the MySQL server. /// - /// A connection string for the MySQL server in the form "Server=host;Port=port;User ID=root;Password=password". - public string? GetConnectionString() - { - return $"Server={PrimaryEndpoint.Host};Port={PrimaryEndpoint.Port};User ID=root;Password=\"{PasswordUtil.EscapePassword(Password)}\""; - } + /// + /// + public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken) => + ConnectionString.GetValueAsync(cancellationToken); private readonly Dictionary _databases = new Dictionary(StringComparers.ResourceName); diff --git a/src/Aspire.Hosting/MySql/PhpMyAdminConfigWriterHook.cs b/src/Aspire.Hosting/MySql/PhpMyAdminConfigWriterHook.cs index e81f77bd150..32489ce96ba 100644 --- a/src/Aspire.Hosting/MySql/PhpMyAdminConfigWriterHook.cs +++ b/src/Aspire.Hosting/MySql/PhpMyAdminConfigWriterHook.cs @@ -34,7 +34,7 @@ public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, C var endpoint = singleInstance.PrimaryEndpoint; myAdminResource.Annotations.Add(new EnvironmentCallbackAnnotation((EnvironmentCallbackContext context) => { - context.EnvironmentVariables.Add("PMA_HOST", $"host.docker.internal:{endpoint.Port}"); + context.EnvironmentVariables.Add("PMA_HOST", $"{endpoint.ContainerHost}:{endpoint.Port}"); context.EnvironmentVariables.Add("PMA_USER", "root"); context.EnvironmentVariables.Add("PMA_PASSWORD", singleInstance.Password); })); @@ -55,7 +55,7 @@ public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, C { var endpoint = mySqlInstance.PrimaryEndpoint; writer.WriteLine("$i++;"); - writer.WriteLine($"$cfg['Servers'][$i]['host'] = 'host.docker.internal:{endpoint.Port}';"); + writer.WriteLine($"$cfg['Servers'][$i]['host'] = '{endpoint.ContainerHost}:{endpoint.Port}';"); writer.WriteLine($"$cfg['Servers'][$i]['verbose'] = '{mySqlInstance.Name}';"); writer.WriteLine($"$cfg['Servers'][$i]['auth_type'] = 'cookie';"); writer.WriteLine($"$cfg['Servers'][$i]['user'] = 'root';"); diff --git a/src/Aspire.Hosting/Nats/NatsServerResource.cs b/src/Aspire.Hosting/Nats/NatsServerResource.cs index 36aecef11c0..95a6e2b13a4 100644 --- a/src/Aspire.Hosting/Nats/NatsServerResource.cs +++ b/src/Aspire.Hosting/Nats/NatsServerResource.cs @@ -20,18 +20,21 @@ public class NatsServerResource(string name) : ContainerResource(name), IResourc /// public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName); + private ReferenceExpression ConnectionString => + ReferenceExpression.Create( + $"{PrimaryNatsSchemeName}://{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}"); + /// /// Gets the connection string expression for the NATS server for the manifest. /// - public string? ConnectionStringExpression => $"{PrimaryNatsSchemeName}://{PrimaryEndpoint.GetExpression(EndpointProperty.Host)}:{PrimaryEndpoint.GetExpression(EndpointProperty.Port)}"; + public string? ConnectionStringExpression => + ConnectionString.ValueExpression; /// /// Gets the connection string (NATS_URL) for the NATS server. /// + /// Cancellation token. /// A connection string for the NATS server in the form "nats://host:port". - - public string GetConnectionString() - { - return $"{PrimaryNatsSchemeName}://{PrimaryEndpoint.Host}:{PrimaryEndpoint.Port}"; - } + public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken) => + ConnectionString.GetValueAsync(cancellationToken); } diff --git a/src/Aspire.Hosting/Oracle/OracleDatabaseResource.cs b/src/Aspire.Hosting/Oracle/OracleDatabaseResource.cs index 75e9ad93480..d7855e388dd 100644 --- a/src/Aspire.Hosting/Oracle/OracleDatabaseResource.cs +++ b/src/Aspire.Hosting/Oracle/OracleDatabaseResource.cs @@ -27,9 +27,9 @@ public class OracleDatabaseResource(string name, string databaseName, OracleData /// Gets the connection string for the Oracle Database. /// /// A connection string for the Oracle Database. - public string? GetConnectionString() + public async ValueTask GetConnectionStringAsync(CancellationToken cancellationToken) { - if (Parent.GetConnectionString() is { } connectionString) + if (await Parent.GetConnectionStringAsync(cancellationToken).ConfigureAwait(false) is { } connectionString) { return $"{connectionString}/{DatabaseName}"; } diff --git a/src/Aspire.Hosting/Oracle/OracleDatabaseServerResource.cs b/src/Aspire.Hosting/Oracle/OracleDatabaseServerResource.cs index 49b298a70b3..d6d1148ba38 100644 --- a/src/Aspire.Hosting/Oracle/OracleDatabaseServerResource.cs +++ b/src/Aspire.Hosting/Oracle/OracleDatabaseServerResource.cs @@ -1,8 +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 Aspire.Hosting.Utils; - namespace Aspire.Hosting.ApplicationModel; /// @@ -37,20 +35,22 @@ public OracleDatabaseServerResource(string name, string? password = null) : base /// public string Password => PasswordInput.Input.Value ?? throw new InvalidOperationException("Password cannot be null."); + private ReferenceExpression ConnectionString => + ReferenceExpression.Create( + $"user id=system;password={PasswordInput};data source={PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}"); + /// /// Gets the connection string expression for the Oracle Database server. /// public string ConnectionStringExpression => - $"user id=system;password={PasswordInput.ValueExpression};data source={PrimaryEndpoint.GetExpression(EndpointProperty.Host)}:{PrimaryEndpoint.GetExpression(EndpointProperty.Port)};"; + ConnectionString.ValueExpression; /// /// Gets the connection string for the Oracle Database server. /// /// A connection string for the Oracle Database server in the form "user id=system;password=password;data source=host:port". - public string? GetConnectionString() - { - return $"user id=system;password={PasswordUtil.EscapePassword(Password)};data source={PrimaryEndpoint.Host}:{PrimaryEndpoint.Port}"; - } + public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken) => + ConnectionString.GetValueAsync(cancellationToken); private readonly Dictionary _databases = new(StringComparers.ResourceName); diff --git a/src/Aspire.Hosting/Postgres/PgAdminConfigWriterHook.cs b/src/Aspire.Hosting/Postgres/PgAdminConfigWriterHook.cs index 86fbc3a2b72..9f0b0e2f683 100644 --- a/src/Aspire.Hosting/Postgres/PgAdminConfigWriterHook.cs +++ b/src/Aspire.Hosting/Postgres/PgAdminConfigWriterHook.cs @@ -35,7 +35,7 @@ public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, C writer.WriteStartObject($"{serverIndex}"); writer.WriteString("Name", postgresInstance.Name); writer.WriteString("Group", "Aspire instances"); - writer.WriteString("Host", "host.docker.internal"); + writer.WriteString("Host", endpoint.ContainerHost); writer.WriteNumber("Port", endpoint.Port); writer.WriteString("Username", "postgres"); writer.WriteString("SSLMode", "prefer"); diff --git a/src/Aspire.Hosting/Postgres/PostgresDatabaseResource.cs b/src/Aspire.Hosting/Postgres/PostgresDatabaseResource.cs index a621be72ba2..204ba5d529e 100644 --- a/src/Aspire.Hosting/Postgres/PostgresDatabaseResource.cs +++ b/src/Aspire.Hosting/Postgres/PostgresDatabaseResource.cs @@ -23,22 +23,6 @@ public class PostgresDatabaseResource(string name, string databaseName, Postgres /// public string ConnectionStringExpression => $"{{{Parent.Name}.connectionString}};Database={DatabaseName}"; - /// - /// Gets the connection string for the Postgres database. - /// - /// A connection string for the Postgres database. - public string? GetConnectionString() - { - if (Parent.GetConnectionString() is { } connectionString) - { - return $"{connectionString};Database={DatabaseName}"; - } - else - { - throw new DistributedApplicationException("Parent resource connection string was null."); - } - } - /// /// Gets the connection string for the Postgres database. /// diff --git a/src/Aspire.Hosting/Postgres/PostgresServerResource.cs b/src/Aspire.Hosting/Postgres/PostgresServerResource.cs index d31500d5ed0..e38edc1e34c 100644 --- a/src/Aspire.Hosting/Postgres/PostgresServerResource.cs +++ b/src/Aspire.Hosting/Postgres/PostgresServerResource.cs @@ -1,8 +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 Aspire.Hosting.Utils; - namespace Aspire.Hosting.ApplicationModel; /// @@ -37,6 +35,10 @@ public PostgresServerResource(string name, string? password = null) : base(name) /// public string Password => PasswordInput.Input.Value ?? throw new InvalidOperationException("Password cannot be null."); + private ReferenceExpression ConnectionString => + ReferenceExpression.Create( + $"Host={PrimaryEndpoint.Property(EndpointProperty.Host)};Port={PrimaryEndpoint.Property(EndpointProperty.Port)};Username=postgres;Password={PasswordInput}"); + /// /// Gets the connection string expression for the PostgreSQL server for the manifest. /// @@ -49,7 +51,7 @@ public string? ConnectionStringExpression return connectionStringAnnotation.Resource.ConnectionStringExpression; } - return $"Host={PrimaryEndpoint.GetExpression(EndpointProperty.Host)};Port={PrimaryEndpoint.GetExpression(EndpointProperty.Port)};Username=postgres;Password={PasswordInput.ValueExpression}"; + return ConnectionString.ValueExpression; } } @@ -65,21 +67,7 @@ public string? ConnectionStringExpression return connectionStringAnnotation.Resource.GetConnectionStringAsync(cancellationToken); } - return new(GetConnectionString()); - } - - /// - /// Gets the connection string for the PostgreSQL server. - /// - /// A connection string for the PostgreSQL server in the form "Host=host;Port=port;Username=postgres;Password=password". - public string? GetConnectionString() - { - if (this.TryGetLastAnnotation(out var connectionStringAnnotation)) - { - return connectionStringAnnotation.Resource.GetConnectionString(); - } - - return $"Host={PrimaryEndpoint.Host};Port={PrimaryEndpoint.Port};Username=postgres;Password={PasswordUtil.EscapePassword(Password)}"; + return ConnectionString.GetValueAsync(cancellationToken); } private readonly Dictionary _databases = new Dictionary(StringComparers.ResourceName); diff --git a/src/Aspire.Hosting/RabbitMQ/RabbitMQServerResource.cs b/src/Aspire.Hosting/RabbitMQ/RabbitMQServerResource.cs index 1e14cea4ba1..6dea78d4177 100644 --- a/src/Aspire.Hosting/RabbitMQ/RabbitMQServerResource.cs +++ b/src/Aspire.Hosting/RabbitMQ/RabbitMQServerResource.cs @@ -36,18 +36,20 @@ public RabbitMQServerResource(string name, string? password = null) : base(name) /// public string Password => PasswordInput.Input.Value ?? throw new InvalidOperationException("Password cannot be null."); + private ReferenceExpression ConnectionString => + ReferenceExpression.Create( + $"amqp://guest:{PasswordInput}@{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}"); + /// /// Gets the connection string expression for the RabbitMQ server for the manifest. /// public string ConnectionStringExpression => - $"amqp://guest:{PasswordInput.ValueExpression}@{PrimaryEndpoint.GetExpression(EndpointProperty.Host)}:{PrimaryEndpoint.GetExpression(EndpointProperty.Port)}"; + ConnectionString.ValueExpression; /// /// Gets the connection string for the RabbitMQ server. /// /// A connection string for the RabbitMQ server in the form "amqp://user:password@host:port". - public string? GetConnectionString() - { - return $"amqp://guest:{Password}@{PrimaryEndpoint.Host}:{PrimaryEndpoint.Port}"; - } + public async ValueTask GetConnectionStringAsync(CancellationToken cancellationToken) => + await ConnectionString.GetValueAsync(cancellationToken).ConfigureAwait(false); } diff --git a/src/Aspire.Hosting/Redis/RedisCommanderConfigWriterHook.cs b/src/Aspire.Hosting/Redis/RedisCommanderConfigWriterHook.cs index 2be00ca1638..41792eb20b0 100644 --- a/src/Aspire.Hosting/Redis/RedisCommanderConfigWriterHook.cs +++ b/src/Aspire.Hosting/Redis/RedisCommanderConfigWriterHook.cs @@ -31,7 +31,7 @@ public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, C { if (redisInstance.PrimaryEndpoint.IsAllocated) { - var hostString = $"{(hostsVariableBuilder.Length > 0 ? "," : string.Empty)}{redisInstance.Name}:host.docker.internal:{redisInstance.PrimaryEndpoint.Port}:0"; + var hostString = $"{(hostsVariableBuilder.Length > 0 ? "," : string.Empty)}{redisInstance.Name}:{redisInstance.PrimaryEndpoint.ContainerHost}:{redisInstance.PrimaryEndpoint.Port}:0"; hostsVariableBuilder.Append(hostString); } } diff --git a/src/Aspire.Hosting/Redis/RedisResource.cs b/src/Aspire.Hosting/Redis/RedisResource.cs index 9ae6c839fab..59080ac16a7 100644 --- a/src/Aspire.Hosting/Redis/RedisResource.cs +++ b/src/Aspire.Hosting/Redis/RedisResource.cs @@ -18,6 +18,10 @@ public class RedisResource(string name) : ContainerResource(name), IResourceWith /// public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName); + private ReferenceExpression ConnectionString => + ReferenceExpression.Create( + $"{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}"); + /// /// Gets the connection string expression for the Redis server for the manifest. /// @@ -30,7 +34,7 @@ public string? ConnectionStringExpression return connectionStringAnnotation.Resource.ConnectionStringExpression; } - return $"{PrimaryEndpoint.GetExpression(EndpointProperty.Host)}:{PrimaryEndpoint.GetExpression(EndpointProperty.Port)}"; + return ConnectionString.ValueExpression; } } @@ -46,20 +50,6 @@ public string? ConnectionStringExpression return connectionStringAnnotation.Resource.GetConnectionStringAsync(cancellationToken); } - return new(GetConnectionString()); - } - - /// - /// Gets the connection string for the Redis server. - /// - /// A connection string for the redis server in the form "host:port". - public string? GetConnectionString() - { - if (this.TryGetLastAnnotation(out var connectionStringAnnotation)) - { - return connectionStringAnnotation.Resource.GetConnectionString(); - } - - return $"{PrimaryEndpoint.Host}:{PrimaryEndpoint.Port}"; + return ConnectionString.GetValueAsync(cancellationToken); } } diff --git a/src/Aspire.Hosting/Seq/SeqResource.cs b/src/Aspire.Hosting/Seq/SeqResource.cs index 447ef9856c1..93b3665c5c3 100644 --- a/src/Aspire.Hosting/Seq/SeqResource.cs +++ b/src/Aspire.Hosting/Seq/SeqResource.cs @@ -18,17 +18,18 @@ public class SeqResource(string name) : ContainerResource(name), IResourceWithCo /// public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName); + private EndpointReferenceExpression ConnectionEndpoint => + PrimaryEndpoint.Property(EndpointProperty.Url); + /// /// Gets the Uri of the Seq endpoint /// - public string? GetConnectionString() - { - return PrimaryEndpoint.Url; - } + public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken) + => ConnectionEndpoint.GetValueAsync(cancellationToken); /// /// Gets the connection string expression for the Seq server for the manifest. /// public string? ConnectionStringExpression => - PrimaryEndpoint.GetExpression(EndpointProperty.Url); + ConnectionEndpoint.ValueExpression; } diff --git a/src/Aspire.Hosting/SqlServer/SqlServerDatabaseResource.cs b/src/Aspire.Hosting/SqlServer/SqlServerDatabaseResource.cs index b91c8aaed9f..e9c995b6a24 100644 --- a/src/Aspire.Hosting/SqlServer/SqlServerDatabaseResource.cs +++ b/src/Aspire.Hosting/SqlServer/SqlServerDatabaseResource.cs @@ -23,23 +23,6 @@ public class SqlServerDatabaseResource(string name, string databaseName, SqlServ /// public string ConnectionStringExpression => $"{{{Parent.Name}.connectionString}};Database={DatabaseName}"; - /// - /// Gets the connection string for the database resource. - /// - /// The connection string for the database resource. - /// Thrown when the parent resource connection string is null. - public string? GetConnectionString() - { - if (Parent.GetConnectionString() is { } connectionString) - { - return $"{connectionString};Database={DatabaseName}"; - } - else - { - throw new DistributedApplicationException("Parent resource connection string was null."); - } - } - /// /// Gets the connection string for the database resource. /// diff --git a/src/Aspire.Hosting/SqlServer/SqlServerServerResource.cs b/src/Aspire.Hosting/SqlServer/SqlServerServerResource.cs index ce3bf5c5c55..cbde94a8de3 100644 --- a/src/Aspire.Hosting/SqlServer/SqlServerServerResource.cs +++ b/src/Aspire.Hosting/SqlServer/SqlServerServerResource.cs @@ -1,8 +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 Aspire.Hosting.Utils; - namespace Aspire.Hosting.ApplicationModel; /// @@ -38,6 +36,10 @@ public SqlServerServerResource(string name, string? password = null) : base(name /// public string Password => PasswordInput.Input.Value ?? throw new InvalidOperationException("Password cannot be null."); + private ReferenceExpression ConnectionString => + ReferenceExpression.Create( + $"Server={PrimaryEndpoint.Property(EndpointProperty.IPV4Host)},{PrimaryEndpoint.Property(EndpointProperty.Port)};User ID=sa;Password={PasswordInput};TrustServerCertificate=true"); + /// /// Gets the connection string expression for the SQL Server for the manifest. /// @@ -50,7 +52,7 @@ public string? ConnectionStringExpression return connectionStringAnnotation.Resource.ConnectionStringExpression; } - return $"Server={PrimaryEndpoint.GetExpression(EndpointProperty.Host)},{PrimaryEndpoint.GetExpression(EndpointProperty.Port)};User ID=sa;Password={PasswordInput.ValueExpression};TrustServerCertificate=true"; + return ConnectionString.ValueExpression; } } @@ -66,23 +68,7 @@ public string? ConnectionStringExpression return connectionStringAnnotation.Resource.GetConnectionStringAsync(cancellationToken); } - return new(GetConnectionString()); - } - - /// - /// Gets the connection string for the SQL Server. - /// - /// A connection string for the SQL Server in the form "Server=host,port;User ID=sa;Password=password;TrustServerCertificate=true". - public string? GetConnectionString() - { - if (this.TryGetLastAnnotation(out var connectionStringAnnotation)) - { - return connectionStringAnnotation.Resource.GetConnectionString(); - } - - // HACK: Use the 127.0.0.1 address because localhost is resolving to [::1] following - // up with DCP on this issue. - return $"Server=127.0.0.1,{PrimaryEndpoint.Port};User ID=sa;Password={PasswordUtil.EscapePassword(Password)};TrustServerCertificate=true"; + return ConnectionString.GetValueAsync(cancellationToken); } private readonly Dictionary _databases = new(StringComparers.ResourceName); diff --git a/tests/Aspire.Hosting.Testing.Tests/TestingBuilderTests.cs b/tests/Aspire.Hosting.Testing.Tests/TestingBuilderTests.cs index 520336b131f..9b133ae8c71 100644 --- a/tests/Aspire.Hosting.Testing.Tests/TestingBuilderTests.cs +++ b/tests/Aspire.Hosting.Testing.Tests/TestingBuilderTests.cs @@ -24,7 +24,7 @@ public async Task HasEndPoints() Assert.True(workerEndpoint.Host.Length > 0); // Get a connection string from a resource - var pgConnectionString = app.GetConnectionString("postgres1"); + var pgConnectionString = await app.GetConnectionStringAsync("postgres1"); Assert.NotNull(pgConnectionString); Assert.True(pgConnectionString.Length > 0); } diff --git a/tests/Aspire.Hosting.Testing.Tests/TestingFactoryTests.cs b/tests/Aspire.Hosting.Testing.Tests/TestingFactoryTests.cs index e5a5a3b3bd7..92dc111b890 100644 --- a/tests/Aspire.Hosting.Testing.Tests/TestingFactoryTests.cs +++ b/tests/Aspire.Hosting.Testing.Tests/TestingFactoryTests.cs @@ -13,7 +13,7 @@ public class TestingFactoryTests(DistributedApplicationFixture fixture) private readonly DistributedApplication _app = fixture.Application; [LocalOnlyFact] - public void HasEndPoints() + public async void HasEndPoints() { // Get an endpoint from a resource var workerEndpoint = _app.GetEndpoint("myworker1", "myendpoint1"); @@ -21,7 +21,7 @@ public void HasEndPoints() Assert.True(workerEndpoint.Host.Length > 0); // Get a connection string from a resource - var pgConnectionString = _app.GetConnectionString("postgres1"); + var pgConnectionString = await _app.GetConnectionStringAsync("postgres1"); Assert.NotNull(pgConnectionString); Assert.True(pgConnectionString.Length > 0); } diff --git a/tests/Aspire.Hosting.Tests/Azure/AzureBicepProvisionerTests.cs b/tests/Aspire.Hosting.Tests/Azure/AzureBicepProvisionerTests.cs index a9147b295ee..745557cb56a 100644 --- a/tests/Aspire.Hosting.Tests/Azure/AzureBicepProvisionerTests.cs +++ b/tests/Aspire.Hosting.Tests/Azure/AzureBicepProvisionerTests.cs @@ -145,6 +145,6 @@ private sealed class ResourceWithConnectionString(string name, string connection Resource(name), IResourceWithConnectionString { - public string? GetConnectionString() => connectionString; + public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken) => new(connectionString); } } diff --git a/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs b/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs index 33b5c419c22..ece58fbcfec 100644 --- a/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs +++ b/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs @@ -102,7 +102,7 @@ public void AssertManifestLayout() } [Fact] - public void AddAzureCosmosDb() + public async Task AddAzureCosmosDb() { var builder = DistributedApplication.CreateBuilder(); @@ -118,7 +118,7 @@ public void AddAzureCosmosDb() Assert.Equal("cosmos", cosmos.Resource.Parameters["databaseAccountName"]); Assert.NotNull(databases); Assert.Equal(["mydatabase"], databases); - Assert.Equal("mycosmosconnectionstring", cosmos.Resource.GetConnectionString()); + Assert.Equal("mycosmosconnectionstring", await cosmos.Resource.GetConnectionStringAsync()); Assert.Equal("{cosmos.secretOutputs.connectionString}", cosmos.Resource.ConnectionStringExpression); } @@ -157,11 +157,11 @@ public async Task AddAzureCosmosDbConstruct() ); Assert.Equal("cosmos", cosmos.Resource.Name); - Assert.Equal("mycosmosconnectionstring", cosmos.Resource.GetConnectionString()); + Assert.Equal("mycosmosconnectionstring", await cosmos.Resource.GetConnectionStringAsync(default)); } [Fact] - public void AddAppConfiguration() + public async Task AddAppConfiguration() { var builder = DistributedApplication.CreateBuilder(); @@ -172,7 +172,7 @@ public void AddAppConfiguration() Assert.Equal("Aspire.Hosting.Azure.Bicep.appconfig.bicep", appConfig.Resource.TemplateResourceName); Assert.Equal("appConfig", appConfig.Resource.Name); Assert.Equal("appconfig", appConfig.Resource.Parameters["configName"]); - Assert.Equal("https://myendpoint", appConfig.Resource.GetConnectionString()); + Assert.Equal("https://myendpoint", await appConfig.Resource.GetConnectionStringAsync(default)); Assert.Equal("{appConfig.outputs.appConfigEndpoint}", appConfig.Resource.ConnectionStringExpression); } @@ -189,7 +189,7 @@ public async Task AddApplicationInsights() Assert.Equal("appInsights", appInsights.Resource.Name); Assert.Equal("appinsights", appInsights.Resource.Parameters["appInsightsName"]); Assert.True(appInsights.Resource.Parameters.ContainsKey(AzureBicepResource.KnownParameters.LogAnalyticsWorkspaceId)); - Assert.Equal("myinstrumentationkey", appInsights.Resource.GetConnectionString()); + Assert.Equal("myinstrumentationkey", await appInsights.Resource.GetConnectionStringAsync(default)); Assert.Equal("{appInsights.outputs.appInsightsConnectionString}", appInsights.Resource.ConnectionStringExpression); var appInsightsManifest = await ManifestUtils.GetManifest(appInsights.Resource); @@ -321,7 +321,7 @@ public async Task PublishAsRedisPublishesRedisAsAzureRedis() Assert.True(redis.Resource.IsContainer()); - Assert.Equal("localhost:12455", redis.Resource.GetConnectionString()); + Assert.Equal("localhost:12455", await redis.Resource.GetConnectionStringAsync()); var manifest = await ManifestUtils.GetManifest(redis.Resource); @@ -340,7 +340,7 @@ public async Task PublishAsRedisPublishesRedisAsAzureRedisConstruct() Assert.True(redis.Resource.IsContainer()); - Assert.Equal("localhost:12455", redis.Resource.GetConnectionString()); + Assert.Equal("localhost:12455", await redis.Resource.GetConnectionStringAsync()); var manifest = await ManifestUtils.GetManifest(redis.Resource); var expectedManifest = """ @@ -360,7 +360,7 @@ public async Task PublishAsRedisPublishesRedisAsAzureRedisConstruct() } [Fact] - public void AddBicepKeyVault() + public async Task AddKeyVault() { var builder = DistributedApplication.CreateBuilder(); @@ -371,7 +371,7 @@ public void AddBicepKeyVault() Assert.Equal("Aspire.Hosting.Azure.Bicep.keyvault.bicep", keyVault.Resource.TemplateResourceName); Assert.Equal("keyVault", keyVault.Resource.Name); Assert.Equal("keyvault", keyVault.Resource.Parameters["vaultName"]); - Assert.Equal("https://myvault", keyVault.Resource.GetConnectionString()); + Assert.Equal("https://myvault", await keyVault.Resource.GetConnectionStringAsync()); Assert.Equal("{keyVault.outputs.vaultUri}", keyVault.Resource.ConnectionStringExpression); } @@ -406,7 +406,7 @@ public async Task AddKeyVaultConstruct() } [Fact] - public void AsAzureSqlDatabase() + public async Task AsAzureSqlDatabase() { var builder = DistributedApplication.CreateBuilder(); @@ -429,7 +429,7 @@ public void AsAzureSqlDatabase() Assert.Equal("sql", azureSql.Resource.Parameters["serverName"]); Assert.NotNull(databases); Assert.Equal(["dbName"], databases); - Assert.Equal("Server=tcp:myserver,1433;Encrypt=True;Authentication=\"Active Directory Default\"", sql.Resource.GetConnectionString()); + Assert.Equal("Server=tcp:myserver,1433;Encrypt=True;Authentication=\"Active Directory Default\"", await sql.Resource.GetConnectionStringAsync(default)); Assert.Equal("Server=tcp:{sql.outputs.sqlServerFqdn},1433;Encrypt=True;Authentication=\"Active Directory Default\"", sql.Resource.ConnectionStringExpression); } @@ -486,7 +486,7 @@ public async void AsAzureSqlDatabaseConstruct() } [Fact] - public void AsAzurePostgresFlexibleServer() + public async Task AsAzurePostgresFlexibleServer() { var builder = DistributedApplication.CreateBuilder(); @@ -521,7 +521,7 @@ public void AsAzurePostgresFlexibleServer() // Setup to verify that connection strings is acquired via resource connectionstring redirct. azurePostgres.Resource.SecretOutputs["connectionString"] = "myconnectionstring"; - Assert.Equal("myconnectionstring", postgres.Resource.GetConnectionString()); + Assert.Equal("myconnectionstring", await postgres.Resource.GetConnectionStringAsync(default)); Assert.Equal("{postgres.secretOutputs.connectionString}", azurePostgres.Resource.ConnectionStringExpression); } @@ -563,7 +563,7 @@ public async Task PublishAsAzurePostgresFlexibleServer() // still uses the local endpoint. postgres.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 1234)); var expectedConnectionString = $"Host=localhost;Port=1234;Username=postgres;Password={PasswordUtil.EscapePassword(postgres.Resource.Password)}"; - Assert.Equal(expectedConnectionString, postgres.Resource.GetConnectionString()); + Assert.Equal(expectedConnectionString, await postgres.Resource.GetConnectionStringAsync(default)); Assert.Equal("{postgres.secretOutputs.connectionString}", azurePostgres.Resource.ConnectionStringExpression); @@ -721,7 +721,7 @@ public async Task PublishAsAzurePostgresFlexibleServerNoUserPassParams() } [Fact] - public void AddAzureServiceBus() + public async Task AddAzureServiceBus() { var builder = DistributedApplication.CreateBuilder(); var serviceBus = builder.AddAzureServiceBus("sb"); @@ -749,7 +749,7 @@ public void AddAzureServiceBus() Assert.Equal(["queue1", "queue2"], queues); Assert.NotNull(topics); Assert.Equal("""[{"name":"t1","subscriptions":["s1","s2"]},{"name":"t2","subscriptions":[]},{"name":"t3","subscriptions":["s3"]}]""", topics.ToJsonString()); - Assert.Equal("mynamespaceEndpoint", serviceBus.Resource.GetConnectionString()); + Assert.Equal("mynamespaceEndpoint", await serviceBus.Resource.GetConnectionStringAsync(default)); Assert.Equal("{sb.outputs.serviceBusEndpoint}", serviceBus.Resource.ConnectionStringExpression); } @@ -769,7 +769,7 @@ public async Task AddAzureServiceBusConstruct() serviceBus.Resource.Outputs["serviceBusEndpoint"] = "mynamespaceEndpoint"; Assert.Equal("sb", serviceBus.Resource.Name); - Assert.Equal("mynamespaceEndpoint", serviceBus.Resource.GetConnectionString()); + Assert.Equal("mynamespaceEndpoint", await serviceBus.Resource.GetConnectionStringAsync()); Assert.Equal("{sb.outputs.serviceBusEndpoint}", serviceBus.Resource.ConnectionStringExpression); var manifest = await ManifestUtils.GetManifest(serviceBus.Resource); @@ -806,9 +806,9 @@ public async Task AddAzureStorage() Assert.Equal("storage", storage.Resource.Name); Assert.Equal("storage", storage.Resource.Parameters["storageName"]); - Assert.Equal("https://myblob", blob.Resource.GetConnectionString()); - Assert.Equal("https://myqueue", queue.Resource.GetConnectionString()); - Assert.Equal("https://mytable", table.Resource.GetConnectionString()); + Assert.Equal("https://myblob", await blob.Resource.GetConnectionStringAsync()); + Assert.Equal("https://myqueue", await queue.Resource.GetConnectionStringAsync()); + Assert.Equal("https://mytable", await table.Resource.GetConnectionStringAsync()); Assert.Equal("{storage.outputs.blobEndpoint}", blob.Resource.ConnectionStringExpression); Assert.Equal("{storage.outputs.queueEndpoint}", queue.Resource.ConnectionStringExpression); Assert.Equal("{storage.outputs.tableEndpoint}", table.Resource.ConnectionStringExpression); @@ -856,7 +856,7 @@ public async Task AddAzureConstructStorage() // Check blob resource. var blob = storage.AddBlobs("blob"); - Assert.Equal("https://myblob", blob.Resource.GetConnectionString()); + Assert.Equal("https://myblob", await blob.Resource.GetConnectionStringAsync()); var expectedBlobManifest = """ { "type": "value.v0", @@ -868,7 +868,7 @@ public async Task AddAzureConstructStorage() // Check queue resource. var queue = storage.AddQueues("queue"); - Assert.Equal("https://myqueue", queue.Resource.GetConnectionString()); + Assert.Equal("https://myqueue", await queue.Resource.GetConnectionStringAsync()); var expectedQueueManifest = """ { "type": "value.v0", @@ -880,7 +880,7 @@ public async Task AddAzureConstructStorage() // Check table resource. var table = storage.AddTables("table"); - Assert.Equal("https://mytable", table.Resource.GetConnectionString()); + Assert.Equal("https://mytable", await table.Resource.GetConnectionStringAsync()); var expectedTableManifest = """ { "type": "value.v0", @@ -892,7 +892,7 @@ public async Task AddAzureConstructStorage() } [Fact] - public void AddAzureSearch() + public async Task AddAzureSearch() { var builder = DistributedApplication.CreateBuilder(); @@ -902,7 +902,7 @@ public void AddAzureSearch() Assert.Equal("Aspire.Hosting.Azure.Bicep.search.bicep", search.Resource.TemplateResourceName); Assert.Equal("search", search.Resource.Name); - Assert.Equal("mysearchconnectionstring", search.Resource.GetConnectionString()); + Assert.Equal("mysearchconnectionstring", await search.Resource.GetConnectionStringAsync(default)); Assert.Equal("{search.outputs.connectionString}", search.Resource.ConnectionStringExpression); } @@ -932,7 +932,7 @@ public async Task PublishAsConnectionString() } [Fact] - public void AddAzureOpenAI() + public async Task AddAzureOpenAI() { var builder = DistributedApplication.CreateBuilder(); @@ -947,7 +947,7 @@ public void AddAzureOpenAI() Assert.Equal("Aspire.Hosting.Azure.Bicep.openai.bicep", openai.Resource.TemplateResourceName); Assert.Equal("openai", openai.Resource.Name); - Assert.Equal("myopenaiconnectionstring", openai.Resource.GetConnectionString()); + Assert.Equal("myopenaiconnectionstring", await openai.Resource.GetConnectionStringAsync(default)); Assert.Equal("{openai.outputs.connectionString}", openai.Resource.ConnectionStringExpression); Assert.NotNull(deployment); Assert.Equal("mymodel", deployment["name"]?.ToString()); diff --git a/tests/Aspire.Hosting.Tests/Kafka/AddKafkaTests.cs b/tests/Aspire.Hosting.Tests/Kafka/AddKafkaTests.cs index a4775b3fbea..3e618641e9c 100644 --- a/tests/Aspire.Hosting.Tests/Kafka/AddKafkaTests.cs +++ b/tests/Aspire.Hosting.Tests/Kafka/AddKafkaTests.cs @@ -42,7 +42,7 @@ public void AddKafkaContainerWithDefaultsAddsAnnotationMetadata() } [Fact] - public void KafkaCreatesConnectionString() + public async Task KafkaCreatesConnectionString() { var appBuilder = DistributedApplication.CreateBuilder(); appBuilder @@ -54,7 +54,7 @@ public void KafkaCreatesConnectionString() var appModel = app.Services.GetRequiredService(); var connectionStringResource = Assert.Single(appModel.Resources.OfType()); - var connectionString = connectionStringResource.GetConnectionString(); + var connectionString = await connectionStringResource.GetConnectionStringAsync(default); Assert.Equal("localhost:27017", connectionString); Assert.Equal("{kafka.bindings.tcp.host}:{kafka.bindings.tcp.port}", connectionStringResource.ConnectionStringExpression); diff --git a/tests/Aspire.Hosting.Tests/MongoDB/AddMongoDBTests.cs b/tests/Aspire.Hosting.Tests/MongoDB/AddMongoDBTests.cs index bbff7a1c5cf..302dfab1987 100644 --- a/tests/Aspire.Hosting.Tests/MongoDB/AddMongoDBTests.cs +++ b/tests/Aspire.Hosting.Tests/MongoDB/AddMongoDBTests.cs @@ -75,7 +75,7 @@ public void AddMongoDBContainerAddsAnnotationMetadata() } [Fact] - public void MongoDBCreatesConnectionString() + public async Task MongoDBCreatesConnectionString() { var appBuilder = DistributedApplication.CreateBuilder(); appBuilder @@ -88,9 +88,9 @@ public void MongoDBCreatesConnectionString() var appModel = app.Services.GetRequiredService(); var connectionStringResource = Assert.Single(appModel.Resources.OfType()); - var connectionString = connectionStringResource.GetConnectionString(); + var connectionString = await connectionStringResource.GetConnectionStringAsync(default); - Assert.Equal("mongodb://localhost:27017/", connectionStringResource.Parent.GetConnectionString()); + Assert.Equal("mongodb://localhost:27017", await connectionStringResource.Parent.GetConnectionStringAsync(default)); Assert.Equal("mongodb://{mongodb.bindings.tcp.host}:{mongodb.bindings.tcp.port}", connectionStringResource.Parent.ConnectionStringExpression); Assert.Equal("mongodb://localhost:27017/mydatabase", connectionString); Assert.Equal("{mongodb.connectionString}/mydatabase", connectionStringResource.ConnectionStringExpression); diff --git a/tests/Aspire.Hosting.Tests/MySql/AddMySqlTests.cs b/tests/Aspire.Hosting.Tests/MySql/AddMySqlTests.cs index bbeaa6974e0..00cf58a90e5 100644 --- a/tests/Aspire.Hosting.Tests/MySql/AddMySqlTests.cs +++ b/tests/Aspire.Hosting.Tests/MySql/AddMySqlTests.cs @@ -94,7 +94,7 @@ public async Task AddMySqlAddsAnnotationMetadata() } [Fact] - public void MySqlCreatesConnectionString() + public async Task MySqlCreatesConnectionString() { var appBuilder = DistributedApplication.CreateBuilder(); appBuilder.AddMySql("mysql") @@ -105,14 +105,14 @@ public void MySqlCreatesConnectionString() var appModel = app.Services.GetRequiredService(); var connectionStringResource = Assert.Single(appModel.Resources.OfType()); - var connectionString = connectionStringResource.GetConnectionString(); + var connectionString = await connectionStringResource.GetConnectionStringAsync(); Assert.Equal("Server={mysql.bindings.tcp.host};Port={mysql.bindings.tcp.port};User ID=root;Password={mysql.inputs.password}", connectionStringResource.ConnectionStringExpression); Assert.StartsWith("Server=localhost;Port=2000;User ID=root;Password=", connectionString); } [Fact] - public void MySqlCreatesConnectionStringWithDatabase() + public async Task MySqlCreatesConnectionStringWithDatabase() { var appBuilder = DistributedApplication.CreateBuilder(); appBuilder.AddMySql("mysql") @@ -124,9 +124,9 @@ public void MySqlCreatesConnectionStringWithDatabase() var appModel = app.Services.GetRequiredService(); var mySqlResource = Assert.Single(appModel.Resources.OfType()); - var mySqlConnectionString = mySqlResource.GetConnectionString(); + var mySqlConnectionString = await mySqlResource.GetConnectionStringAsync(default); var mySqlDatabaseResource = Assert.Single(appModel.Resources.OfType()); - var dbConnectionString = mySqlDatabaseResource.GetConnectionString(); + var dbConnectionString = await mySqlDatabaseResource.GetConnectionStringAsync(default); Assert.Equal(mySqlConnectionString + ";Database=db", dbConnectionString); Assert.Equal("{mysql.connectionString};Database=db", mySqlDatabaseResource.ConnectionStringExpression); diff --git a/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs b/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs index 224ed1ecddc..c2bb2de96f3 100644 --- a/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs +++ b/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs @@ -92,7 +92,7 @@ public async Task AddOracleAddsAnnotationMetadata() } [Fact] - public void OracleCreatesConnectionString() + public async Task OracleCreatesConnectionString() { var appBuilder = DistributedApplication.CreateBuilder(); appBuilder.AddOracle("orcl") @@ -103,15 +103,15 @@ public void OracleCreatesConnectionString() var appModel = app.Services.GetRequiredService(); var connectionStringResource = Assert.Single(appModel.Resources.OfType()); - var connectionString = connectionStringResource.GetConnectionString(); + var connectionString = await connectionStringResource.GetConnectionStringAsync(default); - Assert.Equal("user id=system;password={orcl.inputs.password};data source={orcl.bindings.tcp.host}:{orcl.bindings.tcp.port};", connectionStringResource.ConnectionStringExpression); + Assert.Equal("user id=system;password={orcl.inputs.password};data source={orcl.bindings.tcp.host}:{orcl.bindings.tcp.port}", connectionStringResource.ConnectionStringExpression); Assert.StartsWith("user id=system;password=", connectionString); Assert.EndsWith(";data source=localhost:2000", connectionString); } [Fact] - public void OracleCreatesConnectionStringWithDatabase() + public async void OracleCreatesConnectionStringWithDatabase() { var appBuilder = DistributedApplication.CreateBuilder(); appBuilder.AddOracle("orcl") @@ -123,9 +123,9 @@ public void OracleCreatesConnectionStringWithDatabase() var appModel = app.Services.GetRequiredService(); var oracleResource = Assert.Single(appModel.Resources.OfType()); - var oracleConnectionString = oracleResource.GetConnectionString(); + var oracleConnectionString = oracleResource.GetConnectionStringAsync(default); var oracleDatabaseResource = Assert.Single(appModel.Resources.OfType()); - var dbConnectionString = oracleDatabaseResource.GetConnectionString(); + var dbConnectionString = await oracleDatabaseResource.GetConnectionStringAsync(default); Assert.Equal("{orcl.connectionString}/db", oracleDatabaseResource.ConnectionStringExpression); Assert.Equal(oracleConnectionString + "/db", dbConnectionString); @@ -185,7 +185,7 @@ public async Task VerifyManifest() var expectedManifest = """ { "type": "container.v0", - "connectionString": "user id=system;password={oracle.inputs.password};data source={oracle.bindings.tcp.host}:{oracle.bindings.tcp.port};", + "connectionString": "user id=system;password={oracle.inputs.password};data source={oracle.bindings.tcp.host}:{oracle.bindings.tcp.port}", "image": "container-registry.oracle.com/database/free:23.3.0.0", "env": { "ORACLE_PWD": "{oracle.inputs.password}" diff --git a/tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs b/tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs index e846bb31d59..ce04d105f6f 100644 --- a/tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs +++ b/tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs @@ -114,19 +114,19 @@ public async Task AddPostgresAddsAnnotationMetadata() } [Fact] - public void PostgresCreatesConnectionString() + public async Task PostgresCreatesConnectionString() { var appBuilder = DistributedApplication.CreateBuilder(); var postgres = appBuilder.AddPostgres("postgres") .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000)); - var connectionString = postgres.Resource.GetConnectionString(); + var connectionString = await postgres.Resource.GetConnectionStringAsync(); Assert.Equal("Host={postgres.bindings.tcp.host};Port={postgres.bindings.tcp.port};Username=postgres;Password={postgres.inputs.password}", postgres.Resource.ConnectionStringExpression); Assert.Equal($"Host=localhost;Port=2000;Username=postgres;Password={PasswordUtil.EscapePassword(postgres.Resource.Password)}", connectionString); } [Fact] - public void PostgresCreatesConnectionStringWithDatabase() + public async Task PostgresCreatesConnectionStringWithDatabase() { var appBuilder = DistributedApplication.CreateBuilder(); appBuilder.AddPostgres("postgres") @@ -138,9 +138,9 @@ public void PostgresCreatesConnectionStringWithDatabase() var appModel = app.Services.GetRequiredService(); var postgresResource = Assert.Single(appModel.Resources.OfType()); - var postgresConnectionString = postgresResource.GetConnectionString(); + var postgresConnectionString = await postgresResource.GetConnectionStringAsync(); var postgresDatabaseResource = Assert.Single(appModel.Resources.OfType()); - var dbConnectionString = postgresDatabaseResource.GetConnectionString(); + var dbConnectionString = await postgresDatabaseResource.GetConnectionStringAsync(default); Assert.Equal("{postgres.connectionString};Database=db", postgresDatabaseResource.ConnectionStringExpression); Assert.Equal(postgresConnectionString + ";Database=db", dbConnectionString); diff --git a/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs b/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs index 6947fde7d3a..c083e1ef5c4 100644 --- a/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs +++ b/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs @@ -43,7 +43,7 @@ public void AddRabbitMQContainerWithDefaultsAddsAnnotationMetadata() } [Fact] - public void RabbitMQCreatesConnectionString() + public async Task RabbitMQCreatesConnectionString() { var appBuilder = DistributedApplication.CreateBuilder(); appBuilder @@ -55,7 +55,7 @@ public void RabbitMQCreatesConnectionString() var appModel = app.Services.GetRequiredService(); var connectionStringResource = Assert.Single(appModel.Resources.OfType()); - var connectionString = connectionStringResource.GetConnectionString(); + var connectionString = await connectionStringResource.GetConnectionStringAsync(default); var password = connectionStringResource.Password; Assert.Equal($"amqp://guest:{password}@localhost:27011", connectionString); diff --git a/tests/Aspire.Hosting.Tests/Redis/AddRedisTests.cs b/tests/Aspire.Hosting.Tests/Redis/AddRedisTests.cs index 751eea4fd5b..e423320f275 100644 --- a/tests/Aspire.Hosting.Tests/Redis/AddRedisTests.cs +++ b/tests/Aspire.Hosting.Tests/Redis/AddRedisTests.cs @@ -75,7 +75,7 @@ public void AddRedisContainerAddsAnnotationMetadata() } [Fact] - public void RedisCreatesConnectionString() + public async Task RedisCreatesConnectionString() { var appBuilder = DistributedApplication.CreateBuilder(); appBuilder.AddRedis("myRedis") @@ -86,7 +86,7 @@ public void RedisCreatesConnectionString() var appModel = app.Services.GetRequiredService(); var connectionStringResource = Assert.Single(appModel.Resources.OfType()); - var connectionString = connectionStringResource.GetConnectionString(); + var connectionString = await connectionStringResource.GetConnectionStringAsync(default); Assert.Equal("{myRedis.bindings.tcp.host}:{myRedis.bindings.tcp.port}", connectionStringResource.ConnectionStringExpression); Assert.StartsWith("localhost:2000", connectionString); } diff --git a/tests/Aspire.Hosting.Tests/ResourceNotificationTests.cs b/tests/Aspire.Hosting.Tests/ResourceNotificationTests.cs index 5fae9a24300..d59dd3f657c 100644 --- a/tests/Aspire.Hosting.Tests/ResourceNotificationTests.cs +++ b/tests/Aspire.Hosting.Tests/ResourceNotificationTests.cs @@ -150,6 +150,6 @@ private sealed class CustomResource(string name) : Resource(name), IResourceWithEnvironment, IResourceWithConnectionString { - public string? GetConnectionString() => "CustomConnectionString"; + public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken) => new("CustomConnectionString"); } } diff --git a/tests/Aspire.Hosting.Tests/SqlServer/AddSqlServerTests.cs b/tests/Aspire.Hosting.Tests/SqlServer/AddSqlServerTests.cs index a0aaed675da..eac3cf765d4 100644 --- a/tests/Aspire.Hosting.Tests/SqlServer/AddSqlServerTests.cs +++ b/tests/Aspire.Hosting.Tests/SqlServer/AddSqlServerTests.cs @@ -59,7 +59,7 @@ public async Task AddSqlServerContainerWithDefaultsAddsAnnotationMetadata() } [Fact] - public void SqlServerCreatesConnectionString() + public async Task SqlServerCreatesConnectionString() { var appBuilder = DistributedApplication.CreateBuilder(); appBuilder @@ -71,7 +71,7 @@ public void SqlServerCreatesConnectionString() var appModel = app.Services.GetRequiredService(); var connectionStringResource = Assert.Single(appModel.Resources.OfType()); - var connectionString = connectionStringResource.GetConnectionString(); + var connectionString = await connectionStringResource.GetConnectionStringAsync(default); var password = PasswordUtil.EscapePassword(connectionStringResource.Password); Assert.Equal($"Server=127.0.0.1,1433;User ID=sa;Password={password};TrustServerCertificate=true", connectionString); @@ -79,7 +79,7 @@ public void SqlServerCreatesConnectionString() } [Fact] - public void SqlServerDatabaseCreatesConnectionString() + public async Task SqlServerDatabaseCreatesConnectionString() { var appBuilder = DistributedApplication.CreateBuilder(); appBuilder @@ -92,7 +92,7 @@ public void SqlServerDatabaseCreatesConnectionString() var appModel = app.Services.GetRequiredService(); var connectionStringResource = Assert.Single(appModel.Resources.OfType()); - var connectionString = connectionStringResource.GetConnectionString(); + var connectionString = await connectionStringResource.GetConnectionStringAsync(default); var password = PasswordUtil.EscapePassword(connectionStringResource.Parent.Password); Assert.Equal($"Server=127.0.0.1,1433;User ID=sa;Password={password};TrustServerCertificate=true;Database=mydb", connectionString); diff --git a/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs b/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs index 9a185e06d75..3eed28bf8b1 100644 --- a/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs +++ b/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs @@ -135,5 +135,49 @@ public async Task ComplexEnvironmentCallbackPopulatesValueWhenCalled() Assert.Equal(1, servicesKeysCount); Assert.Contains(config, kvp => kvp.Key == "myName" && kvp.Value == "value2"); } + + [Fact] + public async Task EnvironmentVariableExpressions() + { + var builder = DistributedApplication.CreateBuilder(); + + var test = builder.AddResource(new TestResource("test", "connectionString")); + + var container = builder.AddContainer("container1", "image") + .WithHttpEndpoint(name: "primary") + .WithEndpoint("primary", ep => + { + ep.AllocatedEndpoint = new AllocatedEndpoint(ep, "localhost", 90); + }); + + var endpoint = container.GetEndpoint("primary"); + + var containerB = builder.AddContainer("container2", "imageB") + .WithEnvironment("URL", $"{endpoint}/foo") + .WithEnvironment("PORT", $"{endpoint.Property(EndpointProperty.Port)}") + .WithEnvironment("HOST", $"{test.Resource};name=1"); + + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(containerB.Resource); + var manifestConfig = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(containerB.Resource, DistributedApplicationOperation.Publish); + + Assert.Equal(3, config.Count); + Assert.Equal($"http://localhost:90/foo", config["URL"]); + Assert.Equal("90", config["PORT"]); + Assert.Equal("connectionString;name=1", config["HOST"]); + + Assert.Equal(3, manifestConfig.Count); + Assert.Equal("{container1.bindings.primary.url}/foo", manifestConfig["URL"]); + Assert.Equal("{container1.bindings.primary.port}", manifestConfig["PORT"]); + Assert.Equal("{test.connectionString};name=1", manifestConfig["HOST"]); + } + + private sealed class TestResource(string name, string connectionString) : Resource(name), IResourceWithConnectionString + { + public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) + { + return new(connectionString); + } + } + private static TestProgram CreateTestProgram(string[]? args = null) => TestProgram.Create(args); } diff --git a/tests/Aspire.Hosting.Tests/WithReferenceTests.cs b/tests/Aspire.Hosting.Tests/WithReferenceTests.cs index 95201b5a290..6441ca2d555 100644 --- a/tests/Aspire.Hosting.Tests/WithReferenceTests.cs +++ b/tests/Aspire.Hosting.Tests/WithReferenceTests.cs @@ -330,9 +330,9 @@ private sealed class TestResource(string name) : IResourceWithConnectionString public ResourceAnnotationCollection Annotations => throw new NotImplementedException(); - public string? GetConnectionString() + public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken) { - return ConnectionString; + return new(ConnectionString); } } } From 1ec8d16e2f8f717cedb3b0b61d6bb1b0cc5cd82b Mon Sep 17 00:00:00 2001 From: David Fowler Date: Sun, 10 Mar 2024 14:41:35 -0700 Subject: [PATCH 28/50] Use ResourceLoggerService and ResourceNotification service for DCP based resource changes (#2731) * Move DCP resource updates and logs to use apphost APIs - Moved all of the DCP logic calling into k8s APIs into the ApplicationExecutor. - Added an overload of WatchAsync that watches all resources. This is what the dashboard uses. - Watch logs as soon as they become available an push them through the ResourceLoggerService API. This makes them avaialble to anyone looking for logs. - Support sending updates based on IResource and a unique resource id. This is how we can support unique snapshots for replicas. - Introduce a "Hidden" state to allow hiding resources from the dashboard views. Use this to hide app model resources after DCP has loaded the resource. - Watching resource state changes are global. This makes it possible to listen to changes to replicas since they aren't directly represented in the model, but they are still associated with an IResource. - Publish the initial state of any resource that has a ResourceSnapshot annotation. * Resource state can't be null --- .../Components/Pages/ConsoleLogs.razor.cs | 7 +- .../Components/Pages/Resources.razor.cs | 13 +- .../StateColumnDisplay.razor | 19 +- .../Model/ResourceEndpointHelpers.cs | 4 +- .../Model/ResourceViewModel.cs | 11 +- .../Provisioners/BicepProvisioner.cs | 12 +- .../CustomResourceSnapshot.cs | 19 +- .../ApplicationModel/EndpointAnnotation.cs | 5 + .../ApplicationModel/ResourceLoggerService.cs | 42 +- .../ResourceNotificationService.cs | 180 +++--- .../Dashboard/ConsoleLogPublisher.cs | 83 --- .../Dashboard/ContainerSnapshot.cs | 31 - .../Dashboard/DashboardServiceData.cs | 75 ++- src/Aspire.Hosting/Dashboard/DcpDataSource.cs | 567 ------------------ .../Dashboard/ExecutableSnapshot.cs | 33 - .../Dashboard/GenericResourceSnapshot.cs | 13 +- .../Dashboard/ProjectSnapshot.cs | 27 - .../Dashboard/ResourceSnapshot.cs | 2 +- src/Aspire.Hosting/Dcp/ApplicationExecutor.cs | 533 +++++++++++++++- .../DockerContainerLogSource.cs | 2 +- .../{Dashboard => Dcp}/FileLogSource.cs | 2 +- src/Aspire.Hosting/Dcp/Model/ModelCommon.cs | 1 + .../{Dashboard => Dcp}/ResourceLogSource.cs | 9 +- .../ProjectResourceBuilderExtensions.cs | 1 + .../Dashboard/ResourcePublisherTests.cs | 14 +- .../Dcp/ApplicationExecutorTests.cs | 2 +- .../ResourceNotificationTests.cs | 131 ++-- 27 files changed, 863 insertions(+), 975 deletions(-) delete mode 100644 src/Aspire.Hosting/Dashboard/ConsoleLogPublisher.cs delete mode 100644 src/Aspire.Hosting/Dashboard/ContainerSnapshot.cs delete mode 100644 src/Aspire.Hosting/Dashboard/DcpDataSource.cs delete mode 100644 src/Aspire.Hosting/Dashboard/ExecutableSnapshot.cs delete mode 100644 src/Aspire.Hosting/Dashboard/ProjectSnapshot.cs rename src/Aspire.Hosting/{Dashboard => Dcp}/DockerContainerLogSource.cs (99%) rename src/Aspire.Hosting/{Dashboard => Dcp}/FileLogSource.cs (99%) rename src/Aspire.Hosting/{Dashboard => Dcp}/ResourceLogSource.cs (92%) diff --git a/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.cs b/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.cs index 5fef37dfa60..676fdb52875 100644 --- a/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.cs +++ b/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.cs @@ -170,7 +170,10 @@ private void UpdateResourcesList() var builder = ImmutableList.CreateBuilder>(); builder.Add(_noSelection); - foreach (var resourceGroupsByApplicationName in _resourceByName.Values.OrderBy(c => c.Name).GroupBy(resource => resource.DisplayName)) + foreach (var resourceGroupsByApplicationName in _resourceByName + .Where(r => r.Value.State != ResourceStates.HiddenState) + .OrderBy(c => c.Value.Name) + .GroupBy(r => r.Value.DisplayName, r => r.Value)) { if (resourceGroupsByApplicationName.Count() > 1) { @@ -203,7 +206,7 @@ SelectViewModel ToOption(ResourceViewModel resource, bool i string GetDisplayText() { - var resourceName = ResourceViewModel.GetResourceName(resource, _resourceByName.Values); + var resourceName = ResourceViewModel.GetResourceName(resource, _resourceByName); return resource.State switch { diff --git a/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs b/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs index 091a9dca3a4..c6ccfcc3b44 100644 --- a/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs +++ b/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs @@ -44,7 +44,7 @@ public Resources() _visibleResourceTypes = new(StringComparers.ResourceType); } - private bool Filter(ResourceViewModel resource) => _visibleResourceTypes.ContainsKey(resource.ResourceType) && (_filter.Length == 0 || resource.MatchesFilter(_filter)); + private bool Filter(ResourceViewModel resource) => _visibleResourceTypes.ContainsKey(resource.ResourceType) && (_filter.Length == 0 || resource.MatchesFilter(_filter)) && resource.State != ResourceStates.HiddenState; protected void OnResourceTypeVisibilityChanged(string resourceType, bool isVisible) { @@ -101,7 +101,7 @@ static bool UnionWithKeys(ConcurrentDictionary left, ConcurrentDic } } - private bool HasResourcesWithCommands => _resourceByName.Values.Any(r => r.Commands.Any()); + private bool HasResourcesWithCommands => _resourceByName.Any(r => r.Value.Commands.Any()); private IQueryable? FilteredResources => _resourceByName.Values.Where(Filter).OrderBy(e => e.ResourceType).ThenBy(e => e.Name).AsQueryable(); @@ -207,13 +207,18 @@ private void ClearSelectedResource() SelectedResource = null; } - private string GetResourceName(ResourceViewModel resource) => ResourceViewModel.GetResourceName(resource, _resourceByName.Values); + private string GetResourceName(ResourceViewModel resource) => ResourceViewModel.GetResourceName(resource, _resourceByName); private bool HasMultipleReplicas(ResourceViewModel resource) { var count = 0; - foreach (var item in _resourceByName.Values) + foreach (var (_, item) in _resourceByName) { + if (item.State == ResourceStates.HiddenState) + { + continue; + } + if (item.DisplayName == resource.DisplayName) { count++; diff --git a/src/Aspire.Dashboard/Components/ResourcesGridColumns/StateColumnDisplay.razor b/src/Aspire.Dashboard/Components/ResourcesGridColumns/StateColumnDisplay.razor index b4f6e6a1f2a..c2fd7674b24 100644 --- a/src/Aspire.Dashboard/Components/ResourcesGridColumns/StateColumnDisplay.razor +++ b/src/Aspire.Dashboard/Components/ResourcesGridColumns/StateColumnDisplay.razor @@ -13,7 +13,7 @@ + Class="severity-icon" /> } else { @@ -21,16 +21,16 @@ + Class="severity-icon" /> } } else if (Resource is { State: ResourceStates.StartingState }) { + Class="severity-icon" /> } -else if (Resource is { State: /* unknown */ null }) +else if (Resource is { State: /* unknown */ null or { Length: 0 } }) { + Class="severity-icon" /> } -@(Resource.State?.Humanize() ?? "Unknown") +@if (string.IsNullOrEmpty(Resource.State)) +{ + Unknown +} +else +{ + @Resource.State.Humanize() +} diff --git a/src/Aspire.Dashboard/Model/ResourceEndpointHelpers.cs b/src/Aspire.Dashboard/Model/ResourceEndpointHelpers.cs index 6509e8a93c9..4e35a6279f9 100644 --- a/src/Aspire.Dashboard/Model/ResourceEndpointHelpers.cs +++ b/src/Aspire.Dashboard/Model/ResourceEndpointHelpers.cs @@ -12,10 +12,10 @@ internal static class ResourceEndpointHelpers /// public static List GetEndpoints(ILogger logger, ResourceViewModel resource, bool excludeServices = false, bool includeEndpointUrl = false) { - var isKnownResourceType = resource.IsContainer() || resource.IsExecutable(allowSubtypes: false) || resource.IsProject(); - var displayedEndpoints = new List(); + var isKnownResourceType = resource.IsContainer() || resource.IsExecutable(allowSubtypes: false) || resource.IsProject(); + if (isKnownResourceType) { if (!excludeServices) diff --git a/src/Aspire.Dashboard/Model/ResourceViewModel.cs b/src/Aspire.Dashboard/Model/ResourceViewModel.cs index 8fac9b3fe48..4613ee6d819 100644 --- a/src/Aspire.Dashboard/Model/ResourceViewModel.cs +++ b/src/Aspire.Dashboard/Model/ResourceViewModel.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 System.Collections.Concurrent; using System.Collections.Frozen; using System.Collections.Immutable; using System.Diagnostics; @@ -30,11 +31,16 @@ internal bool MatchesFilter(string filter) return Name.Contains(filter, StringComparisons.UserTextSearch); } - public static string GetResourceName(ResourceViewModel resource, IEnumerable allResources) + public static string GetResourceName(ResourceViewModel resource, ConcurrentDictionary allResources) { var count = 0; - foreach (var item in allResources) + foreach (var (_, item) in allResources) { + if (item.State == ResourceStates.HiddenState) + { + continue; + } + if (item.DisplayName == resource.DisplayName) { count++; @@ -127,4 +133,5 @@ public static class ResourceStates public const string FailedToStartState = "FailedToStart"; public const string StartingState = "Starting"; public const string RunningState = "Running"; + public const string HiddenState = "Hidden"; } diff --git a/src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs b/src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs index 08b1a6aa5fc..ba444e313fa 100644 --- a/src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs +++ b/src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs @@ -85,13 +85,13 @@ public override async Task ConfigureResourceAsync(IConfiguration configura await notificationService.PublishUpdateAsync(resource, state => { - ImmutableArray<(string, string)> props = [ + ImmutableArray<(string, object?)> props = [ .. state.Properties, - ("azure.subscription.id", configuration["Azure:SubscriptionId"] ?? ""), + ("azure.subscription.id", configuration["Azure:SubscriptionId"]), // ("azure.resource.group", configuration["Azure:ResourceGroup"]!), - ("azure.tenant.domain", configuration["Azure:Tenant"] ?? ""), - ("azure.location", configuration["Azure:Location"] ?? ""), - (CustomResourceKnownProperties.Source, section["Id"] ?? "") + ("azure.tenant.domain", configuration["Azure:Tenant"]), + ("azure.location", configuration["Azure:Location"]), + (CustomResourceKnownProperties.Source, section["Id"]) ]; return state with @@ -308,7 +308,7 @@ await notificationService.PublishUpdateAsync(resource, state => await notificationService.PublishUpdateAsync(resource, state => { - ImmutableArray<(string, string)> properties = [ + ImmutableArray<(string, object?)> properties = [ .. state.Properties, (CustomResourceKnownProperties.Source, deployment.Id.Name) ]; diff --git a/src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs b/src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs index 7ec98d577f7..4044d4e9a56 100644 --- a/src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs +++ b/src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs @@ -18,7 +18,7 @@ public sealed record CustomResourceSnapshot /// /// The properties that should show up in the dashboard for this resource. /// - public required ImmutableArray<(string Key, string Value)> Properties { get; init; } + public required ImmutableArray<(string Key, object? Value)> Properties { get; init; } /// /// The creation timestamp of the resource. @@ -30,13 +30,28 @@ public sealed record CustomResourceSnapshot /// public string? State { get; init; } + /// + /// The exit code of the resource. + /// + public int? ExitCode { get; init; } + /// /// The environment variables that should show up in the dashboard for this resource. /// - public ImmutableArray<(string Name, string Value)> EnvironmentVariables { get; init; } = []; + public ImmutableArray<(string Name, string Value, bool IsFromSpec)> EnvironmentVariables { get; init; } = []; /// /// The URLs that should show up in the dashboard for this resource. /// public ImmutableArray<(string Name, string Url)> Urls { get; init; } = []; + + /// + /// The services that should show up in the dashboard for this resource. + /// + public ImmutableArray<(string Name, string? AllocatedAddress, int? AllocatedPort)> Services { get; init; } = []; + + /// + /// The endpoints that should show up in the dashboard for this resource. + /// + public ImmutableArray<(string EndpointUrl, string ProxyUrl)> Endpoints { get; init; } = []; } diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs index cfa7739b767..3ebc67a2aaf 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs @@ -105,6 +105,11 @@ public string Transport /// Defaults to true. public bool IsProxied { get; set; } = true; + /// + /// Gets or sets a value indicating whether the endpoint is from a launch profile. + /// + internal bool FromLaunchProfile { get; set; } + /// /// Gets or sets the allocated endpoint. /// diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceLoggerService.cs b/src/Aspire.Hosting/ApplicationModel/ResourceLoggerService.cs index 13bc79e1e68..4a9d6fad972 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceLoggerService.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceLoggerService.cs @@ -23,6 +23,14 @@ public class ResourceLoggerService public ILogger GetLogger(IResource resource) => GetResourceLoggerState(resource.Name).Logger; + /// + /// Gets the logger for the resource to write to. + /// + /// + /// + public ILogger GetLogger(string resourceName) => + GetResourceLoggerState(resourceName).Logger; + /// /// Watch for changes to the log stream for a resource. /// @@ -50,6 +58,19 @@ public void Complete(IResource resource) logger.Complete(); } } + + /// + /// Completes the log stream for the resource. + /// + /// The name of the resource. + public void Complete(string name) + { + if (_loggers.TryGetValue(name, out var logger)) + { + logger.Complete(); + } + } + private ResourceLoggerState GetResourceLoggerState(string resourceName) => _loggers.GetOrAdd(resourceName, _ => new ResourceLoggerState()); @@ -76,7 +97,14 @@ public ResourceLoggerState() /// Watch for changes to the log stream for a resource. /// /// The log stream for the resource. - public IAsyncEnumerable> WatchAsync() => new LogAsyncEnumerable(this); + public IAsyncEnumerable> WatchAsync() + { + lock (_backlog) + { + // REVIEW: Performance makes me very sad, but we can optimize this later. + return new LogAsyncEnumerable(this, _backlog.ToList()); + } + } // This provides the fan out to multiple subscribers. private Action<(string, bool)>? OnNewLog { get; set; } @@ -123,19 +151,13 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except } } - private sealed class LogAsyncEnumerable(ResourceLoggerState annotation) : IAsyncEnumerable> + private sealed class LogAsyncEnumerable(ResourceLoggerState annotation, List<(string, bool)> backlogSnapshot) : IAsyncEnumerable> { public async IAsyncEnumerator> GetAsyncEnumerator(CancellationToken cancellationToken = default) { - // Yield the backlog first. - - lock (annotation._backlog) + if (backlogSnapshot.Count > 0) { - if (annotation._backlog.Count > 0) - { - // REVIEW: Performance makes me very sad, but we can optimize this later. - yield return annotation._backlog.ToList(); - } + yield return backlogSnapshot; } var channel = Channel.CreateUnbounded<(string, bool)>(); diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceNotificationService.cs b/src/Aspire.Hosting/ApplicationModel/ResourceNotificationService.cs index 7267a48d42a..a3653c9ae97 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceNotificationService.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceNotificationService.cs @@ -3,43 +3,36 @@ using System.Collections.Concurrent; using System.Threading.Channels; +using Microsoft.Extensions.Logging; namespace Aspire.Hosting.ApplicationModel; /// /// A service that allows publishing and subscribing to changes in the state of a resource. /// -public class ResourceNotificationService +public class ResourceNotificationService(ILogger logger) { - private readonly ConcurrentDictionary _resourceNotificationStates = new(); + // Resource state is keyed by the resource and the unique name of the resource. This could be the name of the resource, or a replica ID. + private readonly ConcurrentDictionary<(IResource, string), ResourceNotificationState> _resourceNotificationStates = new(); + + private Action? OnResourceUpdated { get; set; } /// - /// Watch for changes to the dashboard state for a resource. + /// Watch for changes to the state for all resources. /// - /// The name of the resource /// - public IAsyncEnumerable WatchAsync(IResource resource) - { - var notificationState = GetResourceNotificationState(resource.Name); - - lock (notificationState) - { - // When watching a resource, make sure the initial snapshot is set. - notificationState.LastSnapshot = GetInitialSnapshot(resource, notificationState); - } - - return notificationState.WatchAsync(); - } + public IAsyncEnumerable WatchAsync() => + new AllResourceUpdatesAsyncEnumerable(this); /// /// Updates the snapshot of the for a resource. /// - /// - /// - /// - public Task PublishUpdateAsync(IResource resource, Func stateFactory) + /// The resource to update + /// The id of the resource. + /// A factory that creates the new state based on the previous state. + public Task PublishUpdateAsync(IResource resource, string resourceId, Func stateFactory) { - var notificationState = GetResourceNotificationState(resource.Name); + var notificationState = GetResourceNotificationState(resource, resourceId); lock (notificationState) { @@ -49,10 +42,27 @@ public Task PublishUpdateAsync(IResource resource, Func {State}", resource.Name, resourceId, newState.State); + } + + return Task.CompletedTask; } } + /// + /// Updates the snapshot of the for a resource. + /// + /// The resource to update + /// A factory that creates the new state based on the previous state. + public Task PublishUpdateAsync(IResource resource, Func stateFactory) + { + return PublishUpdateAsync(resource, resource.Name, stateFactory); + } + private static CustomResourceSnapshot? GetInitialSnapshot(IResource resource, ResourceNotificationState notificationState) { var previousState = notificationState.LastSnapshot; @@ -75,92 +85,76 @@ public Task PublishUpdateAsync(IResource resource, Func - /// Signal that no more updates are expected for this resource. - /// - public void Complete(IResource resource) - { - if (_resourceNotificationStates.TryGetValue(resource.Name, out var state)) - { - state.Complete(); - } - } - - private ResourceNotificationState GetResourceNotificationState(string resourceName) => - _resourceNotificationStates.GetOrAdd(resourceName, _ => new ResourceNotificationState()); + private ResourceNotificationState GetResourceNotificationState(IResource resource, string resourceId) => + _resourceNotificationStates.GetOrAdd((resource, resourceId), _ => new ResourceNotificationState()); - /// - /// The annotation that allows publishing and subscribing to changes in the state of a resource. - /// - private sealed class ResourceNotificationState + private sealed class AllResourceUpdatesAsyncEnumerable(ResourceNotificationService resourceNotificationService) : IAsyncEnumerable { - private readonly CancellationTokenSource _streamClosedCts = new(); - - private Action? OnSnapshotUpdated { get; set; } - - public CustomResourceSnapshot? LastSnapshot { get; set; } - - /// - /// Watch for changes to the dashboard state for a resource. - /// - public IAsyncEnumerable WatchAsync() => new ResourceUpdatesAsyncEnumerable(this); - - /// - /// Updates the snapshot of the for a resource. - /// - /// The new . - public Task PublishUpdateAsync(CustomResourceSnapshot state) + public async IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) { - if (_streamClosedCts.IsCancellationRequested) + // Return the last snapshot for each resource. + foreach (var state in resourceNotificationService._resourceNotificationStates) { - return Task.CompletedTask; - } - - OnSnapshotUpdated?.Invoke(state); - - return Task.CompletedTask; - } + var (resource, resourceId) = state.Key; - /// - /// Signal that no more updates are expected for this resource. - /// - public void Complete() - { - _streamClosedCts.Cancel(); - } - - private sealed class ResourceUpdatesAsyncEnumerable(ResourceNotificationState customResourceAnnotation) : IAsyncEnumerable - { - public async IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) - { - if (customResourceAnnotation.LastSnapshot is not null) + if (state.Value.LastSnapshot is not null) { - yield return customResourceAnnotation.LastSnapshot; + yield return new ResourceEvent(resource, resourceId, state.Value.LastSnapshot); } + } - var channel = Channel.CreateUnbounded(); - - void WriteToChannel(CustomResourceSnapshot state) - => channel.Writer.TryWrite(state); + var channel = Channel.CreateUnbounded(); - using var _ = customResourceAnnotation._streamClosedCts.Token.Register(() => channel.Writer.TryComplete()); + void WriteToChannel(ResourceEvent resourceEvent) => + channel.Writer.TryWrite(resourceEvent); - customResourceAnnotation.OnSnapshotUpdated = WriteToChannel; + resourceNotificationService.OnResourceUpdated += WriteToChannel; - try + try + { + await foreach (var item in channel.Reader.ReadAllAsync(cancellationToken)) { - await foreach (var item in channel.Reader.ReadAllAsync(cancellationToken)) - { - yield return item; - } + yield return item; } - finally - { - customResourceAnnotation.OnSnapshotUpdated -= WriteToChannel; + } + finally + { + resourceNotificationService.OnResourceUpdated -= WriteToChannel; - channel.Writer.TryComplete(); - } + channel.Writer.TryComplete(); } } } + + /// + /// The annotation that allows publishing and subscribing to changes in the state of a resource. + /// + private sealed class ResourceNotificationState + { + public CustomResourceSnapshot? LastSnapshot { get; set; } + } +} + +/// +/// Represents a change in the state of a resource. +/// +/// The resource associated with the event. +/// The unique id of the resource. +/// The snapshot of the resource state. +public class ResourceEvent(IResource resource, string resourceId, CustomResourceSnapshot snapshot) +{ + /// + /// The resource associated with the event. + /// + public IResource Resource { get; } = resource; + + /// + /// The unique id of the resource. + /// + public string ResourceId { get; } = resourceId; + + /// + /// The snapshot of the resource state. + /// + public CustomResourceSnapshot Snapshot { get; } = snapshot; } diff --git a/src/Aspire.Hosting/Dashboard/ConsoleLogPublisher.cs b/src/Aspire.Hosting/Dashboard/ConsoleLogPublisher.cs deleted file mode 100644 index 2016b667f79..00000000000 --- a/src/Aspire.Hosting/Dashboard/ConsoleLogPublisher.cs +++ /dev/null @@ -1,83 +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 Aspire.Hosting.ApplicationModel; -using Aspire.Hosting.Dcp; -using Aspire.Hosting.Dcp.Model; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; - -namespace Aspire.Hosting.Dashboard; - -using LogsEnumerable = IAsyncEnumerable>; - -internal sealed class ConsoleLogPublisher( - ResourcePublisher resourcePublisher, - ResourceLoggerService resourceLoggerService, - IKubernetesService kubernetesService, - ILoggerFactory loggerFactory, - IConfiguration configuration) -{ - internal LogsEnumerable? Subscribe(string resourceName) - { - // Look up the requested resource, so we know how to obtain logs. - if (!resourcePublisher.TryGetResource(resourceName, out var resource)) - { - throw new ArgumentException($"Unknown resource {resourceName}.", nameof(resourceName)); - } - - // Obtain logs using the relevant approach. - if (configuration.GetBool("DOTNET_ASPIRE_USE_STREAMING_LOGS") is true) - { - return resource switch - { - ExecutableSnapshot executable => SubscribeExecutableResource(executable), - ContainerSnapshot container => SubscribeContainerResource(container), - GenericResourceSnapshot genericResource => resourceLoggerService.WatchAsync(genericResource.Name), - _ => throw new NotSupportedException($"Unsupported resource type {resource.GetType()}.") - }; - } - else - { - return resource switch - { - ExecutableSnapshot executable => SubscribeExecutable(executable), - ContainerSnapshot container => SubscribeContainer(container), - GenericResourceSnapshot genericResource => resourceLoggerService.WatchAsync(genericResource.Name), - _ => throw new NotSupportedException($"Unsupported resource type {resource.GetType()}.") - }; - } - - LogsEnumerable SubscribeExecutableResource(ExecutableSnapshot executable) - { - var executableIdentity = Executable.Create(executable.Name, string.Empty); - return new ResourceLogSource(loggerFactory, kubernetesService, executableIdentity); - } - - LogsEnumerable SubscribeContainerResource(ContainerSnapshot container) - { - var containerIdentity = Container.Create(container.Name, string.Empty); - return new ResourceLogSource(loggerFactory, kubernetesService, containerIdentity); - } - - static FileLogSource? SubscribeExecutable(ExecutableSnapshot executable) - { - if (executable.StdOutFile is null || executable.StdErrFile is null) - { - return null; - } - - return new FileLogSource(executable.StdOutFile, executable.StdErrFile); - } - - static DockerContainerLogSource? SubscribeContainer(ContainerSnapshot container) - { - if (container.ContainerId is null) - { - return null; - } - - return new DockerContainerLogSource(container.ContainerId); - } - } -} diff --git a/src/Aspire.Hosting/Dashboard/ContainerSnapshot.cs b/src/Aspire.Hosting/Dashboard/ContainerSnapshot.cs deleted file mode 100644 index be60c3a0853..00000000000 --- a/src/Aspire.Hosting/Dashboard/ContainerSnapshot.cs +++ /dev/null @@ -1,31 +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.Collections.Immutable; -using Aspire.Dashboard.Model; -using Google.Protobuf.WellKnownTypes; - -namespace Aspire.Hosting.Dashboard; - -/// -/// Immutable snapshot of a container's state at a point in time. -/// -internal sealed class ContainerSnapshot : ResourceSnapshot -{ - public override string ResourceType => KnownResourceTypes.Container; - - public required string? ContainerId { get; init; } - public required string Image { get; init; } - public required ImmutableArray Ports { get; init; } - public required string? Command { get; init; } - public required ImmutableArray? Args { get; init; } - - protected override IEnumerable<(string Key, Value Value)> GetProperties() - { - yield return (KnownProperties.Container.Id, ContainerId is null ? Value.ForNull() : Value.ForString(ContainerId)); - yield return (KnownProperties.Container.Image, Value.ForString(Image)); - yield return (KnownProperties.Container.Ports, Value.ForList(Ports.Select(port => Value.ForNumber(port)).ToArray())); - yield return (KnownProperties.Container.Command, Command is null ? Value.ForNull() : Value.ForString(Command)); - yield return (KnownProperties.Container.Args, Args is null ? Value.ForNull() : Value.ForList(Args.Value.Select(port => Value.ForString(port)).ToArray())); - } -} diff --git a/src/Aspire.Hosting/Dashboard/DashboardServiceData.cs b/src/Aspire.Hosting/Dashboard/DashboardServiceData.cs index 6761ba99940..297c35e34e3 100644 --- a/src/Aspire.Hosting/Dashboard/DashboardServiceData.cs +++ b/src/Aspire.Hosting/Dashboard/DashboardServiceData.cs @@ -1,10 +1,9 @@ // 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 System.Runtime.CompilerServices; using Aspire.Hosting.ApplicationModel; -using Aspire.Hosting.Dcp; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace Aspire.Hosting.Dashboard; @@ -17,22 +16,74 @@ internal sealed class DashboardServiceData : IAsyncDisposable { private readonly CancellationTokenSource _cts = new(); private readonly ResourcePublisher _resourcePublisher; - private readonly ConsoleLogPublisher _consoleLogPublisher; + private readonly ResourceLoggerService _resourceLoggerService; public DashboardServiceData( - DistributedApplicationModel applicationModel, - IKubernetesService kubernetesService, ResourceNotificationService resourceNotificationService, ResourceLoggerService resourceLoggerService, - IConfiguration configuration, - ILoggerFactory loggerFactory) + ILogger logger) { - var resourceMap = applicationModel.Resources.ToDictionary(resource => resource.Name, StringComparer.Ordinal); - + _resourceLoggerService = resourceLoggerService; _resourcePublisher = new ResourcePublisher(_cts.Token); - _consoleLogPublisher = new ConsoleLogPublisher(_resourcePublisher, resourceLoggerService, kubernetesService, loggerFactory, configuration); - _ = new DcpDataSource(kubernetesService, resourceNotificationService, resourceMap, configuration, loggerFactory, _resourcePublisher.IntegrateAsync, _cts.Token); + var cancellationToken = _cts.Token; + + Task.Run(async () => + { + static GenericResourceSnapshot CreateResourceSnapshot(IResource resource, string resourceId, DateTime creationTimestamp, CustomResourceSnapshot snapshot) + { + ImmutableArray environmentVariables = [.. + snapshot.EnvironmentVariables.Select(e => new EnvironmentVariableSnapshot(e.Name, e.Value, e.IsFromSpec))]; + + ImmutableArray services = + [ + ..snapshot.Urls.Select(u => new ResourceServiceSnapshot(u.Name, u.Url, null)), + ..snapshot.Services.Select(e => new ResourceServiceSnapshot(e.Name, e.AllocatedAddress, e.AllocatedPort)) + ]; + + ImmutableArray endpoints = [ + ..snapshot.Urls.Select(u => new EndpointSnapshot(u.Url, u.Url)), + ..snapshot.Endpoints.Select(e => new EndpointSnapshot(e.EndpointUrl, e.ProxyUrl)) + ]; + + return new GenericResourceSnapshot(snapshot) + { + Uid = resourceId, + CreationTimeStamp = snapshot.CreationTimeStamp ?? creationTimestamp, + Name = resourceId, + DisplayName = resource.Name, + Endpoints = endpoints, + Environment = environmentVariables, + ExitCode = snapshot.ExitCode, + ExpectedEndpointsCount = endpoints.Length, + Services = services, + State = snapshot.State + }; + } + + var timestamp = DateTime.UtcNow; + + await foreach (var @event in resourceNotificationService.WatchAsync().WithCancellation(cancellationToken)) + { + try + { + var snapshot = CreateResourceSnapshot(@event.Resource, @event.ResourceId, timestamp, @event.Snapshot); + + if (logger.IsEnabled(LogLevel.Debug)) + { + logger.LogDebug("Updating resource snapshot for {Name}/{DisplayName}: {State}", snapshot.Name, snapshot.DisplayName, snapshot.State); + } + + await _resourcePublisher.IntegrateAsync(snapshot, ResourceSnapshotChangeType.Upsert) + .ConfigureAwait(false); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + logger.LogError(ex, "Error updating resource snapshot for {Name}", @event.Resource.Name); + } + } + }, + cancellationToken); } public async ValueTask DisposeAsync() @@ -49,7 +100,7 @@ internal ResourceSnapshotSubscription SubscribeResources() internal IAsyncEnumerable>? SubscribeConsoleLogs(string resourceName) { - var sequence = _consoleLogPublisher.Subscribe(resourceName); + var sequence = _resourceLoggerService.WatchAsync(resourceName); return sequence is null ? null : Enumerate(); diff --git a/src/Aspire.Hosting/Dashboard/DcpDataSource.cs b/src/Aspire.Hosting/Dashboard/DcpDataSource.cs deleted file mode 100644 index 0ea962130ae..00000000000 --- a/src/Aspire.Hosting/Dashboard/DcpDataSource.cs +++ /dev/null @@ -1,567 +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.Collections.Concurrent; -using System.Collections.Immutable; -using System.Text.Json; -using Aspire.Dashboard.Model; -using Aspire.Hosting.ApplicationModel; -using Aspire.Hosting.Dcp; -using Aspire.Hosting.Dcp.Model; -using k8s; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; - -namespace Aspire.Hosting.Dashboard; - -/// -/// Pulls data about resources from DCP's kubernetes API. Streams updates to consumers. -/// -/// -/// DCP data is obtained from . -/// is also used for mapping some project data. -/// -internal sealed class DcpDataSource -{ - private readonly IKubernetesService _kubernetesService; - private readonly ResourceNotificationService _notificationService; - private readonly IReadOnlyDictionary _applicationModel; - private readonly ConcurrentDictionary _placeHolderResources = []; - private readonly Func _onResourceChanged; - private readonly ILogger _logger; - - private readonly ConcurrentDictionary _containersMap = []; - private readonly ConcurrentDictionary _executablesMap = []; - private readonly ConcurrentDictionary _servicesMap = []; - private readonly ConcurrentDictionary _endpointsMap = []; - private readonly ConcurrentDictionary<(string, string), List> _resourceAssociatedServicesMap = []; - - public DcpDataSource( - IKubernetesService kubernetesService, - ResourceNotificationService notificationService, - IReadOnlyDictionary applicationModelMap, - IConfiguration configuration, - ILoggerFactory loggerFactory, - Func onResourceChanged, - CancellationToken cancellationToken) - { - _kubernetesService = kubernetesService; - _notificationService = notificationService; - _applicationModel = applicationModelMap; - _onResourceChanged = onResourceChanged; - - _logger = loggerFactory.CreateLogger(); - - var semaphore = new SemaphoreSlim(1); - - Task.Run( - async () => - { - // Show all resources initially and allow updates from DCP (for the relevant resources) - foreach (var (_, resource) in _applicationModel) - { - if (resource.Name == KnownResourceNames.AspireDashboard && - configuration.GetBool("DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES") is not true) - { - continue; - } - - await ProcessInitialResourceAsync(resource, cancellationToken).ConfigureAwait(false); - } - - using (semaphore) - { - await Task.WhenAll( - Task.Run(() => WatchKubernetesResource((t, r) => ProcessResourceChange(t, r, _executablesMap, "Executable", ToSnapshot)), cancellationToken), - Task.Run(() => WatchKubernetesResource((t, r) => ProcessResourceChange(t, r, _containersMap, "Container", ToSnapshot)), cancellationToken), - Task.Run(() => WatchKubernetesResource(ProcessServiceChange), cancellationToken), - Task.Run(() => WatchKubernetesResource(ProcessEndpointChange), cancellationToken)).ConfigureAwait(false); - } - }, - cancellationToken); - - async Task WatchKubernetesResource(Func handler) where T : CustomResource - { - try - { - await foreach (var (eventType, resource) in _kubernetesService.WatchAsync(cancellationToken: cancellationToken)) - { - await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - - try - { - if (IsFilteredResource(resource)) - { - continue; - } - - await handler(eventType, resource).ConfigureAwait(false); - } - finally - { - semaphore.Release(); - } - } - } - catch (Exception ex) when (ex is not OperationCanceledException) - { - _logger.LogError(ex, "Watch task over kubernetes {ResourceType} resources terminated", typeof(T).Name); - } - } - - bool IsFilteredResource(T resource) where T : CustomResource - { - // We filter out any resources that start with aspire-dashboard (there are services as well as executables). - if (resource.Metadata.Name.StartsWith(KnownResourceNames.AspireDashboard, StringComparisons.ResourceName)) - { - return configuration.GetBool("DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES") is not true; - } - - return false; - } - } - - private Task ProcessInitialResourceAsync(IResource resource, CancellationToken cancellationToken) - { - // The initial snapshots are all generic resources until we get the real state from DCP (for projects, containers and executables). - if (resource.IsContainer()) - { - var snapshot = CreateResourceSnapshot(resource, DateTime.UtcNow, new CustomResourceSnapshot - { - ResourceType = KnownResourceTypes.Container, - Properties = [], - }); - - _placeHolderResources.TryAdd(resource.Name, snapshot); - } - else if (resource is ProjectResource) - { - var snapshot = CreateResourceSnapshot(resource, DateTime.UtcNow, new CustomResourceSnapshot - { - ResourceType = KnownResourceTypes.Project, - Properties = [], - }); - - _placeHolderResources.TryAdd(resource.Name, snapshot); - } - else if (resource is ExecutableResource) - { - var snapshot = CreateResourceSnapshot(resource, DateTime.UtcNow, new CustomResourceSnapshot - { - ResourceType = KnownResourceTypes.Executable, - Properties = [], - }); - - _placeHolderResources.TryAdd(resource.Name, snapshot); - } - - var creationTimestamp = DateTime.UtcNow; - - _ = Task.Run(async () => - { - await foreach (var state in _notificationService.WatchAsync(resource).WithCancellation(cancellationToken)) - { - try - { - var snapshot = CreateResourceSnapshot(resource, creationTimestamp, state); - - await _onResourceChanged(snapshot, ResourceSnapshotChangeType.Upsert).ConfigureAwait(false); - } - catch (Exception ex) when (ex is not OperationCanceledException) - { - _logger.LogError(ex, "Error updating resource snapshot for {Name}", resource.Name); - } - } - - }, cancellationToken); - - return Task.CompletedTask; - } - - private static GenericResourceSnapshot CreateResourceSnapshot(IResource resource, DateTime creationTimestamp, CustomResourceSnapshot snapshot) - { - ImmutableArray environmentVariables = [.. - snapshot.EnvironmentVariables.Select(e => new EnvironmentVariableSnapshot(e.Name, e.Value, false))]; - - ImmutableArray services = [.. - snapshot.Urls.Select(u => new ResourceServiceSnapshot(u.Name, u.Url, null))]; - - ImmutableArray endpoints = [.. - snapshot.Urls.Select(u => new EndpointSnapshot(u.Url, u.Url))]; - - return new GenericResourceSnapshot(snapshot) - { - Uid = resource.Name, - CreationTimeStamp = snapshot.CreationTimeStamp ?? creationTimestamp, - Name = resource.Name, - DisplayName = resource.Name, - Endpoints = endpoints, - Environment = environmentVariables, - ExitCode = null, - ExpectedEndpointsCount = endpoints.Length, - Services = services, - State = snapshot.State ?? "Running" - }; - } - - private async Task ProcessResourceChange(WatchEventType watchEventType, T resource, ConcurrentDictionary resourceByName, string resourceKind, Func snapshotFactory) where T : CustomResource - { - if (ProcessResourceChange(resourceByName, watchEventType, resource)) - { - UpdateAssociatedServicesMap(); - - var changeType = watchEventType switch - { - WatchEventType.Added or WatchEventType.Modified => ResourceSnapshotChangeType.Upsert, - WatchEventType.Deleted => ResourceSnapshotChangeType.Delete, - _ => throw new System.ComponentModel.InvalidEnumArgumentException($"Cannot convert {nameof(WatchEventType)} with value {watchEventType} into enum of type {nameof(ResourceSnapshotChangeType)}.") - }; - - // Remove the placeholder resource if it exists since we're getting an update about the real resource - // from DCP. - string? resourceName = null; - resource.Metadata.Annotations?.TryGetValue(Executable.ResourceNameAnnotation, out resourceName); - - if (resourceName is not null && _placeHolderResources.TryRemove(resourceName, out var placeHolder)) - { - await _onResourceChanged(placeHolder, ResourceSnapshotChangeType.Delete).ConfigureAwait(false); - } - - var snapshot = snapshotFactory(resource); - - await _onResourceChanged(snapshot, changeType).ConfigureAwait(false); - } - - void UpdateAssociatedServicesMap() - { - // We keep track of associated services for the resource - // So whenever we get the service we can figure out if the service can generate endpoint for the resource - if (watchEventType == WatchEventType.Deleted) - { - _resourceAssociatedServicesMap.Remove((resourceKind, resource.Metadata.Name), out _); - } - else if (resource.Metadata.Annotations?.TryGetValue(CustomResource.ServiceProducerAnnotation, out var servicesProducedAnnotationJson) == true) - { - var serviceProducerAnnotations = JsonSerializer.Deserialize(servicesProducedAnnotationJson); - if (serviceProducerAnnotations is not null) - { - _resourceAssociatedServicesMap[(resourceKind, resource.Metadata.Name)] - = serviceProducerAnnotations.Select(e => e.ServiceName).ToList(); - } - } - } - } - - private async Task ProcessEndpointChange(WatchEventType watchEventType, Endpoint endpoint) - { - if (!ProcessResourceChange(_endpointsMap, watchEventType, endpoint)) - { - return; - } - - if (endpoint.Metadata.OwnerReferences is null) - { - return; - } - - foreach (var ownerReference in endpoint.Metadata.OwnerReferences) - { - await TryRefreshResource(ownerReference.Kind, ownerReference.Name).ConfigureAwait(false); - } - } - - private async Task ProcessServiceChange(WatchEventType watchEventType, Service service) - { - if (!ProcessResourceChange(_servicesMap, watchEventType, service)) - { - return; - } - - foreach (var ((resourceKind, resourceName), _) in _resourceAssociatedServicesMap.Where(e => e.Value.Contains(service.Metadata.Name))) - { - await TryRefreshResource(resourceKind, resourceName).ConfigureAwait(false); - } - } - - private async ValueTask TryRefreshResource(string resourceKind, string resourceName) - { - ResourceSnapshot? snapshot = resourceKind switch - { - "Container" => _containersMap.TryGetValue(resourceName, out var container) ? ToSnapshot(container) : null, - "Executable" => _executablesMap.TryGetValue(resourceName, out var executable) ? ToSnapshot(executable) : null, - _ => null - }; - - if (snapshot is not null) - { - await _onResourceChanged(snapshot, ResourceSnapshotChangeType.Upsert).ConfigureAwait(false); - } - } - - private ContainerSnapshot ToSnapshot(Container container) - { - var containerId = container.Status?.ContainerId; - var (endpoints, services) = GetEndpointsAndServices(container, "Container"); - - var environment = GetEnvironmentVariables(container.Status?.EffectiveEnv ?? container.Spec.Env, container.Spec.Env); - - return new ContainerSnapshot - { - Name = container.Metadata.Name, - DisplayName = container.Metadata.Name, - Uid = container.Metadata.Uid, - ContainerId = containerId, - CreationTimeStamp = container.Metadata.CreationTimestamp?.ToLocalTime(), - Image = container.Spec.Image!, - State = container.Status?.State, - // Map a container exit code of -1 (unknown) to null - ExitCode = container.Status?.ExitCode is null or Conventions.UnknownExitCode ? null : container.Status.ExitCode, - ExpectedEndpointsCount = GetExpectedEndpointsCount(container), - Environment = environment, - Endpoints = endpoints, - Services = services, - Command = container.Spec.Command, - Args = container.Status?.EffectiveArgs?.ToImmutableArray() ?? [], - Ports = GetPorts() - }; - - ImmutableArray GetPorts() - { - if (container.Spec.Ports is null) - { - return []; - } - - var ports = ImmutableArray.CreateBuilder(); - foreach (var port in container.Spec.Ports) - { - if (port.ContainerPort != null) - { - ports.Add(port.ContainerPort.Value); - } - } - return ports.ToImmutable(); - } - } - - private ExecutableSnapshot ToSnapshot(Executable executable) - { - string? projectPath = null; - executable.Metadata.Annotations?.TryGetValue(Executable.CSharpProjectPathAnnotation, out projectPath); - - var (endpoints, services) = GetEndpointsAndServices(executable, "Executable", projectPath); - - if (projectPath is not null) - { - // This executable represents a C# project, so we create a slightly different type here - // that captures the project's path, making it more convenient for consumers to work with - // the project. - return new ProjectSnapshot - { - Name = executable.Metadata.Name, - DisplayName = GetDisplayName(executable), - Uid = executable.Metadata.Uid, - CreationTimeStamp = executable.Metadata.CreationTimestamp?.ToLocalTime(), - ExecutablePath = executable.Spec.ExecutablePath, - WorkingDirectory = executable.Spec.WorkingDirectory, - Arguments = executable.Status?.EffectiveArgs?.ToImmutableArray() ?? [], - ProjectPath = projectPath, - State = executable.Status?.State, - ExitCode = executable.Status?.ExitCode, - StdOutFile = executable.Status?.StdOutFile, - StdErrFile = executable.Status?.StdErrFile, - ProcessId = executable.Status?.ProcessId, - ExpectedEndpointsCount = GetExpectedEndpointsCount(executable), - Environment = GetEnvironmentVariables(executable.Status?.EffectiveEnv, executable.Spec.Env), - Endpoints = endpoints, - Services = services - }; - } - - return new ExecutableSnapshot - { - Name = executable.Metadata.Name, - DisplayName = GetDisplayName(executable), - Uid = executable.Metadata.Uid, - CreationTimeStamp = executable.Metadata.CreationTimestamp?.ToLocalTime(), - ExecutablePath = executable.Spec.ExecutablePath, - WorkingDirectory = executable.Spec.WorkingDirectory, - Arguments = executable.Status?.EffectiveArgs?.ToImmutableArray() ?? [], - State = executable.Status?.State, - ExitCode = executable.Status?.ExitCode, - StdOutFile = executable.Status?.StdOutFile, - StdErrFile = executable.Status?.StdErrFile, - ProcessId = executable.Status?.ProcessId, - ExpectedEndpointsCount = GetExpectedEndpointsCount(executable), - Environment = GetEnvironmentVariables(executable.Status?.EffectiveEnv, executable.Spec.Env), - Endpoints = endpoints, - Services = services - }; - - static string GetDisplayName(Executable executable) - { - var displayName = executable.Metadata.Name; - var replicaSetOwner = executable.Metadata.OwnerReferences?.FirstOrDefault( - or => or.Kind == Dcp.Model.Dcp.ExecutableReplicaSetKind - ); - if (replicaSetOwner is not null && displayName.Length > 3) - { - var lastHyphenIndex = displayName.LastIndexOf('-'); - if (lastHyphenIndex > 0 && lastHyphenIndex < displayName.Length - 1) - { - // Strip the replica ID from the name. - displayName = displayName[..lastHyphenIndex]; - } - } - return displayName; - } - } - - private (ImmutableArray Endpoints, ImmutableArray Services) GetEndpointsAndServices( - CustomResource resource, - string resourceKind, - string? projectPath = null) - { - var endpoints = ImmutableArray.CreateBuilder(); - var services = ImmutableArray.CreateBuilder(); - var name = resource.Metadata.Name; - string? resourceName = null; - resource.Metadata.Annotations?.TryGetValue(Executable.ResourceNameAnnotation, out resourceName); - - foreach (var endpoint in _endpointsMap.Values) - { - if (endpoint.Metadata.OwnerReferences?.Any(or => or.Kind == resource.Kind && or.Name == name) != true) - { - continue; - } - - if (endpoint.Spec.ServiceName is not null - && _servicesMap.TryGetValue(endpoint.Spec.ServiceName, out var service) - && service?.UsesHttpProtocol(out var uriScheme) == true) - { - var endpointString = $"{uriScheme}://{endpoint.Spec.Address}:{endpoint.Spec.Port}"; - var proxyUrlString = $"{uriScheme}://{service.AllocatedAddress}:{service.AllocatedPort}"; - - // For project look into launch profile to append launch url - if (projectPath is not null - && resourceName is not null - && _applicationModel.TryGetValue(resourceName, out var appModelResource) - && appModelResource is ProjectResource project - && project.GetEffectiveLaunchProfile() is LaunchProfile launchProfile - && launchProfile.LaunchUrl is string launchUrl) - { - if (!launchUrl.Contains("://")) - { - // This is relative URL - endpointString += $"/{launchUrl}"; - proxyUrlString += $"/{launchUrl}"; - } - else - { - // For absolute URL we need to update the port value if possible - if (launchProfile.ApplicationUrl is string applicationUrl - && launchUrl.StartsWith(applicationUrl)) - { - endpointString = launchUrl.Replace(applicationUrl, endpointString); - proxyUrlString = launchUrl; - } - } - - // If we cannot process launchUrl then we just show endpoint string - } - - endpoints.Add(new(endpointString, proxyUrlString)); - } - } - - if (_resourceAssociatedServicesMap.TryGetValue((resourceKind, name), out var resourceServiceMappings)) - { - foreach (var serviceName in resourceServiceMappings) - { - if (_servicesMap.TryGetValue(serviceName, out var service)) - { - services.Add(new ResourceServiceSnapshot(service.Metadata.Name, service.AllocatedAddress, service.AllocatedPort)); - } - } - } - - return (endpoints.ToImmutable(), services.ToImmutable()); - } - - private int? GetExpectedEndpointsCount(CustomResource resource) - { - var expectedCount = 0; - if (resource.Metadata.Annotations?.TryGetValue(CustomResource.ServiceProducerAnnotation, out var servicesProducedAnnotationJson) == true) - { - var serviceProducerAnnotations = JsonSerializer.Deserialize(servicesProducedAnnotationJson); - if (serviceProducerAnnotations is not null) - { - foreach (var serviceProducer in serviceProducerAnnotations) - { - if (!_servicesMap.TryGetValue(serviceProducer.ServiceName, out var service)) - { - // We don't have matching service so we cannot compute endpoint count completely - // So we return null indicating that it is unknown. - // Dashboard should show this as Starting - return null; - } - - if (service.UsesHttpProtocol(out _)) - { - expectedCount++; - } - } - } - } - - return expectedCount; - } - - private static ImmutableArray GetEnvironmentVariables(List? effectiveSource, List? specSource) - { - if (effectiveSource is null or { Count: 0 }) - { - return []; - } - - var environment = ImmutableArray.CreateBuilder(effectiveSource.Count); - - foreach (var env in effectiveSource) - { - if (env.Name is not null) - { - var isFromSpec = specSource?.Any(e => string.Equals(e.Name, env.Name, StringComparison.Ordinal)) is true or null; - - environment.Add(new(env.Name, env.Value, isFromSpec)); - } - } - - environment.Sort((v1, v2) => string.Compare(v1.Name, v2.Name, StringComparison.Ordinal)); - - return environment.ToImmutable(); - } - - private static bool ProcessResourceChange(ConcurrentDictionary map, WatchEventType watchEventType, T resource) - where T : CustomResource - { - switch (watchEventType) - { - case WatchEventType.Added: - map.TryAdd(resource.Metadata.Name, resource); - break; - - case WatchEventType.Modified: - map[resource.Metadata.Name] = resource; - break; - - case WatchEventType.Deleted: - map.Remove(resource.Metadata.Name, out _); - break; - - default: - return false; - } - - return true; - } -} diff --git a/src/Aspire.Hosting/Dashboard/ExecutableSnapshot.cs b/src/Aspire.Hosting/Dashboard/ExecutableSnapshot.cs deleted file mode 100644 index dd49c8dd057..00000000000 --- a/src/Aspire.Hosting/Dashboard/ExecutableSnapshot.cs +++ /dev/null @@ -1,33 +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.Collections.Immutable; -using System.Globalization; -using Aspire.Dashboard.Model; -using Google.Protobuf.WellKnownTypes; - -namespace Aspire.Hosting.Dashboard; - -/// -/// Immutable snapshot of an executable's state at a point in time. -/// -internal class ExecutableSnapshot : ResourceSnapshot -{ - public override string ResourceType => KnownResourceTypes.Executable; - - public required int? ProcessId { get; init; } - public required string? ExecutablePath { get; init; } - public required string? WorkingDirectory { get; init; } - public required ImmutableArray? Arguments { get; init; } - public required string? StdOutFile { get; init; } - public required string? StdErrFile { get; init; } - - protected override IEnumerable<(string Key, Value Value)> GetProperties() - { - yield return (KnownProperties.Executable.Path, ExecutablePath is null ? Value.ForNull() : Value.ForString(ExecutablePath)); - yield return (KnownProperties.Executable.WorkDir, WorkingDirectory is null ? Value.ForNull() : Value.ForString(WorkingDirectory)); - yield return (KnownProperties.Executable.Args, Arguments is null ? Value.ForNull() : Value.ForList(Arguments.Value.Select(arg => Value.ForString(arg)).ToArray())); - yield return (KnownProperties.Executable.Pid, ProcessId is null ? Value.ForNull() : Value.ForString(ProcessId.Value.ToString("D", CultureInfo.InvariantCulture))); - // TODO decide whether to send StdOut/StdErr file paths or not, and what we could use them for in the client. - } -} diff --git a/src/Aspire.Hosting/Dashboard/GenericResourceSnapshot.cs b/src/Aspire.Hosting/Dashboard/GenericResourceSnapshot.cs index a9d9380079d..748f7aa5f13 100644 --- a/src/Aspire.Hosting/Dashboard/GenericResourceSnapshot.cs +++ b/src/Aspire.Hosting/Dashboard/GenericResourceSnapshot.cs @@ -8,14 +8,23 @@ namespace Aspire.Hosting.Dashboard; internal class GenericResourceSnapshot(CustomResourceSnapshot state) : ResourceSnapshot { - // Default to the resource type name without the "Resource" suffix. public override string ResourceType => state.ResourceType; protected override IEnumerable<(string Key, Value Value)> GetProperties() { foreach (var (key, value) in state.Properties) { - yield return (key, Value.ForString(value)); + var result = value switch + { + string s => Value.ForString(s), + int i => Value.ForNumber(i), + IEnumerable list => Value.ForList(list.Select(Value.ForString).ToArray()), + IEnumerable list => Value.ForList(list.Select(i => Value.ForNumber(i)).ToArray()), + null => Value.ForNull(), + _ => Value.ForString(value.ToString()) + }; + + yield return (key, result); } } } diff --git a/src/Aspire.Hosting/Dashboard/ProjectSnapshot.cs b/src/Aspire.Hosting/Dashboard/ProjectSnapshot.cs deleted file mode 100644 index d0668e8c903..00000000000 --- a/src/Aspire.Hosting/Dashboard/ProjectSnapshot.cs +++ /dev/null @@ -1,27 +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 Aspire.Dashboard.Model; -using Google.Protobuf.WellKnownTypes; - -namespace Aspire.Hosting.Dashboard; - -/// -/// Immutable snapshot of a project's state at a point in time. -/// -internal class ProjectSnapshot : ExecutableSnapshot -{ - public override string ResourceType => KnownResourceTypes.Project; - - public required string ProjectPath { get; init; } - - protected override IEnumerable<(string Key, Value Value)> GetProperties() - { - yield return (KnownProperties.Project.Path, Value.ForString(ProjectPath)); - - foreach (var pair in base.GetProperties()) - { - yield return pair; - } - } -} diff --git a/src/Aspire.Hosting/Dashboard/ResourceSnapshot.cs b/src/Aspire.Hosting/Dashboard/ResourceSnapshot.cs index 8b6e66bad2e..58c63ba767f 100644 --- a/src/Aspire.Hosting/Dashboard/ResourceSnapshot.cs +++ b/src/Aspire.Hosting/Dashboard/ResourceSnapshot.cs @@ -33,7 +33,7 @@ internal abstract class ResourceSnapshot yield return (KnownProperties.Resource.Name, Value.ForString(Name)); yield return (KnownProperties.Resource.Type, Value.ForString(ResourceType)); yield return (KnownProperties.Resource.DisplayName, Value.ForString(DisplayName)); - yield return (KnownProperties.Resource.State, Value.ForString(State)); + yield return (KnownProperties.Resource.State, State is null ? Value.ForNull() : Value.ForString(State)); yield return (KnownProperties.Resource.ExitCode, ExitCode is null ? Value.ForNull() : Value.ForString(ExitCode.Value.ToString("D", CultureInfo.InvariantCulture))); yield return (KnownProperties.Resource.CreateTime, CreationTimeStamp is null ? Value.ForNull() : Value.ForString(CreationTimeStamp.Value.ToString("O"))); diff --git a/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs b/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs index b1c4169f959..f8a11524158 100644 --- a/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs +++ b/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs @@ -1,9 +1,13 @@ // 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.Concurrent; +using System.Collections.Immutable; using System.Net.Sockets; +using System.Text.Json; using Aspire.Dashboard.Model; using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Dashboard; using Aspire.Hosting.Dcp.Model; using Aspire.Hosting.Lifecycle; using Aspire.Hosting.Utils; @@ -67,12 +71,21 @@ internal sealed class ApplicationExecutor(ILogger logger, private readonly ILogger _logger = logger; private readonly DistributedApplicationModel _model = model; + private readonly Dictionary _applicationModel = model.Resources.ToDictionary(r => r.Name); private readonly IDistributedApplicationLifecycleHook[] _lifecycleHooks = lifecycleHooks.ToArray(); private readonly IOptions _options = options; private readonly IDashboardEndpointProvider _dashboardEndpointProvider = dashboardEndpointProvider; private readonly DistributedApplicationExecutionContext _executionContext = executionContext; private readonly List _appResources = []; + private readonly ConcurrentDictionary _containersMap = []; + private readonly ConcurrentDictionary _executablesMap = []; + private readonly ConcurrentDictionary _servicesMap = []; + private readonly ConcurrentDictionary _endpointsMap = []; + private readonly ConcurrentDictionary<(string, string), List> _resourceAssociatedServicesMap = []; + private readonly ConcurrentDictionary _logStreams = new(); + private readonly ConcurrentDictionary _hiddenResources = new(); + public async Task RunApplicationAsync(CancellationToken cancellationToken = default) { AspireEventSource.Instance.DcpModelCreationStart(); @@ -83,19 +96,19 @@ public async Task RunApplicationAsync(CancellationToken cancellationToken = defa if (_model.Resources.SingleOrDefault(r => StringComparers.ResourceName.Equals(r.Name, KnownResourceNames.AspireDashboard)) is not { } dashboardResource) { // No dashboard is specified, so start one. - // TODO validate that the dashboard has not been suppressed await StartDashboardAsDcpExecutableAsync(cancellationToken).ConfigureAwait(false); } else { - await ConfigureAspireDashboardResource(dashboardResource, cancellationToken).ConfigureAwait(false); + ConfigureAspireDashboardResource(dashboardResource); } } - PrepareServices(); PrepareContainers(); PrepareExecutables(); + await PublishResourcesWithInitialStateAsync().ConfigureAwait(false); + await CreateServicesAsync(cancellationToken).ConfigureAwait(false); await CreateContainersAndExecutablesAsync(cancellationToken).ConfigureAwait(false); @@ -104,6 +117,9 @@ public async Task RunApplicationAsync(CancellationToken cancellationToken = defa { await lifecycleHook.AfterResourcesCreatedAsync(_model, cancellationToken).ConfigureAwait(false); } + + // Watch for changes to the resource state. + WatchResourceChanges(cancellationToken); } finally { @@ -111,7 +127,500 @@ public async Task RunApplicationAsync(CancellationToken cancellationToken = defa } } - private async Task ConfigureAspireDashboardResource(IResource dashboardResource, CancellationToken cancellationToken) + private async Task PublishResourcesWithInitialStateAsync() + { + // Publish the initial state of the resources that have a snapshot annotation. + foreach (var resource in _model.Resources) + { + await notificationService.PublishUpdateAsync(resource, s => s).ConfigureAwait(false); + } + } + + private void WatchResourceChanges(CancellationToken cancellationToken) + { + var semaphore = new SemaphoreSlim(1); + + Task.Run( + async () => + { + using (semaphore) + { + await Task.WhenAll( + Task.Run(() => WatchKubernetesResource((t, r) => ProcessResourceChange(t, r, _executablesMap, "Executable", ToSnapshot)), cancellationToken), + Task.Run(() => WatchKubernetesResource((t, r) => ProcessResourceChange(t, r, _containersMap, "Container", ToSnapshot)), cancellationToken), + Task.Run(() => WatchKubernetesResource(ProcessServiceChange), cancellationToken), + Task.Run(() => WatchKubernetesResource(ProcessEndpointChange), cancellationToken)).ConfigureAwait(false); + } + }, + cancellationToken); + + async Task WatchKubernetesResource(Func handler) where T : CustomResource + { + try + { + await foreach (var (eventType, resource) in kubernetesService.WatchAsync(cancellationToken: cancellationToken)) + { + await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + await handler(eventType, resource).ConfigureAwait(false); + } + finally + { + semaphore.Release(); + } + } + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + _logger.LogError(ex, "Watch task over kubernetes {ResourceType} resources terminated", typeof(T).Name); + } + } + } + + private async Task ProcessResourceChange(WatchEventType watchEventType, T resource, ConcurrentDictionary resourceByName, string resourceKind, Func snapshotFactory) where T : CustomResource + { + if (ProcessResourceChange(resourceByName, watchEventType, resource)) + { + UpdateAssociatedServicesMap(); + + var changeType = watchEventType switch + { + WatchEventType.Added or WatchEventType.Modified => ResourceSnapshotChangeType.Upsert, + WatchEventType.Deleted => ResourceSnapshotChangeType.Delete, + _ => throw new System.ComponentModel.InvalidEnumArgumentException($"Cannot convert {nameof(WatchEventType)} with value {watchEventType} into enum of type {nameof(ResourceSnapshotChangeType)}.") + }; + + // Find the associated application model resource and update it. + string? resourceName = null; + resource.Metadata.Annotations?.TryGetValue(Executable.ResourceNameAnnotation, out resourceName); + + if (resourceName is not null && + _applicationModel.TryGetValue(resourceName, out var appModelResource)) + { + if (changeType == ResourceSnapshotChangeType.Delete) + { + // Stop the log stream for the resource + if (_logStreams.TryRemove(resource.Metadata.Name, out var cts)) + { + cts.Cancel(); + } + + // TODO: Handle resource deletion + if (_logger.IsEnabled(LogLevel.Trace)) + { + _logger.LogTrace("Deleting application model resource {ResourceName} with {ResourceKind} resource {ResourceName}", appModelResource.Name, resourceKind, resource.Metadata.Name); + } + } + else + { + if (_logger.IsEnabled(LogLevel.Trace)) + { + _logger.LogTrace("Updating application model resource {ResourceName} with {ResourceKind} resource {ResourceName}", appModelResource.Name, resourceKind, resource.Metadata.Name); + } + + if (_hiddenResources.TryAdd(appModelResource, true)) + { + // Hide the application model resource because we have the DCP resource + await notificationService.PublishUpdateAsync(appModelResource, s => s with { State = "Hidden" }).ConfigureAwait(false); + } + + // Notifications are associated with the application model resource, so we need to update with that context + await notificationService.PublishUpdateAsync(appModelResource, resource.Metadata.Name, s => snapshotFactory(resource, s)).ConfigureAwait(false); + + StartLogStream(resource); + } + } + else + { + // No application model resource found for the DCP resource. This should only happen for the dashboard. + if (_logger.IsEnabled(LogLevel.Trace)) + { + _logger.LogTrace("No application model resource found for {ResourceKind} resource {ResourceName}", resourceKind, resource.Metadata.Name); + } + } + } + + void UpdateAssociatedServicesMap() + { + // We keep track of associated services for the resource + // So whenever we get the service we can figure out if the service can generate endpoint for the resource + if (watchEventType == WatchEventType.Deleted) + { + _resourceAssociatedServicesMap.Remove((resourceKind, resource.Metadata.Name), out _); + } + else if (resource.Metadata.Annotations?.TryGetValue(CustomResource.ServiceProducerAnnotation, out var servicesProducedAnnotationJson) == true) + { + var serviceProducerAnnotations = JsonSerializer.Deserialize(servicesProducedAnnotationJson); + if (serviceProducerAnnotations is not null) + { + _resourceAssociatedServicesMap[(resourceKind, resource.Metadata.Name)] + = serviceProducerAnnotations.Select(e => e.ServiceName).ToList(); + } + } + } + } + + private void StartLogStream(T resource) where T : CustomResource + { + IAsyncEnumerable>? enumerable = resource switch + { + Container c when c.Status?.ContainerId is not null => new DockerContainerLogSource(c.Status.ContainerId), + Executable e when e.Status?.StdOutFile is not null && e.Status?.StdErrFile is not null => new FileLogSource(e.Status.StdOutFile, e.Status.StdErrFile), + // Container or Executable => new ResourceLogSource(_logger, kubernetesService, resource), + _ => null + }; + + // No way to get logs for this resource as yet + if (enumerable is null) + { + return; + } + + // This does not run concurrently for the same resource so we can safely use GetOrAdd without + // creating multiple log streams. + _logStreams.GetOrAdd(resource.Metadata.Name, (_) => + { + var cts = new CancellationTokenSource(); + + var task = Task.Run(async () => + { + try + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Starting log streaming for {ResourceName}", resource.Metadata.Name); + } + + // Pump the logs from the enumerable into the logger + var logger = loggerService.GetLogger(resource.Metadata.Name); + + await foreach (var batch in enumerable.WithCancellation(cts.Token)) + { + foreach (var (content, isError) in batch) + { + var level = isError ? LogLevel.Error : LogLevel.Information; + logger.Log(level, 0, content, null, (s, _) => s); + } + } + } + catch (OperationCanceledException) + { + // Ignore + _logger.LogDebug("Log streaming for {ResourceName} was cancelled", resource.Metadata.Name); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error streaming logs for {ResourceName}", resource.Metadata.Name); + } + finally + { + // Complete the log stream + loggerService.Complete(resource.Metadata.Name); + } + }, + cts.Token); + + return cts; + }); + } + + private async Task ProcessEndpointChange(WatchEventType watchEventType, Endpoint endpoint) + { + if (!ProcessResourceChange(_endpointsMap, watchEventType, endpoint)) + { + return; + } + + if (endpoint.Metadata.OwnerReferences is null) + { + return; + } + + foreach (var ownerReference in endpoint.Metadata.OwnerReferences) + { + await TryRefreshResource(ownerReference.Kind, ownerReference.Name).ConfigureAwait(false); + } + } + + private async Task ProcessServiceChange(WatchEventType watchEventType, Service service) + { + if (!ProcessResourceChange(_servicesMap, watchEventType, service)) + { + return; + } + + foreach (var ((resourceKind, resourceName), _) in _resourceAssociatedServicesMap.Where(e => e.Value.Contains(service.Metadata.Name))) + { + await TryRefreshResource(resourceKind, resourceName).ConfigureAwait(false); + } + } + + private async ValueTask TryRefreshResource(string resourceKind, string resourceName) + { + CustomResource? cr = resourceKind switch + { + "Container" => _containersMap.TryGetValue(resourceName, out var container) ? container : null, + "Executable" => _executablesMap.TryGetValue(resourceName, out var executable) ? executable : null, + _ => null + }; + + if (cr is not null) + { + string? appModelResourceName = null; + cr.Metadata.Annotations?.TryGetValue(Executable.ResourceNameAnnotation, out appModelResourceName); + + if (appModelResourceName is not null && + _applicationModel.TryGetValue(appModelResourceName, out var appModelResource)) + { + await notificationService.PublishUpdateAsync(appModelResource, cr.Metadata.Name, s => + { + if (cr is Container container) + { + return ToSnapshot(container, s); + } + else if (cr is Executable exe) + { + return ToSnapshot(exe, s); + } + return s; + }) + .ConfigureAwait(false); + } + } + } + + private CustomResourceSnapshot ToSnapshot(Container container, CustomResourceSnapshot previous) + { + var containerId = container.Status?.ContainerId; + var (endpointsWithMetadata, services) = GetEndpointsAndServices(container, "Container"); + + var environment = GetEnvironmentVariables(container.Status?.EffectiveEnv ?? container.Spec.Env, container.Spec.Env); + + return previous with + { + ResourceType = KnownResourceTypes.Container, + State = container.Status?.State, + // Map a container exit code of -1 (unknown) to null + ExitCode = container.Status?.ExitCode is null or Conventions.UnknownExitCode ? null : container.Status.ExitCode, + Properties = [ + (KnownProperties.Container.Image, container.Spec.Image), + (KnownProperties.Container.Id, containerId), + (KnownProperties.Container.Command, container.Spec.Command), + (KnownProperties.Container.Args, container.Status?.EffectiveArgs ?? []), + (KnownProperties.Container.Ports, GetPorts()), + ], + EnvironmentVariables = environment, + CreationTimeStamp = container.Metadata.CreationTimestamp?.ToLocalTime(), + Endpoints = [.. endpointsWithMetadata.Select(e => (e.EndpointUrl, e.ProxyUrl))], + Services = services + }; + + ImmutableArray GetPorts() + { + if (container.Spec.Ports is null) + { + return []; + } + + var ports = ImmutableArray.CreateBuilder(); + foreach (var port in container.Spec.Ports) + { + if (port.ContainerPort != null) + { + ports.Add(port.ContainerPort.Value); + } + } + return ports.ToImmutable(); + } + } + + private CustomResourceSnapshot ToSnapshot(Executable executable, CustomResourceSnapshot previous) + { + string? projectPath = null; + executable.Metadata.Annotations?.TryGetValue(Executable.CSharpProjectPathAnnotation, out projectPath); + + string? resourceName = null; + executable.Metadata.Annotations?.TryGetValue(Executable.ResourceNameAnnotation, out resourceName); + + var (endpointsWithMetadata, services) = GetEndpointsAndServices(executable, "Executable"); + + var environment = GetEnvironmentVariables(executable.Status?.EffectiveEnv, executable.Spec.Env); + + if (projectPath is not null) + { + var endpoints = endpointsWithMetadata.Select(e => (e.EndpointUrl, e.ProxyUrl)); + + if (resourceName is not null && + _applicationModel.TryGetValue(resourceName, out var appModelResource) && + appModelResource is ProjectResource p && + p.GetEffectiveLaunchProfile() is LaunchProfile profile && + profile.LaunchUrl is string launchUrl) + { + // Concat the launch url from the launch profile to the urls with IsFromLaunchProfile set to true + + string CombineUrls(string url, string launchUrl) + { + if (!launchUrl.Contains("://")) + { + // This is relative URL + url += $"/{launchUrl}"; + } + else + { + // For absolute URL we need to update the port value if possible + if (profile.ApplicationUrl is string applicationUrl + && launchUrl.StartsWith(applicationUrl)) + { + url = launchUrl.Replace(applicationUrl, url); + } + } + + return url; + } + + endpoints = endpointsWithMetadata.Select(e => e.IsFromLaunchProfile + ? (CombineUrls(e.EndpointUrl, launchUrl), CombineUrls(e.ProxyUrl, launchUrl)) + : (e.EndpointUrl, e.ProxyUrl) + ); + } + + return previous with + { + ResourceType = KnownResourceTypes.Project, + State = executable.Status?.State, + ExitCode = executable.Status?.ExitCode, + Properties = [ + (KnownProperties.Executable.Path, executable.Spec.ExecutablePath), + (KnownProperties.Executable.WorkDir, executable.Spec.WorkingDirectory), + (KnownProperties.Executable.Args, executable.Status?.EffectiveArgs ?? []), + (KnownProperties.Executable.Pid, executable.Status?.ProcessId), + (KnownProperties.Project.Path, projectPath) + ], + EnvironmentVariables = environment, + CreationTimeStamp = executable.Metadata.CreationTimestamp?.ToLocalTime(), + Endpoints = [.. endpoints], + Services = services + }; + } + + return previous with + { + ResourceType = KnownResourceTypes.Executable, + State = executable.Status?.State, + ExitCode = executable.Status?.ExitCode, + Properties = [ + (KnownProperties.Executable.Path, executable.Spec.ExecutablePath), + (KnownProperties.Executable.WorkDir, executable.Spec.WorkingDirectory), + (KnownProperties.Executable.Args, executable.Status?.EffectiveArgs ?? []), + (KnownProperties.Executable.Pid, executable.Status?.ProcessId) + ], + EnvironmentVariables = environment, + CreationTimeStamp = executable.Metadata.CreationTimestamp?.ToLocalTime(), + Endpoints = [.. endpointsWithMetadata.Select(e => (e.EndpointUrl, e.ProxyUrl))], + Services = services + }; + } + + private (ImmutableArray<(string EndpointUrl, string ProxyUrl, bool IsFromLaunchProfile)> Endpoints, + ImmutableArray<(string Name, string? AllocatedAddress, int? AllocatedPort)> Services) + GetEndpointsAndServices(CustomResource resource, string resourceKind) + { + var endpoints = ImmutableArray.CreateBuilder<(string EndpointUrl, string ProxyUrl, bool IsFromLaunchProfile)>(); + var services = ImmutableArray.CreateBuilder<(string Name, string? AllocatedAddress, int? AllocatedPort)>(); + var name = resource.Metadata.Name; + + foreach (var (_, endpoint) in _endpointsMap) + { + if (endpoint.Metadata.OwnerReferences?.Any(or => or.Kind == resource.Kind && or.Name == name) != true) + { + continue; + } + + if (endpoint.Spec.ServiceName is not null + && _servicesMap.TryGetValue(endpoint.Spec.ServiceName, out var service) + && service?.UsesHttpProtocol(out var uriScheme) == true) + { + string? launchProfile = null; + service.Metadata.Annotations?.TryGetValue(CustomResource.LaunchProfileAnnotation, out launchProfile); + + var endpointString = $"{uriScheme}://{endpoint.Spec.Address}:{endpoint.Spec.Port}"; + var proxyUrlString = $"{uriScheme}://{service.AllocatedAddress}:{service.AllocatedPort}"; + var isFromLaunchProfile = false; + + if (launchProfile is not null) + { + _ = bool.TryParse(launchProfile, out isFromLaunchProfile); + } + + endpoints.Add(new(endpointString, proxyUrlString, isFromLaunchProfile)); + } + } + + if (_resourceAssociatedServicesMap.TryGetValue((resourceKind, name), out var resourceServiceMappings)) + { + foreach (var serviceName in resourceServiceMappings) + { + if (_servicesMap.TryGetValue(serviceName, out var service)) + { + services.Add(new(service.Metadata.Name, service.AllocatedAddress, service.AllocatedPort)); + } + } + } + + return (endpoints.ToImmutable(), services.ToImmutable()); + } + + private static ImmutableArray<(string Name, string Value, bool IsFromSpec)> GetEnvironmentVariables(List? effectiveSource, List? specSource) + { + if (effectiveSource is null or { Count: 0 }) + { + return []; + } + + var environment = ImmutableArray.CreateBuilder<(string Name, string Value, bool IsFromSpec)>(effectiveSource.Count); + + foreach (var env in effectiveSource) + { + if (env.Name is not null) + { + var isFromSpec = specSource?.Any(e => string.Equals(e.Name, env.Name, StringComparison.Ordinal)) is true or null; + + environment.Add(new(env.Name, env.Value ?? "", isFromSpec)); + } + } + + environment.Sort((v1, v2) => string.Compare(v1.Name, v2.Name, StringComparison.Ordinal)); + + return environment.ToImmutable(); + } + + private static bool ProcessResourceChange(ConcurrentDictionary map, WatchEventType watchEventType, T resource) + where T : CustomResource + { + switch (watchEventType) + { + case WatchEventType.Added: + map.TryAdd(resource.Metadata.Name, resource); + break; + + case WatchEventType.Modified: + map[resource.Metadata.Name] = resource; + break; + + case WatchEventType.Deleted: + map.Remove(resource.Metadata.Name, out _); + break; + + default: + return false; + } + + return true; + } + + private void ConfigureAspireDashboardResource(IResource dashboardResource) { // Don't publish the resource to the manifest. dashboardResource.Annotations.Add(ManifestPublishingCallbackAnnotation.Ignore); @@ -124,10 +633,7 @@ private async Task ConfigureAspireDashboardResource(IResource dashboardResource, dashboardResource.Annotations.Remove(endpointAnnotation); } - // Get resource endpoint URL. - var grpcEndpointUrl = await _dashboardEndpointProvider.GetResourceServiceUriAsync(cancellationToken).ConfigureAwait(false); - - dashboardResource.Annotations.Add(new EnvironmentCallbackAnnotation(context => + dashboardResource.Annotations.Add(new EnvironmentCallbackAnnotation(async context => { if (configuration["ASPNETCORE_URLS"] is not { } appHostApplicationUrl) { @@ -141,6 +647,8 @@ private async Task ConfigureAspireDashboardResource(IResource dashboardResource, // Grab the resource service URL. We need to inject this into the resource. + var grpcEndpointUrl = await _dashboardEndpointProvider.GetResourceServiceUriAsync(context.CancellationToken).ConfigureAwait(false); + context.EnvironmentVariables["ASPNETCORE_URLS"] = appHostApplicationUrl; context.EnvironmentVariables["DOTNET_RESOURCE_SERVICE_ENDPOINT_URL"] = grpcEndpointUrl; context.EnvironmentVariables["DOTNET_DASHBOARD_OTLP_ENDPOINT_URL"] = otlpEndpointUrl; @@ -334,6 +842,7 @@ void addServiceAppResource(Service svc, IResource producingResource, EndpointAnn { svc.Spec.Protocol = PortProtocol.FromProtocolType(sba.Protocol); svc.Annotate(CustomResource.UriSchemeAnnotation, sba.UriScheme); + svc.Annotate(CustomResource.LaunchProfileAnnotation, sba.FromLaunchProfile.ToString()); svc.Spec.AddressAllocationMode = sba.IsProxied ? AddressAllocationModes.Localhost : AddressAllocationModes.Proxyless; _appResources.Add(new ServiceAppResource(producingResource, svc, sba)); } @@ -653,7 +1162,7 @@ private async Task CreateExecutableAsync(AppResource er, ILogger resourceLogger, } else { - logger.LogInformation("Waiting for value for environment variable value '{Name}' from {Name}.", key, valueProvider.ToString()); + logger.LogInformation("Waiting for value for environment variable value '{Name}' from {ValueProvider}.", key, valueProvider.ToString()); } } @@ -755,7 +1264,7 @@ private void PrepareContainers() if (container.TryGetContainerMounts(out var containerMounts)) { - ctr.Spec.VolumeMounts = new(); + ctr.Spec.VolumeMounts = []; foreach (var mount in containerMounts) { @@ -805,7 +1314,9 @@ async Task CreateContainerAsyncCore(AppResource cr, CancellationToken cancellati await notificationService.PublishUpdateAsync(cr.ModelResource, s => s with { State = "Starting", - Properties = [], // TODO: Add image name + Properties = [ + (KnownProperties.Container.Image, cr.ModelResource.TryGetContainerImageName(out var imageName) ? imageName : ""), + ], ResourceType = KnownResourceTypes.Container }) .ConfigureAwait(false); diff --git a/src/Aspire.Hosting/Dashboard/DockerContainerLogSource.cs b/src/Aspire.Hosting/Dcp/DockerContainerLogSource.cs similarity index 99% rename from src/Aspire.Hosting/Dashboard/DockerContainerLogSource.cs rename to src/Aspire.Hosting/Dcp/DockerContainerLogSource.cs index cc456e35de3..409c9f34fbc 100644 --- a/src/Aspire.Hosting/Dashboard/DockerContainerLogSource.cs +++ b/src/Aspire.Hosting/Dcp/DockerContainerLogSource.cs @@ -5,7 +5,7 @@ using Aspire.Hosting.Dcp.Process; using Aspire.Hosting.Utils; -namespace Aspire.Hosting.Dashboard; +namespace Aspire.Hosting.Dcp; internal sealed class DockerContainerLogSource(string containerId) : IAsyncEnumerable> { diff --git a/src/Aspire.Hosting/Dashboard/FileLogSource.cs b/src/Aspire.Hosting/Dcp/FileLogSource.cs similarity index 99% rename from src/Aspire.Hosting/Dashboard/FileLogSource.cs rename to src/Aspire.Hosting/Dcp/FileLogSource.cs index ea7c7158687..691636d7840 100644 --- a/src/Aspire.Hosting/Dashboard/FileLogSource.cs +++ b/src/Aspire.Hosting/Dcp/FileLogSource.cs @@ -7,7 +7,7 @@ using System.Text.RegularExpressions; using System.Threading.Channels; -namespace Aspire.Hosting.Dashboard; +namespace Aspire.Hosting.Dcp; internal sealed partial class FileLogSource(string stdOutPath, string stdErrPath) : IAsyncEnumerable> { diff --git a/src/Aspire.Hosting/Dcp/Model/ModelCommon.cs b/src/Aspire.Hosting/Dcp/Model/ModelCommon.cs index 432c65dd698..44972d1f2d1 100644 --- a/src/Aspire.Hosting/Dcp/Model/ModelCommon.cs +++ b/src/Aspire.Hosting/Dcp/Model/ModelCommon.cs @@ -20,6 +20,7 @@ internal abstract class CustomResource : KubernetesObject, IMetadata; internal sealed class ResourceLogSource( - ILoggerFactory loggerFactory, + ILogger logger, IKubernetesService kubernetesService, TResource resource) : IAsyncEnumerable @@ -35,8 +34,6 @@ public async IAsyncEnumerator GetAsyncEnumerator(CancellationToken SingleWriter = false }); - var logger = loggerFactory.CreateLogger>(); - var stdoutStreamTask = Task.Run(() => StreamLogsAsync(stdoutStream, isError: false), cancellationToken); var stderrStreamTask = Task.Run(() => StreamLogsAsync(stderrStream, isError: true), cancellationToken); @@ -56,7 +53,7 @@ async Task StreamLogsAsync(Stream stream, bool isError) { try { - using StreamReader sr = new StreamReader(stream, leaveOpen: false); + using var sr = new StreamReader(stream, leaveOpen: false); while (!cancellationToken.IsCancellationRequested) { var line = await sr.ReadLineAsync(cancellationToken).ConfigureAwait(false); diff --git a/src/Aspire.Hosting/Extensions/ProjectResourceBuilderExtensions.cs b/src/Aspire.Hosting/Extensions/ProjectResourceBuilderExtensions.cs index 092605299c7..d903070f7c2 100644 --- a/src/Aspire.Hosting/Extensions/ProjectResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/Extensions/ProjectResourceBuilderExtensions.cs @@ -137,6 +137,7 @@ private static IResourceBuilder WithProjectDefaults(this IResou { e.Port = uri.Port; e.UriScheme = uri.Scheme; + e.FromLaunchProfile = true; }, createIfNotExists: true); } diff --git a/tests/Aspire.Hosting.Tests/Dashboard/ResourcePublisherTests.cs b/tests/Aspire.Hosting.Tests/Dashboard/ResourcePublisherTests.cs index af1f470d9e9..67fdb2efb54 100644 --- a/tests/Aspire.Hosting.Tests/Dashboard/ResourcePublisherTests.cs +++ b/tests/Aspire.Hosting.Tests/Dashboard/ResourcePublisherTests.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.Dashboard.Model; using Aspire.Hosting.Dashboard; using Xunit; @@ -175,9 +176,13 @@ public async Task CancelledSubscriptionIsCleanedUp() await task; } - private static ContainerSnapshot CreateResourceSnapshot(string name) + private static GenericResourceSnapshot CreateResourceSnapshot(string name) { - return new ContainerSnapshot() + return new GenericResourceSnapshot(new() + { + Properties = [], + ResourceType = KnownResourceTypes.Container + }) { Name = name, Uid = "", @@ -189,11 +194,6 @@ private static ContainerSnapshot CreateResourceSnapshot(string name) Environment = [], ExpectedEndpointsCount = null, Services = [], - Args = [], - Command = "", - ContainerId = "", - Image = "", - Ports = [] }; } } diff --git a/tests/Aspire.Hosting.Tests/Dcp/ApplicationExecutorTests.cs b/tests/Aspire.Hosting.Tests/Dcp/ApplicationExecutorTests.cs index 836e6a42c51..611c03bc8f6 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/ApplicationExecutorTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/ApplicationExecutorTests.cs @@ -72,7 +72,7 @@ private static ApplicationExecutor CreateAppExecutor( }), new MockDashboardEndpointProvider(), new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run), - new ResourceNotificationService(), + new ResourceNotificationService(new NullLogger()), new ResourceLoggerService() ); } diff --git a/tests/Aspire.Hosting.Tests/ResourceNotificationTests.cs b/tests/Aspire.Hosting.Tests/ResourceNotificationTests.cs index d59dd3f657c..ccd14f921c1 100644 --- a/tests/Aspire.Hosting.Tests/ResourceNotificationTests.cs +++ b/tests/Aspire.Hosting.Tests/ResourceNotificationTests.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 Microsoft.Extensions.Logging.Abstractions; using Xunit; namespace Aspire.Hosting.Tests; @@ -41,109 +42,109 @@ public async Task ResourceUpdatesAreQueued() { var resource = new CustomResource("myResource"); - var notificationService = new ResourceNotificationService(); + var notificationService = new ResourceNotificationService(new NullLogger()); - async Task> GetValuesAsync() + async Task> GetValuesAsync(CancellationToken cancellationToken) { - var values = new List(); + var values = new List(); - await foreach (var item in notificationService.WatchAsync(resource)) + await foreach (var item in notificationService.WatchAsync().WithCancellation(cancellationToken)) { values.Add(item); + + if (values.Count == 2) + { + break; + } } return values; } - var enumerableTask = GetValuesAsync(); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var enumerableTask = GetValuesAsync(cts.Token); await notificationService.PublishUpdateAsync(resource, state => state with { Properties = state.Properties.Add(("A", "value")) }); await notificationService.PublishUpdateAsync(resource, state => state with { Properties = state.Properties.Add(("B", "value")) }); - notificationService.Complete(resource); - var values = await enumerableTask; - // Watch returns an initial snapshot - Assert.Empty(values[0].Properties); - Assert.Equal("value", values[1].Properties.Single(p => p.Key == "A").Value); - Assert.Equal("value", values[2].Properties.Single(p => p.Key == "B").Value); + Assert.Collection(values, + c => + { + Assert.Equal(resource, c.Resource); + Assert.Equal("myResource", c.ResourceId); + Assert.Equal("CustomResource", c.Snapshot.ResourceType); + Assert.Equal("value", c.Snapshot.Properties.Single(p => p.Key == "A").Value); + }, + c => + { + Assert.Equal(resource, c.Resource); + Assert.Equal("myResource", c.ResourceId); + Assert.Equal("CustomResource", c.Snapshot.ResourceType); + Assert.Equal("value", c.Snapshot.Properties.Single(p => p.Key == "B").Value); + }); } [Fact] - public async Task WatchReturnsAnInitialState() + public async Task WatchingAllResourcesNotifiesOfAnyResourceChange() { - var resource = new CustomResource("myResource"); + var resource1 = new CustomResource("myResource1"); + var resource2 = new CustomResource("myResource2"); - var notificationService = new ResourceNotificationService(); + var notificationService = new ResourceNotificationService(new NullLogger()); - async Task> GetValuesAsync() + async Task> GetValuesAsync(CancellationToken cancellation) { - var values = new List(); + var values = new List(); - await foreach (var item in notificationService.WatchAsync(resource)) + await foreach (var item in notificationService.WatchAsync().WithCancellation(cancellation)) { values.Add(item); + + if (values.Count == 3) + { + break; + } } return values; } - var enumerableTask = GetValuesAsync(); - - notificationService.Complete(resource); - - var values = await enumerableTask; - - // Watch returns an initial snapshot - var snapshot = Assert.Single(values); - - Assert.Equal("CustomResource", snapshot.ResourceType); - Assert.Empty(snapshot.EnvironmentVariables); - Assert.Empty(snapshot.Properties); - } - - [Fact] - public async Task WatchReturnsAnInitialStateIfCustomized() - { - var resource = new CustomResource("myResource"); - resource.Annotations.Add(new ResourceSnapshotAnnotation(new CustomResourceSnapshot - { - ResourceType = "CustomResource1", - Properties = [("A", "B")], - })); - - var notificationService = new ResourceNotificationService(); - - async Task> GetValuesAsync() - { - var values = new List(); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var enumerableTask = GetValuesAsync(cts.Token); - await foreach (var item in notificationService.WatchAsync(resource)) - { - values.Add(item); - } - - return values; - } + await notificationService.PublishUpdateAsync(resource1, state => state with { Properties = state.Properties.Add(("A", "value")) }); - var enumerableTask = GetValuesAsync(); + await notificationService.PublishUpdateAsync(resource2, state => state with { Properties = state.Properties.Add(("B", "value")) }); - notificationService.Complete(resource); + await notificationService.PublishUpdateAsync(resource1, "replica1", state => state with { Properties = state.Properties.Add(("C", "value")) }); var values = await enumerableTask; - // Watch returns an initial snapshot - var snapshot = Assert.Single(values); - - Assert.Equal("CustomResource1", snapshot.ResourceType); - Assert.Empty(snapshot.EnvironmentVariables); - Assert.Collection(snapshot.Properties, c => - { - Assert.Equal("A", c.Key); - Assert.Equal("B", c.Value); - }); + Assert.Collection(values, + c => + { + Assert.Equal(resource1, c.Resource); + Assert.Equal("myResource1", c.ResourceId); + Assert.Equal("CustomResource", c.Snapshot.ResourceType); + Assert.Equal("value", c.Snapshot.Properties.Single(p => p.Key == "A").Value); + }, + c => + { + Assert.Equal(resource2, c.Resource); + Assert.Equal("myResource2", c.ResourceId); + Assert.Equal("CustomResource", c.Snapshot.ResourceType); + Assert.Equal("value", c.Snapshot.Properties.Single(p => p.Key == "B").Value); + }, + c => + { + Assert.Equal(resource1, c.Resource); + Assert.Equal("replica1", c.ResourceId); + Assert.Equal("CustomResource", c.Snapshot.ResourceType); + Assert.Equal("value", c.Snapshot.Properties.Single(p => p.Key == "C").Value); + }); } private sealed class CustomResource(string name) : Resource(name), From 74b7bbf022af4cba81b79c533950afdd83b087b5 Mon Sep 17 00:00:00 2001 From: Adam Ratzman Date: Sun, 10 Mar 2024 17:53:37 -0600 Subject: [PATCH 29/50] Fix safari copy button issues (#2653) Co-authored-by: Adam Ratzman --- .../Components/Controls/GridValue.razor | 7 +++--- .../Components/Controls/GridValue.razor.cs | 4 --- .../Components/Controls/GridValue.razor.css | 5 ++-- .../LogMessageColumnDisplay.razor | 12 ++++----- .../LogMessageColumnDisplay.razor.cs | 16 +++++++----- .../Extensions/FluentUIExtensions.cs | 25 +++++++++++++++++++ src/Aspire.Dashboard/wwwroot/js/app.js | 8 ++++++ 7 files changed, 54 insertions(+), 23 deletions(-) create mode 100644 src/Aspire.Dashboard/Extensions/FluentUIExtensions.cs diff --git a/src/Aspire.Dashboard/Components/Controls/GridValue.razor b/src/Aspire.Dashboard/Components/Controls/GridValue.razor index 2f5a02dcfdf..c307318da43 100644 --- a/src/Aspire.Dashboard/Components/Controls/GridValue.razor +++ b/src/Aspire.Dashboard/Components/Controls/GridValue.razor @@ -1,5 +1,5 @@ -@using Aspire.Dashboard.Resources -@inject IJSRuntime JS +@using Aspire.Dashboard.Extensions +@using Aspire.Dashboard.Resources @inject IStringLocalizer Loc
@@ -35,8 +35,7 @@ + AdditionalAttributes="@FluentUIExtensions.GetClipboardCopyAdditionalAttributes(ValueToCopy ?? Value, PreCopyToolTip, PostCopyToolTip, ("aria-label", @Loc[nameof(ControlsStrings.GridValueCopyToClipboard)]))"> diff --git a/src/Aspire.Dashboard/Components/Controls/GridValue.razor.cs b/src/Aspire.Dashboard/Components/Controls/GridValue.razor.cs index 1dfa1d65bf0..5fd7d34bae0 100644 --- a/src/Aspire.Dashboard/Components/Controls/GridValue.razor.cs +++ b/src/Aspire.Dashboard/Components/Controls/GridValue.razor.cs @@ -4,7 +4,6 @@ using Aspire.Dashboard.Resources; using Microsoft.AspNetCore.Components; using Microsoft.FluentUI.AspNetCore.Components; -using Microsoft.JSInterop; namespace Aspire.Dashboard.Components.Controls; @@ -74,9 +73,6 @@ protected override void OnInitialized() private async Task ToggleMaskStateAsync() => await IsMaskedChanged.InvokeAsync(!IsMasked); - private async Task CopyTextToClipboardAsync(string? text, string id) - => await JS.InvokeVoidAsync("copyTextToClipboard", id, text, PreCopyToolTip, PostCopyToolTip); - private string TrimLength(string? text) { if (text is not null && MaxDisplayLength is int maxLength && text.Length > maxLength) diff --git a/src/Aspire.Dashboard/Components/Controls/GridValue.razor.css b/src/Aspire.Dashboard/Components/Controls/GridValue.razor.css index 34638c3f595..e91f7bfa5ff 100644 --- a/src/Aspire.Dashboard/Components/Controls/GridValue.razor.css +++ b/src/Aspire.Dashboard/Components/Controls/GridValue.razor.css @@ -15,9 +15,10 @@ } ::deep .defaultHidden { - visibility: hidden; + opacity: 0; + cursor: pointer; } ::deep:hover .defaultHidden { - visibility: visible; + opacity: 1; /* safari has a bug where hover is not always called on an invisible element, so we use opacity instead */ } diff --git a/src/Aspire.Dashboard/Components/ResourcesGridColumns/LogMessageColumnDisplay.razor b/src/Aspire.Dashboard/Components/ResourcesGridColumns/LogMessageColumnDisplay.razor index 2c7a3374eed..ee410123e19 100644 --- a/src/Aspire.Dashboard/Components/ResourcesGridColumns/LogMessageColumnDisplay.razor +++ b/src/Aspire.Dashboard/Components/ResourcesGridColumns/LogMessageColumnDisplay.razor @@ -1,14 +1,14 @@ @namespace Aspire.Dashboard.Components +@using Aspire.Dashboard.Extensions @using Aspire.Dashboard.Otlp.Model @using Aspire.Dashboard.Resources -@using Microsoft.Extensions.Logging @inject IJSRuntime JS @inject IStringLocalizer ControlsStringsLoc @inject IStringLocalizer Loc -@if (TryGetErrorInformation(out var errorInfo)) +@if (_hasErrorInfo) { var iconId = Guid.NewGuid().ToString(); @@ -24,19 +24,17 @@ Color="Color.Accent" Class="severity-icon"/> - var copyButtonId = Guid.NewGuid().ToString(); -

@Loc[nameof(Columns.LogMessageColumnExceptionDetailsTitle)]

-
@errorInfo
+
@_errorInfo
+ AdditionalAttributes="@FluentUIExtensions.GetClipboardCopyAdditionalAttributes(_errorInfo, ControlsStringsLoc[nameof(ControlsStrings.GridValueCopyToClipboard)].ToString(), ControlsStringsLoc[nameof(ControlsStrings.GridValueCopied)].ToString())"> @ControlsStringsLoc[nameof(ControlsStrings.GridValueCopyToClipboard)] diff --git a/src/Aspire.Dashboard/Components/ResourcesGridColumns/LogMessageColumnDisplay.razor.cs b/src/Aspire.Dashboard/Components/ResourcesGridColumns/LogMessageColumnDisplay.razor.cs index 8ca48b28e9e..9b9966b231a 100644 --- a/src/Aspire.Dashboard/Components/ResourcesGridColumns/LogMessageColumnDisplay.razor.cs +++ b/src/Aspire.Dashboard/Components/ResourcesGridColumns/LogMessageColumnDisplay.razor.cs @@ -3,13 +3,21 @@ using System.Diagnostics.CodeAnalysis; using Aspire.Dashboard.Otlp.Model; -using Aspire.Dashboard.Resources; -using Microsoft.JSInterop; namespace Aspire.Dashboard.Components; public partial class LogMessageColumnDisplay { + private readonly string _copyButtonId = Guid.NewGuid().ToString(); + + private bool _hasErrorInfo; + private string? _errorInfo; + + protected override void OnInitialized() + { + _hasErrorInfo = TryGetErrorInformation(out _errorInfo); + } + private bool TryGetErrorInformation([NotNullWhen(true)] out string? errorInfo) { // exception.stacktrace includes the exception message and type. @@ -39,8 +47,4 @@ private bool TryGetErrorInformation([NotNullWhen(true)] out string? errorInfo) return LogEntry.Attributes.GetValue(propertyName); } } - - private async Task CopyTextToClipboardAsync(string? text, string id) - => await JS.InvokeVoidAsync("copyTextToClipboard", id, text, ControlsStringsLoc[nameof(ControlsStrings.GridValueCopyToClipboard)].ToString(), ControlsStringsLoc[nameof(ControlsStrings.GridValueCopied)].ToString()); - } diff --git a/src/Aspire.Dashboard/Extensions/FluentUIExtensions.cs b/src/Aspire.Dashboard/Extensions/FluentUIExtensions.cs new file mode 100644 index 00000000000..6470701d7af --- /dev/null +++ b/src/Aspire.Dashboard/Extensions/FluentUIExtensions.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Dashboard.Extensions; + +internal static class FluentUIExtensions +{ + public static Dictionary GetClipboardCopyAdditionalAttributes(string? text, string? precopy, string? postcopy, params (string Attribute, object Value)[] additionalAttributes) + { + var attributes = new Dictionary + { + { "data-text", text ?? string.Empty }, + { "data-precopy", precopy ?? string.Empty }, + { "data-postcopy", postcopy ?? string.Empty }, + { "onclick", $"buttonCopyTextToClipboard(this)" } + }; + + foreach (var (attribute, value) in additionalAttributes) + { + attributes.Add(attribute, value); + } + + return attributes; + } +} diff --git a/src/Aspire.Dashboard/wwwroot/js/app.js b/src/Aspire.Dashboard/wwwroot/js/app.js index 1f85de1baa0..cfb34807686 100644 --- a/src/Aspire.Dashboard/wwwroot/js/app.js +++ b/src/Aspire.Dashboard/wwwroot/js/app.js @@ -52,6 +52,14 @@ function isScrolledToBottom(container) { return container.scrollHeight - container.clientHeight <= container.scrollTop + marginOfError; } +window.buttonCopyTextToClipboard = function(element) { + const text = element.getAttribute("data-text"); + const precopy = element.getAttribute("data-precopy"); + const postcopy = element.getAttribute("data-postcopy"); + + copyTextToClipboard(element.getAttribute("id"), text, precopy, postcopy); +} + window.copyTextToClipboard = function (id, text, precopy, postcopy) { const button = document.getElementById(id); From eb34c3bd86ef127f9af868c3aa3d54fb6e8a3c64 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 11:02:53 +1100 Subject: [PATCH 30/50] Update dependencies from https://github.com/dotnet/arcade build 20240301.4 (#2617) Microsoft.SourceBuild.Intermediate.arcade , Microsoft.DotNet.Arcade.Sdk , Microsoft.DotNet.Build.Tasks.Installers , Microsoft.DotNet.Build.Tasks.Workloads , Microsoft.DotNet.Helix.Sdk , Microsoft.DotNet.RemoteExecutor , Microsoft.DotNet.SharedFramework.Sdk , Microsoft.DotNet.XUnitExtensions From Version 8.0.0-beta.24123.1 -> To Version 8.0.0-beta.24151.4 Co-authored-by: dotnet-maestro[bot] --- eng/Version.Details.xml | 32 +- eng/Versions.props | 8 +- eng/common/templates-official/job/job.yml | 255 ++++++++++++++++ .../templates-official/job/onelocbuild.yml | 112 +++++++ .../job/publish-build-assets.yml | 153 ++++++++++ .../templates-official/job/source-build.yml | 67 ++++ .../job/source-index-stage1.yml | 68 +++++ .../templates-official/jobs/codeql-build.yml | 31 ++ eng/common/templates-official/jobs/jobs.yml | 97 ++++++ .../templates-official/jobs/source-build.yml | 46 +++ .../post-build/common-variables.yml | 22 ++ .../post-build/post-build.yml | 285 ++++++++++++++++++ .../post-build/setup-maestro-vars.yml | 70 +++++ .../post-build/trigger-subscription.yml | 13 + .../steps/add-build-to-channel.yml | 13 + .../templates-official/steps/build-reason.yml | 12 + .../steps/component-governance.yml | 13 + .../steps/execute-codeql.yml | 32 ++ .../templates-official/steps/execute-sdl.yml | 88 ++++++ .../steps/generate-sbom.yml | 48 +++ .../templates-official/steps/publish-logs.yml | 23 ++ .../templates-official/steps/retain-build.yml | 28 ++ .../steps/send-to-helix.yml | 91 ++++++ .../templates-official/steps/source-build.yml | 129 ++++++++ .../variables/pool-providers.yml | 45 +++ .../variables/sdl-variables.yml | 7 + global.json | 6 +- 27 files changed, 1771 insertions(+), 23 deletions(-) create mode 100644 eng/common/templates-official/job/job.yml create mode 100644 eng/common/templates-official/job/onelocbuild.yml create mode 100644 eng/common/templates-official/job/publish-build-assets.yml create mode 100644 eng/common/templates-official/job/source-build.yml create mode 100644 eng/common/templates-official/job/source-index-stage1.yml create mode 100644 eng/common/templates-official/jobs/codeql-build.yml create mode 100644 eng/common/templates-official/jobs/jobs.yml create mode 100644 eng/common/templates-official/jobs/source-build.yml create mode 100644 eng/common/templates-official/post-build/common-variables.yml create mode 100644 eng/common/templates-official/post-build/post-build.yml create mode 100644 eng/common/templates-official/post-build/setup-maestro-vars.yml create mode 100644 eng/common/templates-official/post-build/trigger-subscription.yml create mode 100644 eng/common/templates-official/steps/add-build-to-channel.yml create mode 100644 eng/common/templates-official/steps/build-reason.yml create mode 100644 eng/common/templates-official/steps/component-governance.yml create mode 100644 eng/common/templates-official/steps/execute-codeql.yml create mode 100644 eng/common/templates-official/steps/execute-sdl.yml create mode 100644 eng/common/templates-official/steps/generate-sbom.yml create mode 100644 eng/common/templates-official/steps/publish-logs.yml create mode 100644 eng/common/templates-official/steps/retain-build.yml create mode 100644 eng/common/templates-official/steps/send-to-helix.yml create mode 100644 eng/common/templates-official/steps/source-build.yml create mode 100644 eng/common/templates-official/variables/pool-providers.yml create mode 100644 eng/common/templates-official/variables/sdl-variables.yml diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index a71166e86a3..b84fa5d690d 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -121,42 +121,42 @@ - + https://github.com/dotnet/arcade - 042763a811fd94dc3556253d4c64118dd665216e + cbb61c3a9a42e7c3cce17ee453ff5ecdc7f69282 - + https://github.com/dotnet/arcade - 042763a811fd94dc3556253d4c64118dd665216e + cbb61c3a9a42e7c3cce17ee453ff5ecdc7f69282 - + https://github.com/dotnet/arcade - 042763a811fd94dc3556253d4c64118dd665216e + cbb61c3a9a42e7c3cce17ee453ff5ecdc7f69282 - + https://github.com/dotnet/arcade - 042763a811fd94dc3556253d4c64118dd665216e + cbb61c3a9a42e7c3cce17ee453ff5ecdc7f69282 - + https://github.com/dotnet/arcade - 042763a811fd94dc3556253d4c64118dd665216e + cbb61c3a9a42e7c3cce17ee453ff5ecdc7f69282 - + https://github.com/dotnet/arcade - 042763a811fd94dc3556253d4c64118dd665216e + cbb61c3a9a42e7c3cce17ee453ff5ecdc7f69282 - + https://github.com/dotnet/arcade - 042763a811fd94dc3556253d4c64118dd665216e + cbb61c3a9a42e7c3cce17ee453ff5ecdc7f69282 https://github.com/dotnet/xliff-tasks 73f0850939d96131c28cf6ea6ee5aacb4da0083a - + https://github.com/dotnet/arcade - 042763a811fd94dc3556253d4c64118dd665216e + cbb61c3a9a42e7c3cce17ee453ff5ecdc7f69282 diff --git a/eng/Versions.props b/eng/Versions.props index afd7c7add56..b5acadf3920 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -20,10 +20,10 @@ 0.1.55 8.0.0-rc.1.23419.3 13.3.8825-net8-rc1 - 8.0.0-beta.24123.1 - 8.0.0-beta.24123.1 - 8.0.0-beta.24123.1 - 8.0.0-beta.24123.1 + 8.0.0-beta.24151.4 + 8.0.0-beta.24151.4 + 8.0.0-beta.24151.4 + 8.0.0-beta.24151.4 8.2.0 8.2.0 8.0.0 diff --git a/eng/common/templates-official/job/job.yml b/eng/common/templates-official/job/job.yml new file mode 100644 index 00000000000..9e7bebe9af8 --- /dev/null +++ b/eng/common/templates-official/job/job.yml @@ -0,0 +1,255 @@ +# Internal resources (telemetry, microbuild) can only be accessed from non-public projects, +# and some (Microbuild) should only be applied to non-PR cases for internal builds. + +parameters: +# Job schema parameters - https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=vsts&tabs=schema#job + cancelTimeoutInMinutes: '' + condition: '' + container: '' + continueOnError: false + dependsOn: '' + displayName: '' + pool: '' + steps: [] + strategy: '' + timeoutInMinutes: '' + variables: [] + workspace: '' + +# Job base template specific parameters + # See schema documentation - https://github.com/dotnet/arcade/blob/master/Documentation/AzureDevOps/TemplateSchema.md + artifacts: '' + enableMicrobuild: false + enablePublishBuildArtifacts: false + enablePublishBuildAssets: false + enablePublishTestResults: false + enablePublishUsingPipelines: false + enableBuildRetry: false + disableComponentGovernance: '' + componentGovernanceIgnoreDirectories: '' + mergeTestResults: false + testRunTitle: '' + testResultsFormat: '' + name: '' + preSteps: [] + runAsPublic: false +# Sbom related params + enableSbom: true + PackageVersion: 7.0.0 + BuildDropPath: '$(Build.SourcesDirectory)/artifacts' + +jobs: +- job: ${{ parameters.name }} + + ${{ if ne(parameters.cancelTimeoutInMinutes, '') }}: + cancelTimeoutInMinutes: ${{ parameters.cancelTimeoutInMinutes }} + + ${{ if ne(parameters.condition, '') }}: + condition: ${{ parameters.condition }} + + ${{ if ne(parameters.container, '') }}: + container: ${{ parameters.container }} + + ${{ if ne(parameters.continueOnError, '') }}: + continueOnError: ${{ parameters.continueOnError }} + + ${{ if ne(parameters.dependsOn, '') }}: + dependsOn: ${{ parameters.dependsOn }} + + ${{ if ne(parameters.displayName, '') }}: + displayName: ${{ parameters.displayName }} + + ${{ if ne(parameters.pool, '') }}: + pool: ${{ parameters.pool }} + + ${{ if ne(parameters.strategy, '') }}: + strategy: ${{ parameters.strategy }} + + ${{ if ne(parameters.timeoutInMinutes, '') }}: + timeoutInMinutes: ${{ parameters.timeoutInMinutes }} + + variables: + - ${{ if ne(parameters.enableTelemetry, 'false') }}: + - name: DOTNET_CLI_TELEMETRY_PROFILE + value: '$(Build.Repository.Uri)' + - ${{ if eq(parameters.enableRichCodeNavigation, 'true') }}: + - name: EnableRichCodeNavigation + value: 'true' + # Retry signature validation up to three times, waiting 2 seconds between attempts. + # See https://learn.microsoft.com/en-us/nuget/reference/errors-and-warnings/nu3028#retry-untrusted-root-failures + - name: NUGET_EXPERIMENTAL_CHAIN_BUILD_RETRY_POLICY + value: 3,2000 + - ${{ each variable in parameters.variables }}: + # handle name-value variable syntax + # example: + # - name: [key] + # value: [value] + - ${{ if ne(variable.name, '') }}: + - name: ${{ variable.name }} + value: ${{ variable.value }} + + # handle variable groups + - ${{ if ne(variable.group, '') }}: + - group: ${{ variable.group }} + + # handle template variable syntax + # example: + # - template: path/to/template.yml + # parameters: + # [key]: [value] + - ${{ if ne(variable.template, '') }}: + - template: ${{ variable.template }} + ${{ if ne(variable.parameters, '') }}: + parameters: ${{ variable.parameters }} + + # handle key-value variable syntax. + # example: + # - [key]: [value] + - ${{ if and(eq(variable.name, ''), eq(variable.group, ''), eq(variable.template, '')) }}: + - ${{ each pair in variable }}: + - name: ${{ pair.key }} + value: ${{ pair.value }} + + # DotNet-HelixApi-Access provides 'HelixApiAccessToken' for internal builds + - ${{ if and(eq(parameters.enableTelemetry, 'true'), eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - group: DotNet-HelixApi-Access + + ${{ if ne(parameters.workspace, '') }}: + workspace: ${{ parameters.workspace }} + + steps: + - ${{ if ne(parameters.preSteps, '') }}: + - ${{ each preStep in parameters.preSteps }}: + - ${{ preStep }} + + - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - ${{ if eq(parameters.enableMicrobuild, 'true') }}: + - task: MicroBuildSigningPlugin@3 + displayName: Install MicroBuild plugin + inputs: + signType: $(_SignType) + zipSources: false + feedSource: https://dnceng.pkgs.visualstudio.com/_packaging/MicroBuildToolset/nuget/v3/index.json + env: + TeamName: $(_TeamName) + continueOnError: ${{ parameters.continueOnError }} + condition: and(succeeded(), in(variables['_SignType'], 'real', 'test'), eq(variables['Agent.Os'], 'Windows_NT')) + + - ${{ if and(eq(parameters.runAsPublic, 'false'), eq(variables['System.TeamProject'], 'internal')) }}: + - task: NuGetAuthenticate@1 + + - ${{ if and(ne(parameters.artifacts.download, 'false'), ne(parameters.artifacts.download, '')) }}: + - task: DownloadPipelineArtifact@2 + inputs: + buildType: current + artifactName: ${{ coalesce(parameters.artifacts.download.name, 'Artifacts_$(Agent.OS)_$(_BuildConfig)') }} + targetPath: ${{ coalesce(parameters.artifacts.download.path, 'artifacts') }} + itemPattern: ${{ coalesce(parameters.artifacts.download.pattern, '**') }} + + - ${{ each step in parameters.steps }}: + - ${{ step }} + + - ${{ if eq(parameters.enableRichCodeNavigation, true) }}: + - task: RichCodeNavIndexer@0 + displayName: RichCodeNav Upload + inputs: + languages: ${{ coalesce(parameters.richCodeNavigationLanguage, 'csharp') }} + environment: ${{ coalesce(parameters.richCodeNavigationEnvironment, 'production') }} + richNavLogOutputDirectory: $(Build.SourcesDirectory)/artifacts/bin + uploadRichNavArtifacts: ${{ coalesce(parameters.richCodeNavigationUploadArtifacts, false) }} + continueOnError: true + + - template: /eng/common/templates-official/steps/component-governance.yml + parameters: + ${{ if eq(parameters.disableComponentGovernance, '') }}: + ${{ if and(ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest'), eq(parameters.runAsPublic, 'false'), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), startsWith(variables['Build.SourceBranch'], 'refs/heads/dotnet/'), startsWith(variables['Build.SourceBranch'], 'refs/heads/microsoft/'), eq(variables['Build.SourceBranch'], 'refs/heads/main'))) }}: + disableComponentGovernance: false + ${{ else }}: + disableComponentGovernance: true + ${{ else }}: + disableComponentGovernance: ${{ parameters.disableComponentGovernance }} + componentGovernanceIgnoreDirectories: ${{ parameters.componentGovernanceIgnoreDirectories }} + + - ${{ if eq(parameters.enableMicrobuild, 'true') }}: + - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - task: MicroBuildCleanup@1 + displayName: Execute Microbuild cleanup tasks + condition: and(always(), in(variables['_SignType'], 'real', 'test'), eq(variables['Agent.Os'], 'Windows_NT')) + continueOnError: ${{ parameters.continueOnError }} + env: + TeamName: $(_TeamName) + + - ${{ if ne(parameters.artifacts.publish, '') }}: + - ${{ if and(ne(parameters.artifacts.publish.artifacts, 'false'), ne(parameters.artifacts.publish.artifacts, '')) }}: + - task: CopyFiles@2 + displayName: Gather binaries for publish to artifacts + inputs: + SourceFolder: 'artifacts/bin' + Contents: '**' + TargetFolder: '$(Build.ArtifactStagingDirectory)/artifacts/bin' + - task: CopyFiles@2 + displayName: Gather packages for publish to artifacts + inputs: + SourceFolder: 'artifacts/packages' + Contents: '**' + TargetFolder: '$(Build.ArtifactStagingDirectory)/artifacts/packages' + - task: 1ES.PublishBuildArtifacts@1 + displayName: Publish pipeline artifacts + inputs: + PathtoPublish: '$(Build.ArtifactStagingDirectory)/artifacts' + PublishLocation: Container + ArtifactName: ${{ coalesce(parameters.artifacts.publish.artifacts.name , 'Artifacts_$(Agent.Os)_$(_BuildConfig)') }} + continueOnError: true + condition: always() + - ${{ if and(ne(parameters.artifacts.publish.logs, 'false'), ne(parameters.artifacts.publish.logs, '')) }}: + - publish: artifacts/log + artifact: ${{ coalesce(parameters.artifacts.publish.logs.name, 'Logs_Build_$(Agent.Os)_$(_BuildConfig)') }} + displayName: Publish logs + continueOnError: true + condition: always() + + - ${{ if ne(parameters.enablePublishBuildArtifacts, 'false') }}: + - task: 1ES.PublishBuildArtifacts@1 + displayName: Publish Logs + inputs: + PathtoPublish: '$(Build.SourcesDirectory)/artifacts/log/$(_BuildConfig)' + PublishLocation: Container + ArtifactName: ${{ coalesce(parameters.enablePublishBuildArtifacts.artifactName, '$(Agent.Os)_$(Agent.JobName)' ) }} + continueOnError: true + condition: always() + + - ${{ if or(and(eq(parameters.enablePublishTestResults, 'true'), eq(parameters.testResultsFormat, '')), eq(parameters.testResultsFormat, 'xunit')) }}: + - task: PublishTestResults@2 + displayName: Publish XUnit Test Results + inputs: + testResultsFormat: 'xUnit' + testResultsFiles: '*.xml' + searchFolder: '$(Build.SourcesDirectory)/artifacts/TestResults/$(_BuildConfig)' + testRunTitle: ${{ coalesce(parameters.testRunTitle, parameters.name, '$(System.JobName)') }}-xunit + mergeTestResults: ${{ parameters.mergeTestResults }} + continueOnError: true + condition: always() + - ${{ if or(and(eq(parameters.enablePublishTestResults, 'true'), eq(parameters.testResultsFormat, '')), eq(parameters.testResultsFormat, 'vstest')) }}: + - task: PublishTestResults@2 + displayName: Publish TRX Test Results + inputs: + testResultsFormat: 'VSTest' + testResultsFiles: '*.trx' + searchFolder: '$(Build.SourcesDirectory)/artifacts/TestResults/$(_BuildConfig)' + testRunTitle: ${{ coalesce(parameters.testRunTitle, parameters.name, '$(System.JobName)') }}-trx + mergeTestResults: ${{ parameters.mergeTestResults }} + continueOnError: true + condition: always() + + - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest'), eq(parameters.enableSbom, 'true')) }}: + - template: /eng/common/templates-official/steps/generate-sbom.yml + parameters: + PackageVersion: ${{ parameters.packageVersion}} + BuildDropPath: ${{ parameters.buildDropPath }} + IgnoreDirectories: ${{ parameters.componentGovernanceIgnoreDirectories }} + + - ${{ if eq(parameters.enableBuildRetry, 'true') }}: + - publish: $(Build.SourcesDirectory)\eng\common\BuildConfiguration + artifact: BuildConfiguration + displayName: Publish build retry configuration + continueOnError: true diff --git a/eng/common/templates-official/job/onelocbuild.yml b/eng/common/templates-official/job/onelocbuild.yml new file mode 100644 index 00000000000..ba9ba493032 --- /dev/null +++ b/eng/common/templates-official/job/onelocbuild.yml @@ -0,0 +1,112 @@ +parameters: + # Optional: dependencies of the job + dependsOn: '' + + # Optional: A defined YAML pool - https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=vsts&tabs=schema#pool + pool: '' + + CeapexPat: $(dn-bot-ceapex-package-r) # PAT for the loc AzDO instance https://dev.azure.com/ceapex + GithubPat: $(BotAccount-dotnet-bot-repo-PAT) + + SourcesDirectory: $(Build.SourcesDirectory) + CreatePr: true + AutoCompletePr: false + ReusePr: true + UseLfLineEndings: true + UseCheckedInLocProjectJson: false + SkipLocProjectJsonGeneration: false + LanguageSet: VS_Main_Languages + LclSource: lclFilesInRepo + LclPackageId: '' + RepoType: gitHub + GitHubOrg: dotnet + MirrorRepo: '' + MirrorBranch: main + condition: '' + JobNameSuffix: '' + +jobs: +- job: OneLocBuild${{ parameters.JobNameSuffix }} + + dependsOn: ${{ parameters.dependsOn }} + + displayName: OneLocBuild${{ parameters.JobNameSuffix }} + + variables: + - group: OneLocBuildVariables # Contains the CeapexPat and GithubPat + - name: _GenerateLocProjectArguments + value: -SourcesDirectory ${{ parameters.SourcesDirectory }} + -LanguageSet "${{ parameters.LanguageSet }}" + -CreateNeutralXlfs + - ${{ if eq(parameters.UseCheckedInLocProjectJson, 'true') }}: + - name: _GenerateLocProjectArguments + value: ${{ variables._GenerateLocProjectArguments }} -UseCheckedInLocProjectJson + - template: /eng/common/templates-official/variables/pool-providers.yml + + ${{ if ne(parameters.pool, '') }}: + pool: ${{ parameters.pool }} + ${{ if eq(parameters.pool, '') }}: + pool: + # We don't use the collection uri here because it might vary (.visualstudio.com vs. dev.azure.com) + ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: + name: AzurePipelines-EO + image: 1ESPT-Windows2022 + demands: Cmd + os: windows + # If it's not devdiv, it's dnceng + ${{ if ne(variables['System.TeamProject'], 'DevDiv') }}: + name: $(DncEngInternalBuildPool) + image: 1es-windows-2022-pt + os: windows + + steps: + - ${{ if ne(parameters.SkipLocProjectJsonGeneration, 'true') }}: + - task: Powershell@2 + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/generate-locproject.ps1 + arguments: $(_GenerateLocProjectArguments) + displayName: Generate LocProject.json + condition: ${{ parameters.condition }} + + - task: OneLocBuild@2 + displayName: OneLocBuild + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + inputs: + locProj: eng/Localize/LocProject.json + outDir: $(Build.ArtifactStagingDirectory) + lclSource: ${{ parameters.LclSource }} + lclPackageId: ${{ parameters.LclPackageId }} + isCreatePrSelected: ${{ parameters.CreatePr }} + isAutoCompletePrSelected: ${{ parameters.AutoCompletePr }} + ${{ if eq(parameters.CreatePr, true) }}: + isUseLfLineEndingsSelected: ${{ parameters.UseLfLineEndings }} + ${{ if eq(parameters.RepoType, 'gitHub') }}: + isShouldReusePrSelected: ${{ parameters.ReusePr }} + packageSourceAuth: patAuth + patVariable: ${{ parameters.CeapexPat }} + ${{ if eq(parameters.RepoType, 'gitHub') }}: + repoType: ${{ parameters.RepoType }} + gitHubPatVariable: "${{ parameters.GithubPat }}" + ${{ if ne(parameters.MirrorRepo, '') }}: + isMirrorRepoSelected: true + gitHubOrganization: ${{ parameters.GitHubOrg }} + mirrorRepo: ${{ parameters.MirrorRepo }} + mirrorBranch: ${{ parameters.MirrorBranch }} + condition: ${{ parameters.condition }} + + - task: 1ES.PublishBuildArtifacts@1 + displayName: Publish Localization Files + inputs: + PathtoPublish: '$(Build.ArtifactStagingDirectory)/loc' + PublishLocation: Container + ArtifactName: Loc + condition: ${{ parameters.condition }} + + - task: 1ES.PublishBuildArtifacts@1 + displayName: Publish LocProject.json + inputs: + PathtoPublish: '$(Build.SourcesDirectory)/eng/Localize/' + PublishLocation: Container + ArtifactName: Loc + condition: ${{ parameters.condition }} \ No newline at end of file diff --git a/eng/common/templates-official/job/publish-build-assets.yml b/eng/common/templates-official/job/publish-build-assets.yml new file mode 100644 index 00000000000..ea5104625fa --- /dev/null +++ b/eng/common/templates-official/job/publish-build-assets.yml @@ -0,0 +1,153 @@ +parameters: + configuration: 'Debug' + + # Optional: condition for the job to run + condition: '' + + # Optional: 'true' if future jobs should run even if this job fails + continueOnError: false + + # Optional: dependencies of the job + dependsOn: '' + + # Optional: Include PublishBuildArtifacts task + enablePublishBuildArtifacts: false + + # Optional: A defined YAML pool - https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=vsts&tabs=schema#pool + pool: {} + + # Optional: should run as a public build even in the internal project + # if 'true', the build won't run any of the internal only steps, even if it is running in non-public projects. + runAsPublic: false + + # Optional: whether the build's artifacts will be published using release pipelines or direct feed publishing + publishUsingPipelines: false + + # Optional: whether the build's artifacts will be published using release pipelines or direct feed publishing + publishAssetsImmediately: false + + artifactsPublishingAdditionalParameters: '' + + signingValidationAdditionalParameters: '' + +jobs: +- job: Asset_Registry_Publish + + dependsOn: ${{ parameters.dependsOn }} + timeoutInMinutes: 150 + + ${{ if eq(parameters.publishAssetsImmediately, 'true') }}: + displayName: Publish Assets + ${{ else }}: + displayName: Publish to Build Asset Registry + + variables: + - template: /eng/common/templates-official/variables/pool-providers.yml + - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - group: Publish-Build-Assets + - group: AzureDevOps-Artifact-Feeds-Pats + - name: runCodesignValidationInjection + value: false + - ${{ if eq(parameters.publishAssetsImmediately, 'true') }}: + - template: /eng/common/templates-official/post-build/common-variables.yml + + pool: + # We don't use the collection uri here because it might vary (.visualstudio.com vs. dev.azure.com) + ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: + name: AzurePipelines-EO + image: 1ESPT-Windows2022 + demands: Cmd + os: windows + # If it's not devdiv, it's dnceng + ${{ if ne(variables['System.TeamProject'], 'DevDiv') }}: + name: $(DncEngInternalBuildPool) + image: 1es-windows-2022-pt + os: windows + steps: + - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - task: DownloadBuildArtifacts@0 + displayName: Download artifact + inputs: + artifactName: AssetManifests + downloadPath: '$(Build.StagingDirectory)/Download' + checkDownloadedFiles: true + condition: ${{ parameters.condition }} + continueOnError: ${{ parameters.continueOnError }} + + - task: NuGetAuthenticate@1 + + - task: PowerShell@2 + displayName: Publish Build Assets + inputs: + filePath: eng\common\sdk-task.ps1 + arguments: -task PublishBuildAssets -restore -msbuildEngine dotnet + /p:ManifestsPath='$(Build.StagingDirectory)/Download/AssetManifests' + /p:BuildAssetRegistryToken=$(MaestroAccessToken) + /p:MaestroApiEndpoint=https://maestro-prod.westus2.cloudapp.azure.com + /p:PublishUsingPipelines=${{ parameters.publishUsingPipelines }} + /p:OfficialBuildId=$(Build.BuildNumber) + condition: ${{ parameters.condition }} + continueOnError: ${{ parameters.continueOnError }} + + - task: powershell@2 + displayName: Create ReleaseConfigs Artifact + inputs: + targetType: inline + script: | + Add-Content -Path "$(Build.StagingDirectory)/ReleaseConfigs.txt" -Value $(BARBuildId) + Add-Content -Path "$(Build.StagingDirectory)/ReleaseConfigs.txt" -Value "$(DefaultChannels)" + Add-Content -Path "$(Build.StagingDirectory)/ReleaseConfigs.txt" -Value $(IsStableBuild) + + - task: 1ES.PublishBuildArtifacts@1 + displayName: Publish ReleaseConfigs Artifact + inputs: + PathtoPublish: '$(Build.StagingDirectory)/ReleaseConfigs.txt' + PublishLocation: Container + ArtifactName: ReleaseConfigs + + - task: powershell@2 + displayName: Check if SymbolPublishingExclusionsFile.txt exists + inputs: + targetType: inline + script: | + $symbolExclusionfile = "$(Build.SourcesDirectory)/eng/SymbolPublishingExclusionsFile.txt" + if(Test-Path -Path $symbolExclusionfile) + { + Write-Host "SymbolExclusionFile exists" + Write-Host "##vso[task.setvariable variable=SymbolExclusionFile]true" + } + else{ + Write-Host "Symbols Exclusion file does not exists" + Write-Host "##vso[task.setvariable variable=SymbolExclusionFile]false" + } + + - task: 1ES.PublishBuildArtifacts@1 + displayName: Publish SymbolPublishingExclusionsFile Artifact + condition: eq(variables['SymbolExclusionFile'], 'true') + inputs: + PathtoPublish: '$(Build.SourcesDirectory)/eng/SymbolPublishingExclusionsFile.txt' + PublishLocation: Container + ArtifactName: ReleaseConfigs + + - ${{ if eq(parameters.publishAssetsImmediately, 'true') }}: + - template: /eng/common/templates-official/post-build/setup-maestro-vars.yml + parameters: + BARBuildId: ${{ parameters.BARBuildId }} + PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} + + - task: PowerShell@2 + displayName: Publish Using Darc + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/publish-using-darc.ps1 + arguments: -BuildId $(BARBuildId) + -PublishingInfraVersion 3 + -AzdoToken '$(publishing-dnceng-devdiv-code-r-build-re)' + -MaestroToken '$(MaestroApiAccessToken)' + -WaitPublishingFinish true + -ArtifactsPublishingAdditionalParameters '${{ parameters.artifactsPublishingAdditionalParameters }}' + -SymbolPublishingAdditionalParameters '${{ parameters.symbolPublishingAdditionalParameters }}' + + - ${{ if eq(parameters.enablePublishBuildArtifacts, 'true') }}: + - template: /eng/common/templates-official/steps/publish-logs.yml + parameters: + JobLabel: 'Publish_Artifacts_Logs' diff --git a/eng/common/templates-official/job/source-build.yml b/eng/common/templates-official/job/source-build.yml new file mode 100644 index 00000000000..8aba3b44bb2 --- /dev/null +++ b/eng/common/templates-official/job/source-build.yml @@ -0,0 +1,67 @@ +parameters: + # This template adds arcade-powered source-build to CI. The template produces a server job with a + # default ID 'Source_Build_Complete' to put in a dependency list if necessary. + + # Specifies the prefix for source-build jobs added to pipeline. Use this if disambiguation needed. + jobNamePrefix: 'Source_Build' + + # Defines the platform on which to run the job. By default, a linux-x64 machine, suitable for + # managed-only repositories. This is an object with these properties: + # + # name: '' + # The name of the job. This is included in the job ID. + # targetRID: '' + # The name of the target RID to use, instead of the one auto-detected by Arcade. + # nonPortable: false + # Enables non-portable mode. This means a more specific RID (e.g. fedora.32-x64 rather than + # linux-x64), and compiling against distro-provided packages rather than portable ones. + # skipPublishValidation: false + # Disables publishing validation. By default, a check is performed to ensure no packages are + # published by source-build. + # container: '' + # A container to use. Runs in docker. + # pool: {} + # A pool to use. Runs directly on an agent. + # buildScript: '' + # Specifies the build script to invoke to perform the build in the repo. The default + # './build.sh' should work for typical Arcade repositories, but this is customizable for + # difficult situations. + # jobProperties: {} + # A list of job properties to inject at the top level, for potential extensibility beyond + # container and pool. + platform: {} + +jobs: +- job: ${{ parameters.jobNamePrefix }}_${{ parameters.platform.name }} + displayName: Source-Build (${{ parameters.platform.name }}) + + ${{ each property in parameters.platform.jobProperties }}: + ${{ property.key }}: ${{ property.value }} + + ${{ if ne(parameters.platform.container, '') }}: + container: ${{ parameters.platform.container }} + + ${{ if eq(parameters.platform.pool, '') }}: + # The default VM host AzDO pool. This should be capable of running Docker containers: almost all + # source-build builds run in Docker, including the default managed platform. + # /eng/common/templates-official/variables/pool-providers.yml can't be used here (some customers declare variables already), so duplicate its logic + pool: + ${{ if eq(variables['System.TeamProject'], 'public') }}: + name: $[replace(replace(eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'), True, 'NetCore-Svc-Public' ), False, 'NetCore-Public')] + demands: ImageOverride -equals Build.Ubuntu.1804.Amd64.Open + + ${{ if eq(variables['System.TeamProject'], 'internal') }}: + name: $[replace(replace(eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'), True, 'NetCore1ESPool-Svc-Internal'), False, 'NetCore1ESPool-Internal')] + image: 1es-mariner-2-pt + os: linux + + ${{ if ne(parameters.platform.pool, '') }}: + pool: ${{ parameters.platform.pool }} + + workspace: + clean: all + + steps: + - template: /eng/common/templates-official/steps/source-build.yml + parameters: + platform: ${{ parameters.platform }} diff --git a/eng/common/templates-official/job/source-index-stage1.yml b/eng/common/templates-official/job/source-index-stage1.yml new file mode 100644 index 00000000000..4b633739170 --- /dev/null +++ b/eng/common/templates-official/job/source-index-stage1.yml @@ -0,0 +1,68 @@ +parameters: + runAsPublic: false + sourceIndexPackageVersion: 1.0.1-20230228.2 + sourceIndexPackageSource: https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json + sourceIndexBuildCommand: powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "eng/common/build.ps1 -restore -build -binarylog -ci" + preSteps: [] + binlogPath: artifacts/log/Debug/Build.binlog + condition: '' + dependsOn: '' + pool: '' + +jobs: +- job: SourceIndexStage1 + dependsOn: ${{ parameters.dependsOn }} + condition: ${{ parameters.condition }} + variables: + - name: SourceIndexPackageVersion + value: ${{ parameters.sourceIndexPackageVersion }} + - name: SourceIndexPackageSource + value: ${{ parameters.sourceIndexPackageSource }} + - name: BinlogPath + value: ${{ parameters.binlogPath }} + - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - group: source-dot-net stage1 variables + - template: /eng/common/templates-official/variables/pool-providers.yml + + ${{ if ne(parameters.pool, '') }}: + pool: ${{ parameters.pool }} + ${{ if eq(parameters.pool, '') }}: + pool: + ${{ if eq(variables['System.TeamProject'], 'public') }}: + name: $(DncEngPublicBuildPool) + demands: ImageOverride -equals windows.vs2019.amd64.open + ${{ if eq(variables['System.TeamProject'], 'internal') }}: + name: $(DncEngInternalBuildPool) + image: 1es-windows-2022-pt + os: windows + + steps: + - ${{ each preStep in parameters.preSteps }}: + - ${{ preStep }} + + - task: UseDotNet@2 + displayName: Use .NET Core SDK 6 + inputs: + packageType: sdk + version: 6.0.x + installationPath: $(Agent.TempDirectory)/dotnet + workingDirectory: $(Agent.TempDirectory) + + - script: | + $(Agent.TempDirectory)/dotnet/dotnet tool install BinLogToSln --version $(SourceIndexPackageVersion) --add-source $(SourceIndexPackageSource) --tool-path $(Agent.TempDirectory)/.source-index/tools + $(Agent.TempDirectory)/dotnet/dotnet tool install UploadIndexStage1 --version $(SourceIndexPackageVersion) --add-source $(SourceIndexPackageSource) --tool-path $(Agent.TempDirectory)/.source-index/tools + displayName: Download Tools + # Set working directory to temp directory so 'dotnet' doesn't try to use global.json and use the repo's sdk. + workingDirectory: $(Agent.TempDirectory) + + - script: ${{ parameters.sourceIndexBuildCommand }} + displayName: Build Repository + + - script: $(Agent.TempDirectory)/.source-index/tools/BinLogToSln -i $(BinlogPath) -r $(Build.SourcesDirectory) -n $(Build.Repository.Name) -o .source-index/stage1output + displayName: Process Binlog into indexable sln + + - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - script: $(Agent.TempDirectory)/.source-index/tools/UploadIndexStage1 -i .source-index/stage1output -n $(Build.Repository.Name) + displayName: Upload stage1 artifacts to source index + env: + BLOB_CONTAINER_URL: $(source-dot-net-stage1-blob-container-url) diff --git a/eng/common/templates-official/jobs/codeql-build.yml b/eng/common/templates-official/jobs/codeql-build.yml new file mode 100644 index 00000000000..b68d3c2f319 --- /dev/null +++ b/eng/common/templates-official/jobs/codeql-build.yml @@ -0,0 +1,31 @@ +parameters: + # See schema documentation in /Documentation/AzureDevOps/TemplateSchema.md + continueOnError: false + # Required: A collection of jobs to run - https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=vsts&tabs=schema#job + jobs: [] + # Optional: if specified, restore and use this version of Guardian instead of the default. + overrideGuardianVersion: '' + +jobs: +- template: /eng/common/templates-official/jobs/jobs.yml + parameters: + enableMicrobuild: false + enablePublishBuildArtifacts: false + enablePublishTestResults: false + enablePublishBuildAssets: false + enablePublishUsingPipelines: false + enableTelemetry: true + + variables: + - group: Publish-Build-Assets + # The Guardian version specified in 'eng/common/sdl/packages.config'. This value must be kept in + # sync with the packages.config file. + - name: DefaultGuardianVersion + value: 0.109.0 + - name: GuardianPackagesConfigFile + value: $(Build.SourcesDirectory)\eng\common\sdl\packages.config + - name: GuardianVersion + value: ${{ coalesce(parameters.overrideGuardianVersion, '$(DefaultGuardianVersion)') }} + + jobs: ${{ parameters.jobs }} + diff --git a/eng/common/templates-official/jobs/jobs.yml b/eng/common/templates-official/jobs/jobs.yml new file mode 100644 index 00000000000..857a0f8ba43 --- /dev/null +++ b/eng/common/templates-official/jobs/jobs.yml @@ -0,0 +1,97 @@ +parameters: + # See schema documentation in /Documentation/AzureDevOps/TemplateSchema.md + continueOnError: false + + # Optional: Include PublishBuildArtifacts task + enablePublishBuildArtifacts: false + + # Optional: Enable publishing using release pipelines + enablePublishUsingPipelines: false + + # Optional: Enable running the source-build jobs to build repo from source + enableSourceBuild: false + + # Optional: Parameters for source-build template. + # See /eng/common/templates-official/jobs/source-build.yml for options + sourceBuildParameters: [] + + graphFileGeneration: + # Optional: Enable generating the graph files at the end of the build + enabled: false + # Optional: Include toolset dependencies in the generated graph files + includeToolset: false + + # Required: A collection of jobs to run - https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=vsts&tabs=schema#job + jobs: [] + + # Optional: Override automatically derived dependsOn value for "publish build assets" job + publishBuildAssetsDependsOn: '' + + # Optional: Publish the assets as soon as the publish to BAR stage is complete, rather doing so in a separate stage. + publishAssetsImmediately: false + + # Optional: If using publishAssetsImmediately and additional parameters are needed, can be used to send along additional parameters (normally sent to post-build.yml) + artifactsPublishingAdditionalParameters: '' + signingValidationAdditionalParameters: '' + + # Optional: should run as a public build even in the internal project + # if 'true', the build won't run any of the internal only steps, even if it is running in non-public projects. + runAsPublic: false + + enableSourceIndex: false + sourceIndexParams: {} + +# Internal resources (telemetry, microbuild) can only be accessed from non-public projects, +# and some (Microbuild) should only be applied to non-PR cases for internal builds. + +jobs: +- ${{ each job in parameters.jobs }}: + - template: ../job/job.yml + parameters: + # pass along parameters + ${{ each parameter in parameters }}: + ${{ if ne(parameter.key, 'jobs') }}: + ${{ parameter.key }}: ${{ parameter.value }} + + # pass along job properties + ${{ each property in job }}: + ${{ if ne(property.key, 'job') }}: + ${{ property.key }}: ${{ property.value }} + + name: ${{ job.job }} + +- ${{ if eq(parameters.enableSourceBuild, true) }}: + - template: /eng/common/templates-official/jobs/source-build.yml + parameters: + allCompletedJobId: Source_Build_Complete + ${{ each parameter in parameters.sourceBuildParameters }}: + ${{ parameter.key }}: ${{ parameter.value }} + +- ${{ if eq(parameters.enableSourceIndex, 'true') }}: + - template: ../job/source-index-stage1.yml + parameters: + runAsPublic: ${{ parameters.runAsPublic }} + ${{ each parameter in parameters.sourceIndexParams }}: + ${{ parameter.key }}: ${{ parameter.value }} + +- ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - ${{ if or(eq(parameters.enablePublishBuildAssets, true), eq(parameters.artifacts.publish.manifests, 'true'), ne(parameters.artifacts.publish.manifests, '')) }}: + - template: ../job/publish-build-assets.yml + parameters: + continueOnError: ${{ parameters.continueOnError }} + dependsOn: + - ${{ if ne(parameters.publishBuildAssetsDependsOn, '') }}: + - ${{ each job in parameters.publishBuildAssetsDependsOn }}: + - ${{ job.job }} + - ${{ if eq(parameters.publishBuildAssetsDependsOn, '') }}: + - ${{ each job in parameters.jobs }}: + - ${{ job.job }} + - ${{ if eq(parameters.enableSourceBuild, true) }}: + - Source_Build_Complete + + runAsPublic: ${{ parameters.runAsPublic }} + publishUsingPipelines: ${{ parameters.enablePublishUsingPipelines }} + publishAssetsImmediately: ${{ parameters.publishAssetsImmediately }} + enablePublishBuildArtifacts: ${{ parameters.enablePublishBuildArtifacts }} + artifactsPublishingAdditionalParameters: ${{ parameters.artifactsPublishingAdditionalParameters }} + signingValidationAdditionalParameters: ${{ parameters.signingValidationAdditionalParameters }} diff --git a/eng/common/templates-official/jobs/source-build.yml b/eng/common/templates-official/jobs/source-build.yml new file mode 100644 index 00000000000..08e5db9bb11 --- /dev/null +++ b/eng/common/templates-official/jobs/source-build.yml @@ -0,0 +1,46 @@ +parameters: + # This template adds arcade-powered source-build to CI. A job is created for each platform, as + # well as an optional server job that completes when all platform jobs complete. + + # The name of the "join" job for all source-build platforms. If set to empty string, the job is + # not included. Existing repo pipelines can use this job depend on all source-build jobs + # completing without maintaining a separate list of every single job ID: just depend on this one + # server job. By default, not included. Recommended name if used: 'Source_Build_Complete'. + allCompletedJobId: '' + + # See /eng/common/templates-official/job/source-build.yml + jobNamePrefix: 'Source_Build' + + # This is the default platform provided by Arcade, intended for use by a managed-only repo. + defaultManagedPlatform: + name: 'Managed' + container: 'mcr.microsoft.com/dotnet-buildtools/prereqs:centos-stream8' + + # Defines the platforms on which to run build jobs. One job is created for each platform, and the + # object in this array is sent to the job template as 'platform'. If no platforms are specified, + # one job runs on 'defaultManagedPlatform'. + platforms: [] + +jobs: + +- ${{ if ne(parameters.allCompletedJobId, '') }}: + - job: ${{ parameters.allCompletedJobId }} + displayName: Source-Build Complete + pool: server + dependsOn: + - ${{ each platform in parameters.platforms }}: + - ${{ parameters.jobNamePrefix }}_${{ platform.name }} + - ${{ if eq(length(parameters.platforms), 0) }}: + - ${{ parameters.jobNamePrefix }}_${{ parameters.defaultManagedPlatform.name }} + +- ${{ each platform in parameters.platforms }}: + - template: /eng/common/templates-official/job/source-build.yml + parameters: + jobNamePrefix: ${{ parameters.jobNamePrefix }} + platform: ${{ platform }} + +- ${{ if eq(length(parameters.platforms), 0) }}: + - template: /eng/common/templates-official/job/source-build.yml + parameters: + jobNamePrefix: ${{ parameters.jobNamePrefix }} + platform: ${{ parameters.defaultManagedPlatform }} diff --git a/eng/common/templates-official/post-build/common-variables.yml b/eng/common/templates-official/post-build/common-variables.yml new file mode 100644 index 00000000000..c24193acfc9 --- /dev/null +++ b/eng/common/templates-official/post-build/common-variables.yml @@ -0,0 +1,22 @@ +variables: + - group: Publish-Build-Assets + + # Whether the build is internal or not + - name: IsInternalBuild + value: ${{ and(ne(variables['System.TeamProject'], 'public'), contains(variables['Build.SourceBranch'], 'internal')) }} + + # Default Maestro++ API Endpoint and API Version + - name: MaestroApiEndPoint + value: "https://maestro-prod.westus2.cloudapp.azure.com" + - name: MaestroApiAccessToken + value: $(MaestroAccessToken) + - name: MaestroApiVersion + value: "2020-02-20" + + - name: SourceLinkCLIVersion + value: 3.0.0 + - name: SymbolToolVersion + value: 1.0.1 + + - name: runCodesignValidationInjection + value: false diff --git a/eng/common/templates-official/post-build/post-build.yml b/eng/common/templates-official/post-build/post-build.yml new file mode 100644 index 00000000000..5c98fe1c0f3 --- /dev/null +++ b/eng/common/templates-official/post-build/post-build.yml @@ -0,0 +1,285 @@ +parameters: + # Which publishing infra should be used. THIS SHOULD MATCH THE VERSION ON THE BUILD MANIFEST. + # Publishing V1 is no longer supported + # Publishing V2 is no longer supported + # Publishing V3 is the default + - name: publishingInfraVersion + displayName: Which version of publishing should be used to promote the build definition? + type: number + default: 3 + values: + - 3 + + - name: BARBuildId + displayName: BAR Build Id + type: number + default: 0 + + - name: PromoteToChannelIds + displayName: Channel to promote BARBuildId to + type: string + default: '' + + - name: enableSourceLinkValidation + displayName: Enable SourceLink validation + type: boolean + default: false + + - name: enableSigningValidation + displayName: Enable signing validation + type: boolean + default: true + + - name: enableSymbolValidation + displayName: Enable symbol validation + type: boolean + default: false + + - name: enableNugetValidation + displayName: Enable NuGet validation + type: boolean + default: true + + - name: publishInstallersAndChecksums + displayName: Publish installers and checksums + type: boolean + default: true + + - name: SDLValidationParameters + type: object + default: + enable: false + publishGdn: false + continueOnError: false + params: '' + artifactNames: '' + downloadArtifacts: true + + # These parameters let the user customize the call to sdk-task.ps1 for publishing + # symbols & general artifacts as well as for signing validation + - name: symbolPublishingAdditionalParameters + displayName: Symbol publishing additional parameters + type: string + default: '' + + - name: artifactsPublishingAdditionalParameters + displayName: Artifact publishing additional parameters + type: string + default: '' + + - name: signingValidationAdditionalParameters + displayName: Signing validation additional parameters + type: string + default: '' + + # Which stages should finish execution before post-build stages start + - name: validateDependsOn + type: object + default: + - build + + - name: publishDependsOn + type: object + default: + - Validate + + # Optional: Call asset publishing rather than running in a separate stage + - name: publishAssetsImmediately + type: boolean + default: false + +stages: +- ${{ if or(eq( parameters.enableNugetValidation, 'true'), eq(parameters.enableSigningValidation, 'true'), eq(parameters.enableSourceLinkValidation, 'true'), eq(parameters.SDLValidationParameters.enable, 'true')) }}: + - stage: Validate + dependsOn: ${{ parameters.validateDependsOn }} + displayName: Validate Build Assets + variables: + - template: common-variables.yml + - template: /eng/common/templates-official/variables/pool-providers.yml + jobs: + - job: + displayName: NuGet Validation + condition: and(succeededOrFailed(), eq( ${{ parameters.enableNugetValidation }}, 'true')) + pool: + # We don't use the collection uri here because it might vary (.visualstudio.com vs. dev.azure.com) + ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: + name: AzurePipelines-EO + image: 1ESPT-Windows2022 + demands: Cmd + os: windows + # If it's not devdiv, it's dnceng + ${{ else }}: + name: $(DncEngInternalBuildPool) + image: 1es-windows-2022-pt + os: windows + + steps: + - template: setup-maestro-vars.yml + parameters: + BARBuildId: ${{ parameters.BARBuildId }} + PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} + + - task: DownloadBuildArtifacts@0 + displayName: Download Package Artifacts + inputs: + buildType: specific + buildVersionToDownload: specific + project: $(AzDOProjectName) + pipeline: $(AzDOPipelineId) + buildId: $(AzDOBuildId) + artifactName: PackageArtifacts + checkDownloadedFiles: true + + - task: PowerShell@2 + displayName: Validate + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/nuget-validation.ps1 + arguments: -PackagesPath $(Build.ArtifactStagingDirectory)/PackageArtifacts/ + -ToolDestinationPath $(Agent.BuildDirectory)/Extract/ + + - job: + displayName: Signing Validation + condition: and( eq( ${{ parameters.enableSigningValidation }}, 'true'), ne( variables['PostBuildSign'], 'true')) + pool: + # We don't use the collection uri here because it might vary (.visualstudio.com vs. dev.azure.com) + ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: + name: AzurePipelines-EO + image: 1ESPT-Windows2022 + demands: Cmd + os: windows + # If it's not devdiv, it's dnceng + ${{ else }}: + name: $(DncEngInternalBuildPool) + image: 1es-windows-2022-pt + os: windows + steps: + - template: setup-maestro-vars.yml + parameters: + BARBuildId: ${{ parameters.BARBuildId }} + PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} + + - task: DownloadBuildArtifacts@0 + displayName: Download Package Artifacts + inputs: + buildType: specific + buildVersionToDownload: specific + project: $(AzDOProjectName) + pipeline: $(AzDOPipelineId) + buildId: $(AzDOBuildId) + artifactName: PackageArtifacts + checkDownloadedFiles: true + itemPattern: | + ** + !**/Microsoft.SourceBuild.Intermediate.*.nupkg + + # This is necessary whenever we want to publish/restore to an AzDO private feed + # Since sdk-task.ps1 tries to restore packages we need to do this authentication here + # otherwise it'll complain about accessing a private feed. + - task: NuGetAuthenticate@1 + displayName: 'Authenticate to AzDO Feeds' + + # Signing validation will optionally work with the buildmanifest file which is downloaded from + # Azure DevOps above. + - task: PowerShell@2 + displayName: Validate + inputs: + filePath: eng\common\sdk-task.ps1 + arguments: -task SigningValidation -restore -msbuildEngine vs + /p:PackageBasePath='$(Build.ArtifactStagingDirectory)/PackageArtifacts' + /p:SignCheckExclusionsFile='$(Build.SourcesDirectory)/eng/SignCheckExclusionsFile.txt' + ${{ parameters.signingValidationAdditionalParameters }} + + - template: ../steps/publish-logs.yml + parameters: + StageLabel: 'Validation' + JobLabel: 'Signing' + BinlogToolVersion: $(BinlogToolVersion) + + - job: + displayName: SourceLink Validation + condition: eq( ${{ parameters.enableSourceLinkValidation }}, 'true') + pool: + # We don't use the collection uri here because it might vary (.visualstudio.com vs. dev.azure.com) + ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: + name: AzurePipelines-EO + image: 1ESPT-Windows2022 + demands: Cmd + os: windows + # If it's not devdiv, it's dnceng + ${{ else }}: + name: $(DncEngInternalBuildPool) + image: 1es-windows-2022-pt + os: windows + steps: + - template: setup-maestro-vars.yml + parameters: + BARBuildId: ${{ parameters.BARBuildId }} + PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} + + - task: DownloadBuildArtifacts@0 + displayName: Download Blob Artifacts + inputs: + buildType: specific + buildVersionToDownload: specific + project: $(AzDOProjectName) + pipeline: $(AzDOPipelineId) + buildId: $(AzDOBuildId) + artifactName: BlobArtifacts + checkDownloadedFiles: true + + - task: PowerShell@2 + displayName: Validate + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/sourcelink-validation.ps1 + arguments: -InputPath $(Build.ArtifactStagingDirectory)/BlobArtifacts/ + -ExtractPath $(Agent.BuildDirectory)/Extract/ + -GHRepoName $(Build.Repository.Name) + -GHCommit $(Build.SourceVersion) + -SourcelinkCliVersion $(SourceLinkCLIVersion) + continueOnError: true + +- ${{ if ne(parameters.publishAssetsImmediately, 'true') }}: + - stage: publish_using_darc + ${{ if or(eq(parameters.enableNugetValidation, 'true'), eq(parameters.enableSigningValidation, 'true'), eq(parameters.enableSourceLinkValidation, 'true'), eq(parameters.SDLValidationParameters.enable, 'true')) }}: + dependsOn: ${{ parameters.publishDependsOn }} + ${{ else }}: + dependsOn: ${{ parameters.validateDependsOn }} + displayName: Publish using Darc + variables: + - template: common-variables.yml + - template: /eng/common/templates-official/variables/pool-providers.yml + jobs: + - job: + displayName: Publish Using Darc + timeoutInMinutes: 120 + pool: + # We don't use the collection uri here because it might vary (.visualstudio.com vs. dev.azure.com) + ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: + name: AzurePipelines-EO + image: 1ESPT-Windows2022 + demands: Cmd + os: windows + # If it's not devdiv, it's dnceng + ${{ else }}: + name: $(DncEngInternalBuildPool) + image: 1es-windows-2022-pt + os: windows + steps: + - template: setup-maestro-vars.yml + parameters: + BARBuildId: ${{ parameters.BARBuildId }} + PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} + + - task: NuGetAuthenticate@1 + + - task: PowerShell@2 + displayName: Publish Using Darc + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/publish-using-darc.ps1 + arguments: -BuildId $(BARBuildId) + -PublishingInfraVersion ${{ parameters.publishingInfraVersion }} + -AzdoToken '$(publishing-dnceng-devdiv-code-r-build-re)' + -MaestroToken '$(MaestroApiAccessToken)' + -WaitPublishingFinish true + -ArtifactsPublishingAdditionalParameters '${{ parameters.artifactsPublishingAdditionalParameters }}' + -SymbolPublishingAdditionalParameters '${{ parameters.symbolPublishingAdditionalParameters }}' diff --git a/eng/common/templates-official/post-build/setup-maestro-vars.yml b/eng/common/templates-official/post-build/setup-maestro-vars.yml new file mode 100644 index 00000000000..0c87f149a4a --- /dev/null +++ b/eng/common/templates-official/post-build/setup-maestro-vars.yml @@ -0,0 +1,70 @@ +parameters: + BARBuildId: '' + PromoteToChannelIds: '' + +steps: + - ${{ if eq(coalesce(parameters.PromoteToChannelIds, 0), 0) }}: + - task: DownloadBuildArtifacts@0 + displayName: Download Release Configs + inputs: + buildType: current + artifactName: ReleaseConfigs + checkDownloadedFiles: true + + - task: PowerShell@2 + name: setReleaseVars + displayName: Set Release Configs Vars + inputs: + targetType: inline + pwsh: true + script: | + try { + if (!$Env:PromoteToMaestroChannels -or $Env:PromoteToMaestroChannels.Trim() -eq '') { + $Content = Get-Content $(Build.StagingDirectory)/ReleaseConfigs/ReleaseConfigs.txt + + $BarId = $Content | Select -Index 0 + $Channels = $Content | Select -Index 1 + $IsStableBuild = $Content | Select -Index 2 + + $AzureDevOpsProject = $Env:System_TeamProject + $AzureDevOpsBuildDefinitionId = $Env:System_DefinitionId + $AzureDevOpsBuildId = $Env:Build_BuildId + } + else { + $buildApiEndpoint = "${Env:MaestroApiEndPoint}/api/builds/${Env:BARBuildId}?api-version=${Env:MaestroApiVersion}" + + $apiHeaders = New-Object 'System.Collections.Generic.Dictionary[[String],[String]]' + $apiHeaders.Add('Accept', 'application/json') + $apiHeaders.Add('Authorization',"Bearer ${Env:MAESTRO_API_TOKEN}") + + $buildInfo = try { Invoke-WebRequest -Method Get -Uri $buildApiEndpoint -Headers $apiHeaders | ConvertFrom-Json } catch { Write-Host "Error: $_" } + + $BarId = $Env:BARBuildId + $Channels = $Env:PromoteToMaestroChannels -split "," + $Channels = $Channels -join "][" + $Channels = "[$Channels]" + + $IsStableBuild = $buildInfo.stable + $AzureDevOpsProject = $buildInfo.azureDevOpsProject + $AzureDevOpsBuildDefinitionId = $buildInfo.azureDevOpsBuildDefinitionId + $AzureDevOpsBuildId = $buildInfo.azureDevOpsBuildId + } + + Write-Host "##vso[task.setvariable variable=BARBuildId]$BarId" + Write-Host "##vso[task.setvariable variable=TargetChannels]$Channels" + Write-Host "##vso[task.setvariable variable=IsStableBuild]$IsStableBuild" + + Write-Host "##vso[task.setvariable variable=AzDOProjectName]$AzureDevOpsProject" + Write-Host "##vso[task.setvariable variable=AzDOPipelineId]$AzureDevOpsBuildDefinitionId" + Write-Host "##vso[task.setvariable variable=AzDOBuildId]$AzureDevOpsBuildId" + } + catch { + Write-Host $_ + Write-Host $_.Exception + Write-Host $_.ScriptStackTrace + exit 1 + } + env: + MAESTRO_API_TOKEN: $(MaestroApiAccessToken) + BARBuildId: ${{ parameters.BARBuildId }} + PromoteToMaestroChannels: ${{ parameters.PromoteToChannelIds }} diff --git a/eng/common/templates-official/post-build/trigger-subscription.yml b/eng/common/templates-official/post-build/trigger-subscription.yml new file mode 100644 index 00000000000..da669030daf --- /dev/null +++ b/eng/common/templates-official/post-build/trigger-subscription.yml @@ -0,0 +1,13 @@ +parameters: + ChannelId: 0 + +steps: +- task: PowerShell@2 + displayName: Triggering subscriptions + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/trigger-subscriptions.ps1 + arguments: -SourceRepo $(Build.Repository.Uri) + -ChannelId ${{ parameters.ChannelId }} + -MaestroApiAccessToken $(MaestroAccessToken) + -MaestroApiEndPoint $(MaestroApiEndPoint) + -MaestroApiVersion $(MaestroApiVersion) diff --git a/eng/common/templates-official/steps/add-build-to-channel.yml b/eng/common/templates-official/steps/add-build-to-channel.yml new file mode 100644 index 00000000000..f67a210d62f --- /dev/null +++ b/eng/common/templates-official/steps/add-build-to-channel.yml @@ -0,0 +1,13 @@ +parameters: + ChannelId: 0 + +steps: +- task: PowerShell@2 + displayName: Add Build to Channel + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/add-build-to-channel.ps1 + arguments: -BuildId $(BARBuildId) + -ChannelId ${{ parameters.ChannelId }} + -MaestroApiAccessToken $(MaestroApiAccessToken) + -MaestroApiEndPoint $(MaestroApiEndPoint) + -MaestroApiVersion $(MaestroApiVersion) diff --git a/eng/common/templates-official/steps/build-reason.yml b/eng/common/templates-official/steps/build-reason.yml new file mode 100644 index 00000000000..eba58109b52 --- /dev/null +++ b/eng/common/templates-official/steps/build-reason.yml @@ -0,0 +1,12 @@ +# build-reason.yml +# Description: runs steps if build.reason condition is valid. conditions is a string of valid build reasons +# to include steps (',' separated). +parameters: + conditions: '' + steps: [] + +steps: + - ${{ if and( not(startsWith(parameters.conditions, 'not')), contains(parameters.conditions, variables['build.reason'])) }}: + - ${{ parameters.steps }} + - ${{ if and( startsWith(parameters.conditions, 'not'), not(contains(parameters.conditions, variables['build.reason']))) }}: + - ${{ parameters.steps }} diff --git a/eng/common/templates-official/steps/component-governance.yml b/eng/common/templates-official/steps/component-governance.yml new file mode 100644 index 00000000000..0ecec47b0c9 --- /dev/null +++ b/eng/common/templates-official/steps/component-governance.yml @@ -0,0 +1,13 @@ +parameters: + disableComponentGovernance: false + componentGovernanceIgnoreDirectories: '' + +steps: +- ${{ if eq(parameters.disableComponentGovernance, 'true') }}: + - script: "echo ##vso[task.setvariable variable=skipComponentGovernanceDetection]true" + displayName: Set skipComponentGovernanceDetection variable +- ${{ if ne(parameters.disableComponentGovernance, 'true') }}: + - task: ComponentGovernanceComponentDetection@0 + continueOnError: true + inputs: + ignoreDirectories: ${{ parameters.componentGovernanceIgnoreDirectories }} \ No newline at end of file diff --git a/eng/common/templates-official/steps/execute-codeql.yml b/eng/common/templates-official/steps/execute-codeql.yml new file mode 100644 index 00000000000..9b4a5ffa30a --- /dev/null +++ b/eng/common/templates-official/steps/execute-codeql.yml @@ -0,0 +1,32 @@ +parameters: + # Language that should be analyzed. Defaults to csharp + language: csharp + # Build Commands + buildCommands: '' + overrideParameters: '' # Optional: to override values for parameters. + additionalParameters: '' # Optional: parameters that need user specific values eg: '-SourceToolsList @("abc","def") -ArtifactToolsList @("ghi","jkl")' + # Optional: if specified, restore and use this version of Guardian instead of the default. + overrideGuardianVersion: '' + # Optional: if true, publish the '.gdn' folder as a pipeline artifact. This can help with in-depth + # diagnosis of problems with specific tool configurations. + publishGuardianDirectoryToPipeline: false + # The script to run to execute all SDL tools. Use this if you want to use a script to define SDL + # parameters rather than relying on YAML. It may be better to use a local script, because you can + # reproduce results locally without piecing together a command based on the YAML. + executeAllSdlToolsScript: 'eng/common/sdl/execute-all-sdl-tools.ps1' + # There is some sort of bug (has been reported) in Azure DevOps where if this parameter is named + # 'continueOnError', the parameter value is not correctly picked up. + # This can also be remedied by the caller (post-build.yml) if it does not use a nested parameter + # optional: determines whether to continue the build if the step errors; + sdlContinueOnError: false + +steps: +- template: /eng/common/templates-official/steps/execute-sdl.yml + parameters: + overrideGuardianVersion: ${{ parameters.overrideGuardianVersion }} + executeAllSdlToolsScript: ${{ parameters.executeAllSdlToolsScript }} + overrideParameters: ${{ parameters.overrideParameters }} + additionalParameters: '${{ parameters.additionalParameters }} + -CodeQLAdditionalRunConfigParams @("BuildCommands < ${{ parameters.buildCommands }}", "Language < ${{ parameters.language }}")' + publishGuardianDirectoryToPipeline: ${{ parameters.publishGuardianDirectoryToPipeline }} + sdlContinueOnError: ${{ parameters.sdlContinueOnError }} \ No newline at end of file diff --git a/eng/common/templates-official/steps/execute-sdl.yml b/eng/common/templates-official/steps/execute-sdl.yml new file mode 100644 index 00000000000..07426fde05d --- /dev/null +++ b/eng/common/templates-official/steps/execute-sdl.yml @@ -0,0 +1,88 @@ +parameters: + overrideGuardianVersion: '' + executeAllSdlToolsScript: '' + overrideParameters: '' + additionalParameters: '' + publishGuardianDirectoryToPipeline: false + sdlContinueOnError: false + condition: '' + +steps: +- task: NuGetAuthenticate@1 + inputs: + nuGetServiceConnections: GuardianConnect + +- task: NuGetToolInstaller@1 + displayName: 'Install NuGet.exe' + +- ${{ if ne(parameters.overrideGuardianVersion, '') }}: + - pwsh: | + Set-Location -Path $(Build.SourcesDirectory)\eng\common\sdl + . .\sdl.ps1 + $guardianCliLocation = Install-Gdn -Path $(Build.SourcesDirectory)\.artifacts -Version ${{ parameters.overrideGuardianVersion }} + Write-Host "##vso[task.setvariable variable=GuardianCliLocation]$guardianCliLocation" + displayName: Install Guardian (Overridden) + +- ${{ if eq(parameters.overrideGuardianVersion, '') }}: + - pwsh: | + Set-Location -Path $(Build.SourcesDirectory)\eng\common\sdl + . .\sdl.ps1 + $guardianCliLocation = Install-Gdn -Path $(Build.SourcesDirectory)\.artifacts + Write-Host "##vso[task.setvariable variable=GuardianCliLocation]$guardianCliLocation" + displayName: Install Guardian + +- ${{ if ne(parameters.overrideParameters, '') }}: + - powershell: ${{ parameters.executeAllSdlToolsScript }} ${{ parameters.overrideParameters }} + displayName: Execute SDL (Overridden) + continueOnError: ${{ parameters.sdlContinueOnError }} + condition: ${{ parameters.condition }} + +- ${{ if eq(parameters.overrideParameters, '') }}: + - powershell: ${{ parameters.executeAllSdlToolsScript }} + -GuardianCliLocation $(GuardianCliLocation) + -NugetPackageDirectory $(Build.SourcesDirectory)\.packages + -AzureDevOpsAccessToken $(dn-bot-dotnet-build-rw-code-rw) + ${{ parameters.additionalParameters }} + displayName: Execute SDL + continueOnError: ${{ parameters.sdlContinueOnError }} + condition: ${{ parameters.condition }} + +- ${{ if ne(parameters.publishGuardianDirectoryToPipeline, 'false') }}: + # We want to publish the Guardian results and configuration for easy diagnosis. However, the + # '.gdn' dir is a mix of configuration, results, extracted dependencies, and Guardian default + # tooling files. Some of these files are large and aren't useful during an investigation, so + # exclude them by simply deleting them before publishing. (As of writing, there is no documented + # way to selectively exclude a dir from the pipeline artifact publish task.) + - task: DeleteFiles@1 + displayName: Delete Guardian dependencies to avoid uploading + inputs: + SourceFolder: $(Agent.BuildDirectory)/.gdn + Contents: | + c + i + condition: succeededOrFailed() + + - publish: $(Agent.BuildDirectory)/.gdn + artifact: GuardianConfiguration + displayName: Publish GuardianConfiguration + condition: succeededOrFailed() + + # Publish the SARIF files in a container named CodeAnalysisLogs to enable integration + # with the "SARIF SAST Scans Tab" Azure DevOps extension + - task: CopyFiles@2 + displayName: Copy SARIF files + inputs: + flattenFolders: true + sourceFolder: $(Agent.BuildDirectory)/.gdn/rc/ + contents: '**/*.sarif' + targetFolder: $(Build.SourcesDirectory)/CodeAnalysisLogs + condition: succeededOrFailed() + + # Use PublishBuildArtifacts because the SARIF extension only checks this case + # see microsoft/sarif-azuredevops-extension#4 + - task: PublishBuildArtifacts@1 + displayName: Publish SARIF files to CodeAnalysisLogs container + inputs: + pathToPublish: $(Build.SourcesDirectory)/CodeAnalysisLogs + artifactName: CodeAnalysisLogs + condition: succeededOrFailed() \ No newline at end of file diff --git a/eng/common/templates-official/steps/generate-sbom.yml b/eng/common/templates-official/steps/generate-sbom.yml new file mode 100644 index 00000000000..1bf43bf807a --- /dev/null +++ b/eng/common/templates-official/steps/generate-sbom.yml @@ -0,0 +1,48 @@ +# BuildDropPath - The root folder of the drop directory for which the manifest file will be generated. +# PackageName - The name of the package this SBOM represents. +# PackageVersion - The version of the package this SBOM represents. +# ManifestDirPath - The path of the directory where the generated manifest files will be placed +# IgnoreDirectories - Directories to ignore for SBOM generation. This will be passed through to the CG component detector. + +parameters: + PackageVersion: 8.0.0 + BuildDropPath: '$(Build.SourcesDirectory)/artifacts' + PackageName: '.NET' + ManifestDirPath: $(Build.ArtifactStagingDirectory)/sbom + IgnoreDirectories: '' + sbomContinueOnError: true + +steps: +- task: PowerShell@2 + displayName: Prep for SBOM generation in (Non-linux) + condition: or(eq(variables['Agent.Os'], 'Windows_NT'), eq(variables['Agent.Os'], 'Darwin')) + inputs: + filePath: ./eng/common/generate-sbom-prep.ps1 + arguments: ${{parameters.manifestDirPath}} + +# Chmodding is a workaround for https://github.com/dotnet/arcade/issues/8461 +- script: | + chmod +x ./eng/common/generate-sbom-prep.sh + ./eng/common/generate-sbom-prep.sh ${{parameters.manifestDirPath}} + displayName: Prep for SBOM generation in (Linux) + condition: eq(variables['Agent.Os'], 'Linux') + continueOnError: ${{ parameters.sbomContinueOnError }} + +- task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 + displayName: 'Generate SBOM manifest' + continueOnError: ${{ parameters.sbomContinueOnError }} + inputs: + PackageName: ${{ parameters.packageName }} + BuildDropPath: ${{ parameters.buildDropPath }} + PackageVersion: ${{ parameters.packageVersion }} + ManifestDirPath: ${{ parameters.manifestDirPath }} + ${{ if ne(parameters.IgnoreDirectories, '') }}: + AdditionalComponentDetectorArgs: '--IgnoreDirectories ${{ parameters.IgnoreDirectories }}' + +- task: 1ES.PublishPipelineArtifact@1 + displayName: Publish SBOM manifest + continueOnError: ${{parameters.sbomContinueOnError}} + inputs: + targetPath: '${{parameters.manifestDirPath}}' + artifactName: $(ARTIFACT_NAME) + diff --git a/eng/common/templates-official/steps/publish-logs.yml b/eng/common/templates-official/steps/publish-logs.yml new file mode 100644 index 00000000000..04012fed182 --- /dev/null +++ b/eng/common/templates-official/steps/publish-logs.yml @@ -0,0 +1,23 @@ +parameters: + StageLabel: '' + JobLabel: '' + +steps: +- task: Powershell@2 + displayName: Prepare Binlogs to Upload + inputs: + targetType: inline + script: | + New-Item -ItemType Directory $(Build.SourcesDirectory)/PostBuildLogs/${{parameters.StageLabel}}/${{parameters.JobLabel}}/ + Move-Item -Path $(Build.SourcesDirectory)/artifacts/log/Debug/* $(Build.SourcesDirectory)/PostBuildLogs/${{parameters.StageLabel}}/${{parameters.JobLabel}}/ + continueOnError: true + condition: always() + +- task: 1ES.PublishBuildArtifacts@1 + displayName: Publish Logs + inputs: + PathtoPublish: '$(Build.SourcesDirectory)/PostBuildLogs' + PublishLocation: Container + ArtifactName: PostBuildLogs + continueOnError: true + condition: always() diff --git a/eng/common/templates-official/steps/retain-build.yml b/eng/common/templates-official/steps/retain-build.yml new file mode 100644 index 00000000000..83d97a26a01 --- /dev/null +++ b/eng/common/templates-official/steps/retain-build.yml @@ -0,0 +1,28 @@ +parameters: + # Optional azure devops PAT with build execute permissions for the build's organization, + # only needed if the build that should be retained ran on a different organization than + # the pipeline where this template is executing from + Token: '' + # Optional BuildId to retain, defaults to the current running build + BuildId: '' + # Azure devops Organization URI for the build in the https://dev.azure.com/ format. + # Defaults to the organization the current pipeline is running on + AzdoOrgUri: '$(System.CollectionUri)' + # Azure devops project for the build. Defaults to the project the current pipeline is running on + AzdoProject: '$(System.TeamProject)' + +steps: + - task: powershell@2 + inputs: + targetType: 'filePath' + filePath: eng/common/retain-build.ps1 + pwsh: true + arguments: > + -AzdoOrgUri: ${{parameters.AzdoOrgUri}} + -AzdoProject ${{parameters.AzdoProject}} + -Token ${{coalesce(parameters.Token, '$env:SYSTEM_ACCESSTOKEN') }} + -BuildId ${{coalesce(parameters.BuildId, '$env:BUILD_ID')}} + displayName: Enable permanent build retention + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + BUILD_ID: $(Build.BuildId) \ No newline at end of file diff --git a/eng/common/templates-official/steps/send-to-helix.yml b/eng/common/templates-official/steps/send-to-helix.yml new file mode 100644 index 00000000000..3eb7e2d5f84 --- /dev/null +++ b/eng/common/templates-official/steps/send-to-helix.yml @@ -0,0 +1,91 @@ +# Please remember to update the documentation if you make changes to these parameters! +parameters: + HelixSource: 'pr/default' # required -- sources must start with pr/, official/, prodcon/, or agent/ + HelixType: 'tests/default/' # required -- Helix telemetry which identifies what type of data this is; should include "test" for clarity and must end in '/' + HelixBuild: $(Build.BuildNumber) # required -- the build number Helix will use to identify this -- automatically set to the AzDO build number + HelixTargetQueues: '' # required -- semicolon-delimited list of Helix queues to test on; see https://helix.dot.net/ for a list of queues + HelixAccessToken: '' # required -- access token to make Helix API requests; should be provided by the appropriate variable group + HelixConfiguration: '' # optional -- additional property attached to a job + HelixPreCommands: '' # optional -- commands to run before Helix work item execution + HelixPostCommands: '' # optional -- commands to run after Helix work item execution + WorkItemDirectory: '' # optional -- a payload directory to zip up and send to Helix; requires WorkItemCommand; incompatible with XUnitProjects + WorkItemCommand: '' # optional -- a command to execute on the payload; requires WorkItemDirectory; incompatible with XUnitProjects + WorkItemTimeout: '' # optional -- a timeout in TimeSpan.Parse-ready value (e.g. 00:02:00) for the work item command; requires WorkItemDirectory; incompatible with XUnitProjects + CorrelationPayloadDirectory: '' # optional -- a directory to zip up and send to Helix as a correlation payload + XUnitProjects: '' # optional -- semicolon-delimited list of XUnitProjects to parse and send to Helix; requires XUnitRuntimeTargetFramework, XUnitPublishTargetFramework, XUnitRunnerVersion, and IncludeDotNetCli=true + XUnitWorkItemTimeout: '' # optional -- the workitem timeout in seconds for all workitems created from the xUnit projects specified by XUnitProjects + XUnitPublishTargetFramework: '' # optional -- framework to use to publish your xUnit projects + XUnitRuntimeTargetFramework: '' # optional -- framework to use for the xUnit console runner + XUnitRunnerVersion: '' # optional -- version of the xUnit nuget package you wish to use on Helix; required for XUnitProjects + IncludeDotNetCli: false # optional -- true will download a version of the .NET CLI onto the Helix machine as a correlation payload; requires DotNetCliPackageType and DotNetCliVersion + DotNetCliPackageType: '' # optional -- either 'sdk', 'runtime' or 'aspnetcore-runtime'; determines whether the sdk or runtime will be sent to Helix; see https://raw.githubusercontent.com/dotnet/core/main/release-notes/releases-index.json + DotNetCliVersion: '' # optional -- version of the CLI to send to Helix; based on this: https://raw.githubusercontent.com/dotnet/core/main/release-notes/releases-index.json + WaitForWorkItemCompletion: true # optional -- true will make the task wait until work items have been completed and fail the build if work items fail. False is "fire and forget." + IsExternal: false # [DEPRECATED] -- doesn't do anything, jobs are external if HelixAccessToken is empty and Creator is set + HelixBaseUri: 'https://helix.dot.net/' # optional -- sets the Helix API base URI (allows targeting https://helix.int-dot.net ) + Creator: '' # optional -- if the build is external, use this to specify who is sending the job + DisplayNamePrefix: 'Run Tests' # optional -- rename the beginning of the displayName of the steps in AzDO + condition: succeeded() # optional -- condition for step to execute; defaults to succeeded() + continueOnError: false # optional -- determines whether to continue the build if the step errors; defaults to false + +steps: + - powershell: 'powershell "$env:BUILD_SOURCESDIRECTORY\eng\common\msbuild.ps1 $env:BUILD_SOURCESDIRECTORY\eng\common\helixpublish.proj /restore /p:TreatWarningsAsErrors=false /t:Test /bl:$env:BUILD_SOURCESDIRECTORY\artifacts\log\$env:BuildConfig\SendToHelix.binlog"' + displayName: ${{ parameters.DisplayNamePrefix }} (Windows) + env: + BuildConfig: $(_BuildConfig) + HelixSource: ${{ parameters.HelixSource }} + HelixType: ${{ parameters.HelixType }} + HelixBuild: ${{ parameters.HelixBuild }} + HelixConfiguration: ${{ parameters.HelixConfiguration }} + HelixTargetQueues: ${{ parameters.HelixTargetQueues }} + HelixAccessToken: ${{ parameters.HelixAccessToken }} + HelixPreCommands: ${{ parameters.HelixPreCommands }} + HelixPostCommands: ${{ parameters.HelixPostCommands }} + WorkItemDirectory: ${{ parameters.WorkItemDirectory }} + WorkItemCommand: ${{ parameters.WorkItemCommand }} + WorkItemTimeout: ${{ parameters.WorkItemTimeout }} + CorrelationPayloadDirectory: ${{ parameters.CorrelationPayloadDirectory }} + XUnitProjects: ${{ parameters.XUnitProjects }} + XUnitWorkItemTimeout: ${{ parameters.XUnitWorkItemTimeout }} + XUnitPublishTargetFramework: ${{ parameters.XUnitPublishTargetFramework }} + XUnitRuntimeTargetFramework: ${{ parameters.XUnitRuntimeTargetFramework }} + XUnitRunnerVersion: ${{ parameters.XUnitRunnerVersion }} + IncludeDotNetCli: ${{ parameters.IncludeDotNetCli }} + DotNetCliPackageType: ${{ parameters.DotNetCliPackageType }} + DotNetCliVersion: ${{ parameters.DotNetCliVersion }} + WaitForWorkItemCompletion: ${{ parameters.WaitForWorkItemCompletion }} + HelixBaseUri: ${{ parameters.HelixBaseUri }} + Creator: ${{ parameters.Creator }} + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + condition: and(${{ parameters.condition }}, eq(variables['Agent.Os'], 'Windows_NT')) + continueOnError: ${{ parameters.continueOnError }} + - script: $BUILD_SOURCESDIRECTORY/eng/common/msbuild.sh $BUILD_SOURCESDIRECTORY/eng/common/helixpublish.proj /restore /p:TreatWarningsAsErrors=false /t:Test /bl:$BUILD_SOURCESDIRECTORY/artifacts/log/$BuildConfig/SendToHelix.binlog + displayName: ${{ parameters.DisplayNamePrefix }} (Unix) + env: + BuildConfig: $(_BuildConfig) + HelixSource: ${{ parameters.HelixSource }} + HelixType: ${{ parameters.HelixType }} + HelixBuild: ${{ parameters.HelixBuild }} + HelixConfiguration: ${{ parameters.HelixConfiguration }} + HelixTargetQueues: ${{ parameters.HelixTargetQueues }} + HelixAccessToken: ${{ parameters.HelixAccessToken }} + HelixPreCommands: ${{ parameters.HelixPreCommands }} + HelixPostCommands: ${{ parameters.HelixPostCommands }} + WorkItemDirectory: ${{ parameters.WorkItemDirectory }} + WorkItemCommand: ${{ parameters.WorkItemCommand }} + WorkItemTimeout: ${{ parameters.WorkItemTimeout }} + CorrelationPayloadDirectory: ${{ parameters.CorrelationPayloadDirectory }} + XUnitProjects: ${{ parameters.XUnitProjects }} + XUnitWorkItemTimeout: ${{ parameters.XUnitWorkItemTimeout }} + XUnitPublishTargetFramework: ${{ parameters.XUnitPublishTargetFramework }} + XUnitRuntimeTargetFramework: ${{ parameters.XUnitRuntimeTargetFramework }} + XUnitRunnerVersion: ${{ parameters.XUnitRunnerVersion }} + IncludeDotNetCli: ${{ parameters.IncludeDotNetCli }} + DotNetCliPackageType: ${{ parameters.DotNetCliPackageType }} + DotNetCliVersion: ${{ parameters.DotNetCliVersion }} + WaitForWorkItemCompletion: ${{ parameters.WaitForWorkItemCompletion }} + HelixBaseUri: ${{ parameters.HelixBaseUri }} + Creator: ${{ parameters.Creator }} + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + condition: and(${{ parameters.condition }}, ne(variables['Agent.Os'], 'Windows_NT')) + continueOnError: ${{ parameters.continueOnError }} diff --git a/eng/common/templates-official/steps/source-build.yml b/eng/common/templates-official/steps/source-build.yml new file mode 100644 index 00000000000..829f17c34d1 --- /dev/null +++ b/eng/common/templates-official/steps/source-build.yml @@ -0,0 +1,129 @@ +parameters: + # This template adds arcade-powered source-build to CI. + + # This is a 'steps' template, and is intended for advanced scenarios where the existing build + # infra has a careful build methodology that must be followed. For example, a repo + # (dotnet/runtime) might choose to clone the GitHub repo only once and store it as a pipeline + # artifact for all subsequent jobs to use, to reduce dependence on a strong network connection to + # GitHub. Using this steps template leaves room for that infra to be included. + + # Defines the platform on which to run the steps. See 'eng/common/templates-official/job/source-build.yml' + # for details. The entire object is described in the 'job' template for simplicity, even though + # the usage of the properties on this object is split between the 'job' and 'steps' templates. + platform: {} + +steps: +# Build. Keep it self-contained for simple reusability. (No source-build-specific job variables.) +- script: | + set -x + df -h + + # If building on the internal project, the artifact feeds variable may be available (usually only if needed) + # In that case, call the feed setup script to add internal feeds corresponding to public ones. + # In addition, add an msbuild argument to copy the WIP from the repo to the target build location. + # This is because SetupNuGetSources.sh will alter the current NuGet.config file, and we need to preserve those + # changes. + internalRestoreArgs= + if [ '$(dn-bot-dnceng-artifact-feeds-rw)' != '$''(dn-bot-dnceng-artifact-feeds-rw)' ]; then + # Temporarily work around https://github.com/dotnet/arcade/issues/7709 + chmod +x $(Build.SourcesDirectory)/eng/common/SetupNugetSources.sh + $(Build.SourcesDirectory)/eng/common/SetupNugetSources.sh $(Build.SourcesDirectory)/NuGet.config $(dn-bot-dnceng-artifact-feeds-rw) + internalRestoreArgs='/p:CopyWipIntoInnerSourceBuildRepo=true' + + # The 'Copy WIP' feature of source build uses git stash to apply changes from the original repo. + # This only works if there is a username/email configured, which won't be the case in most CI runs. + git config --get user.email + if [ $? -ne 0 ]; then + git config user.email dn-bot@microsoft.com + git config user.name dn-bot + fi + fi + + # If building on the internal project, the internal storage variable may be available (usually only if needed) + # In that case, add variables to allow the download of internal runtimes if the specified versions are not found + # in the default public locations. + internalRuntimeDownloadArgs= + if [ '$(dotnetbuilds-internal-container-read-token-base64)' != '$''(dotnetbuilds-internal-container-read-token-base64)' ]; then + internalRuntimeDownloadArgs='/p:DotNetRuntimeSourceFeed=https://dotnetbuilds.blob.core.windows.net/internal /p:DotNetRuntimeSourceFeedKey=$(dotnetbuilds-internal-container-read-token-base64) --runtimesourcefeed https://dotnetbuilds.blob.core.windows.net/internal --runtimesourcefeedkey $(dotnetbuilds-internal-container-read-token-base64)' + fi + + buildConfig=Release + # Check if AzDO substitutes in a build config from a variable, and use it if so. + if [ '$(_BuildConfig)' != '$''(_BuildConfig)' ]; then + buildConfig='$(_BuildConfig)' + fi + + officialBuildArgs= + if [ '${{ and(ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}' = 'True' ]; then + officialBuildArgs='/p:DotNetPublishUsingPipelines=true /p:OfficialBuildId=$(BUILD.BUILDNUMBER)' + fi + + targetRidArgs= + if [ '${{ parameters.platform.targetRID }}' != '' ]; then + targetRidArgs='/p:TargetRid=${{ parameters.platform.targetRID }}' + fi + + runtimeOsArgs= + if [ '${{ parameters.platform.runtimeOS }}' != '' ]; then + runtimeOsArgs='/p:RuntimeOS=${{ parameters.platform.runtimeOS }}' + fi + + baseOsArgs= + if [ '${{ parameters.platform.baseOS }}' != '' ]; then + baseOsArgs='/p:BaseOS=${{ parameters.platform.baseOS }}' + fi + + publishArgs= + if [ '${{ parameters.platform.skipPublishValidation }}' != 'true' ]; then + publishArgs='--publish' + fi + + assetManifestFileName=SourceBuild_RidSpecific.xml + if [ '${{ parameters.platform.name }}' != '' ]; then + assetManifestFileName=SourceBuild_${{ parameters.platform.name }}.xml + fi + + ${{ coalesce(parameters.platform.buildScript, './build.sh') }} --ci \ + --configuration $buildConfig \ + --restore --build --pack $publishArgs -bl \ + $officialBuildArgs \ + $internalRuntimeDownloadArgs \ + $internalRestoreArgs \ + $targetRidArgs \ + $runtimeOsArgs \ + $baseOsArgs \ + /p:SourceBuildNonPortable=${{ parameters.platform.nonPortable }} \ + /p:ArcadeBuildFromSource=true \ + /p:AssetManifestFileName=$assetManifestFileName + displayName: Build + +# Upload build logs for diagnosis. +- task: CopyFiles@2 + displayName: Prepare BuildLogs staging directory + inputs: + SourceFolder: '$(Build.SourcesDirectory)' + Contents: | + **/*.log + **/*.binlog + artifacts/source-build/self/prebuilt-report/** + TargetFolder: '$(Build.StagingDirectory)/BuildLogs' + CleanTargetFolder: true + continueOnError: true + condition: succeededOrFailed() + +- task: 1ES.PublishPipelineArtifact@1 + displayName: Publish BuildLogs + inputs: + targetPath: '$(Build.StagingDirectory)/BuildLogs' + artifactName: BuildLogs_SourceBuild_${{ parameters.platform.name }}_Attempt$(System.JobAttempt) + continueOnError: true + condition: succeededOrFailed() + +# Manually inject component detection so that we can ignore the source build upstream cache, which contains +# a nupkg cache of input packages (a local feed). +# This path must match the upstream cache path in property 'CurrentRepoSourceBuiltNupkgCacheDir' +# in src\Microsoft.DotNet.Arcade.Sdk\tools\SourceBuild\SourceBuildArcade.targets +- task: ComponentGovernanceComponentDetection@0 + displayName: Component Detection (Exclude upstream cache) + inputs: + ignoreDirectories: '$(Build.SourcesDirectory)/artifacts/source-build/self/src/artifacts/obj/source-built-upstream-cache' diff --git a/eng/common/templates-official/variables/pool-providers.yml b/eng/common/templates-official/variables/pool-providers.yml new file mode 100644 index 00000000000..beab7d1bfba --- /dev/null +++ b/eng/common/templates-official/variables/pool-providers.yml @@ -0,0 +1,45 @@ +# Select a pool provider based off branch name. Anything with branch name containing 'release' must go into an -Svc pool, +# otherwise it should go into the "normal" pools. This separates out the queueing and billing of released branches. + +# Motivation: +# Once a given branch of a repository's output has been officially "shipped" once, it is then considered to be COGS +# (Cost of goods sold) and should be moved to a servicing pool provider. This allows both separation of queueing +# (allowing release builds and main PR builds to not intefere with each other) and billing (required for COGS. +# Additionally, the pool provider name itself may be subject to change when the .NET Core Engineering Services +# team needs to move resources around and create new and potentially differently-named pools. Using this template +# file from an Arcade-ified repo helps guard against both having to update one's release/* branches and renaming. + +# How to use: +# This yaml assumes your shipped product branches use the naming convention "release/..." (which many do). +# If we find alternate naming conventions in broad usage it can be added to the condition below. +# +# First, import the template in an arcade-ified repo to pick up the variables, e.g.: +# +# variables: +# - template: /eng/common/templates-official/variables/pool-providers.yml +# +# ... then anywhere specifying the pool provider use the runtime variables, +# $(DncEngInternalBuildPool) +# +# pool: +# name: $(DncEngInternalBuildPool) +# image: 1es-windows-2022-pt + +variables: + # Coalesce the target and source branches so we know when a PR targets a release branch + # If these variables are somehow missing, fall back to main (tends to have more capacity) + + # Any new -Svc alternative pools should have variables added here to allow for splitting work + + - name: DncEngInternalBuildPool + value: $[ + replace( + replace( + eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'), + True, + 'NetCore1ESPool-Svc-Internal' + ), + False, + 'NetCore1ESPool-Internal' + ) + ] \ No newline at end of file diff --git a/eng/common/templates-official/variables/sdl-variables.yml b/eng/common/templates-official/variables/sdl-variables.yml new file mode 100644 index 00000000000..dbdd66d4a4b --- /dev/null +++ b/eng/common/templates-official/variables/sdl-variables.yml @@ -0,0 +1,7 @@ +variables: +# The Guardian version specified in 'eng/common/sdl/packages.config'. This value must be kept in +# sync with the packages.config file. +- name: DefaultGuardianVersion + value: 0.109.0 +- name: GuardianPackagesConfigFile + value: $(Build.SourcesDirectory)\eng\common\sdl\packages.config \ No newline at end of file diff --git a/global.json b/global.json index 047a8efd154..abcaf569a91 100644 --- a/global.json +++ b/global.json @@ -8,9 +8,9 @@ "dotnet": "8.0.200" }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "8.0.0-beta.24123.1", - "Microsoft.DotNet.Helix.Sdk": "8.0.0-beta.24123.1", - "Microsoft.DotNet.SharedFramework.Sdk": "8.0.0-beta.24123.1", + "Microsoft.DotNet.Arcade.Sdk": "8.0.0-beta.24151.4", + "Microsoft.DotNet.Helix.Sdk": "8.0.0-beta.24151.4", + "Microsoft.DotNet.SharedFramework.Sdk": "8.0.0-beta.24151.4", "Microsoft.Build.NoTargets": "3.7.0" } } From 064a54bd3b202ae767fb77c77a0d079a595c5e6e Mon Sep 17 00:00:00 2001 From: dotnet bot Date: Sun, 10 Mar 2024 17:11:43 -0700 Subject: [PATCH 31/50] Localized file check-in by OneLocBuild Task: Build definition ID 1309: Build ID 2399216 (#2660) * Localized file check-in by OneLocBuild Task: Build definition ID 1309: Build ID 2394717 * Localized file check-in by OneLocBuild Task: Build definition ID 1309: Build ID 2394717 * Localized file check-in by OneLocBuild Task: Build definition ID 1309: Build ID 2394717 * Localized file check-in by OneLocBuild Task: Build definition ID 1309: Build ID 2394717 * Localized file check-in by OneLocBuild Task: Build definition ID 1309: Build ID 2395589 * Localized file check-in by OneLocBuild Task: Build definition ID 1309: Build ID 2395589 * Localized file check-in by OneLocBuild Task: Build definition ID 1309: Build ID 2395589 * Localized file check-in by OneLocBuild Task: Build definition ID 1309: Build ID 2395589 * Localized file check-in by OneLocBuild Task: Build definition ID 1309: Build ID 2396226 * Localized file check-in by OneLocBuild Task: Build definition ID 1309: Build ID 2396226 * Localized file check-in by OneLocBuild Task: Build definition ID 1309: Build ID 2396226 * Localized file check-in by OneLocBuild Task: Build definition ID 1309: Build ID 2396226 * Localized file check-in by OneLocBuild Task: Build definition ID 1309: Build ID 2396841 * Localized file check-in by OneLocBuild Task: Build definition ID 1309: Build ID 2396841 * Localized file check-in by OneLocBuild Task: Build definition ID 1309: Build ID 2396841 * Localized file check-in by OneLocBuild Task: Build definition ID 1309: Build ID 2396841 * Localized file check-in by OneLocBuild Task: Build definition ID 1309: Build ID 2397578 * Localized file check-in by OneLocBuild Task: Build definition ID 1309: Build ID 2397578 * Localized file check-in by OneLocBuild Task: Build definition ID 1309: Build ID 2397578 * Localized file check-in by OneLocBuild Task: Build definition ID 1309: Build ID 2397578 * Localized file check-in by OneLocBuild Task: Build definition ID 1309: Build ID 2398030 * Localized file check-in by OneLocBuild Task: Build definition ID 1309: Build ID 2398030 --- .../Controls/xlf/ControlsStrings.cs.xlf | 2 +- .../Controls/xlf/ControlsStrings.de.xlf | 2 +- .../Controls/xlf/ControlsStrings.es.xlf | 2 +- .../Controls/xlf/ControlsStrings.fr.xlf | 2 +- .../Controls/xlf/ControlsStrings.it.xlf | 2 +- .../Controls/xlf/ControlsStrings.ja.xlf | 2 +- .../Controls/xlf/ControlsStrings.ko.xlf | 2 +- .../Controls/xlf/ControlsStrings.pl.xlf | 2 +- .../Controls/xlf/ControlsStrings.pt-BR.xlf | 2 +- .../Controls/xlf/ControlsStrings.ru.xlf | 2 +- .../Controls/xlf/ControlsStrings.tr.xlf | 2 +- .../Controls/xlf/ControlsStrings.zh-Hans.xlf | 2 +- .../Controls/xlf/ControlsStrings.zh-Hant.xlf | 2 +- .../Resources/xlf/Columns.cs.xlf | 38 +++---- .../Resources/xlf/Columns.de.xlf | 38 +++---- .../Resources/xlf/Columns.es.xlf | 38 +++---- .../Resources/xlf/Columns.fr.xlf | 38 +++---- .../Resources/xlf/Columns.it.xlf | 38 +++---- .../Resources/xlf/Columns.ja.xlf | 38 +++---- .../Resources/xlf/Columns.ko.xlf | 38 +++---- .../Resources/xlf/Columns.pl.xlf | 38 +++---- .../Resources/xlf/Columns.pt-BR.xlf | 38 +++---- .../Resources/xlf/Columns.ru.xlf | 38 +++---- .../Resources/xlf/Columns.tr.xlf | 38 +++---- .../Resources/xlf/Columns.zh-Hans.xlf | 38 +++---- .../Resources/xlf/Columns.zh-Hant.xlf | 38 +++---- .../Resources/xlf/ConsoleLogs.cs.xlf | 22 ++-- .../Resources/xlf/ConsoleLogs.de.xlf | 22 ++-- .../Resources/xlf/ConsoleLogs.es.xlf | 22 ++-- .../Resources/xlf/ConsoleLogs.fr.xlf | 22 ++-- .../Resources/xlf/ConsoleLogs.it.xlf | 22 ++-- .../Resources/xlf/ConsoleLogs.ja.xlf | 22 ++-- .../Resources/xlf/ConsoleLogs.ko.xlf | 22 ++-- .../Resources/xlf/ConsoleLogs.pl.xlf | 22 ++-- .../Resources/xlf/ConsoleLogs.pt-BR.xlf | 22 ++-- .../Resources/xlf/ConsoleLogs.ru.xlf | 22 ++-- .../Resources/xlf/ConsoleLogs.tr.xlf | 22 ++-- .../Resources/xlf/ConsoleLogs.zh-Hans.xlf | 22 ++-- .../Resources/xlf/ConsoleLogs.zh-Hant.xlf | 22 ++-- .../Resources/xlf/ControlsStrings.cs.xlf | 104 +++++++++--------- .../Resources/xlf/ControlsStrings.de.xlf | 104 +++++++++--------- .../Resources/xlf/ControlsStrings.es.xlf | 104 +++++++++--------- .../Resources/xlf/ControlsStrings.fr.xlf | 104 +++++++++--------- .../Resources/xlf/ControlsStrings.it.xlf | 104 +++++++++--------- .../Resources/xlf/ControlsStrings.ja.xlf | 104 +++++++++--------- .../Resources/xlf/ControlsStrings.ko.xlf | 104 +++++++++--------- .../Resources/xlf/ControlsStrings.pl.xlf | 104 +++++++++--------- .../Resources/xlf/ControlsStrings.pt-BR.xlf | 104 +++++++++--------- .../Resources/xlf/ControlsStrings.ru.xlf | 104 +++++++++--------- .../Resources/xlf/ControlsStrings.tr.xlf | 104 +++++++++--------- .../Resources/xlf/ControlsStrings.zh-Hans.xlf | 104 +++++++++--------- .../Resources/xlf/ControlsStrings.zh-Hant.xlf | 104 +++++++++--------- .../Resources/xlf/Dialogs.cs.xlf | 56 +++++----- .../Resources/xlf/Dialogs.de.xlf | 56 +++++----- .../Resources/xlf/Dialogs.es.xlf | 56 +++++----- .../Resources/xlf/Dialogs.fr.xlf | 56 +++++----- .../Resources/xlf/Dialogs.it.xlf | 56 +++++----- .../Resources/xlf/Dialogs.ja.xlf | 56 +++++----- .../Resources/xlf/Dialogs.ko.xlf | 56 +++++----- .../Resources/xlf/Dialogs.pl.xlf | 56 +++++----- .../Resources/xlf/Dialogs.pt-BR.xlf | 56 +++++----- .../Resources/xlf/Dialogs.ru.xlf | 56 +++++----- .../Resources/xlf/Dialogs.tr.xlf | 56 +++++----- .../Resources/xlf/Dialogs.zh-Hans.xlf | 56 +++++----- .../Resources/xlf/Dialogs.zh-Hant.xlf | 56 +++++----- .../Resources/xlf/Layout.cs.xlf | 28 ++--- .../Resources/xlf/Layout.de.xlf | 28 ++--- .../Resources/xlf/Layout.es.xlf | 28 ++--- .../Resources/xlf/Layout.fr.xlf | 28 ++--- .../Resources/xlf/Layout.it.xlf | 28 ++--- .../Resources/xlf/Layout.ja.xlf | 28 ++--- .../Resources/xlf/Layout.ko.xlf | 28 ++--- .../Resources/xlf/Layout.pl.xlf | 28 ++--- .../Resources/xlf/Layout.pt-BR.xlf | 28 ++--- .../Resources/xlf/Layout.ru.xlf | 28 ++--- .../Resources/xlf/Layout.tr.xlf | 28 ++--- .../Resources/xlf/Layout.zh-Hans.xlf | 28 ++--- .../Resources/xlf/Layout.zh-Hant.xlf | 28 ++--- .../Resources/xlf/Metrics.cs.xlf | 34 +++--- .../Resources/xlf/Metrics.de.xlf | 34 +++--- .../Resources/xlf/Metrics.es.xlf | 34 +++--- .../Resources/xlf/Metrics.fr.xlf | 34 +++--- .../Resources/xlf/Metrics.it.xlf | 34 +++--- .../Resources/xlf/Metrics.ja.xlf | 34 +++--- .../Resources/xlf/Metrics.ko.xlf | 34 +++--- .../Resources/xlf/Metrics.pl.xlf | 34 +++--- .../Resources/xlf/Metrics.pt-BR.xlf | 34 +++--- .../Resources/xlf/Metrics.ru.xlf | 34 +++--- .../Resources/xlf/Metrics.tr.xlf | 34 +++--- .../Resources/xlf/Metrics.zh-Hans.xlf | 34 +++--- .../Resources/xlf/Metrics.zh-Hant.xlf | 34 +++--- .../Resources/xlf/Resources.cs.xlf | 72 ++++++------ .../Resources/xlf/Resources.de.xlf | 72 ++++++------ .../Resources/xlf/Resources.es.xlf | 72 ++++++------ .../Resources/xlf/Resources.fr.xlf | 72 ++++++------ .../Resources/xlf/Resources.it.xlf | 72 ++++++------ .../Resources/xlf/Resources.ja.xlf | 72 ++++++------ .../Resources/xlf/Resources.ko.xlf | 72 ++++++------ .../Resources/xlf/Resources.pl.xlf | 72 ++++++------ .../Resources/xlf/Resources.pt-BR.xlf | 72 ++++++------ .../Resources/xlf/Resources.ru.xlf | 72 ++++++------ .../Resources/xlf/Resources.tr.xlf | 72 ++++++------ .../Resources/xlf/Resources.zh-Hans.xlf | 72 ++++++------ .../Resources/xlf/Resources.zh-Hant.xlf | 72 ++++++------ .../Resources/xlf/Routes.cs.xlf | 4 +- .../Resources/xlf/Routes.de.xlf | 4 +- .../Resources/xlf/Routes.es.xlf | 4 +- .../Resources/xlf/Routes.fr.xlf | 4 +- .../Resources/xlf/Routes.it.xlf | 4 +- .../Resources/xlf/Routes.ja.xlf | 4 +- .../Resources/xlf/Routes.ko.xlf | 4 +- .../Resources/xlf/Routes.pl.xlf | 4 +- .../Resources/xlf/Routes.pt-BR.xlf | 4 +- .../Resources/xlf/Routes.ru.xlf | 4 +- .../Resources/xlf/Routes.tr.xlf | 4 +- .../Resources/xlf/Routes.zh-Hans.xlf | 4 +- .../Resources/xlf/Routes.zh-Hant.xlf | 4 +- .../Resources/xlf/StructuredLogs.cs.xlf | 36 +++--- .../Resources/xlf/StructuredLogs.de.xlf | 36 +++--- .../Resources/xlf/StructuredLogs.es.xlf | 36 +++--- .../Resources/xlf/StructuredLogs.fr.xlf | 36 +++--- .../Resources/xlf/StructuredLogs.it.xlf | 36 +++--- .../Resources/xlf/StructuredLogs.ja.xlf | 36 +++--- .../Resources/xlf/StructuredLogs.ko.xlf | 36 +++--- .../Resources/xlf/StructuredLogs.pl.xlf | 36 +++--- .../Resources/xlf/StructuredLogs.pt-BR.xlf | 36 +++--- .../Resources/xlf/StructuredLogs.ru.xlf | 36 +++--- .../Resources/xlf/StructuredLogs.tr.xlf | 36 +++--- .../Resources/xlf/StructuredLogs.zh-Hans.xlf | 36 +++--- .../Resources/xlf/StructuredLogs.zh-Hant.xlf | 36 +++--- .../Resources/xlf/TraceDetail.cs.xlf | 14 +-- .../Resources/xlf/TraceDetail.de.xlf | 14 +-- .../Resources/xlf/TraceDetail.es.xlf | 14 +-- .../Resources/xlf/TraceDetail.fr.xlf | 14 +-- .../Resources/xlf/TraceDetail.it.xlf | 14 +-- .../Resources/xlf/TraceDetail.ja.xlf | 14 +-- .../Resources/xlf/TraceDetail.ko.xlf | 14 +-- .../Resources/xlf/TraceDetail.pl.xlf | 14 +-- .../Resources/xlf/TraceDetail.pt-BR.xlf | 14 +-- .../Resources/xlf/TraceDetail.ru.xlf | 14 +-- .../Resources/xlf/TraceDetail.tr.xlf | 14 +-- .../Resources/xlf/TraceDetail.zh-Hans.xlf | 14 +-- .../Resources/xlf/TraceDetail.zh-Hant.xlf | 14 +-- .../Resources/xlf/Traces.cs.xlf | 20 ++-- .../Resources/xlf/Traces.de.xlf | 20 ++-- .../Resources/xlf/Traces.es.xlf | 20 ++-- .../Resources/xlf/Traces.fr.xlf | 20 ++-- .../Resources/xlf/Traces.it.xlf | 20 ++-- .../Resources/xlf/Traces.ja.xlf | 20 ++-- .../Resources/xlf/Traces.ko.xlf | 20 ++-- .../Resources/xlf/Traces.pl.xlf | 20 ++-- .../Resources/xlf/Traces.pt-BR.xlf | 20 ++-- .../Resources/xlf/Traces.ru.xlf | 20 ++-- .../Resources/xlf/Traces.tr.xlf | 20 ++-- .../Resources/xlf/Traces.zh-Hans.xlf | 20 ++-- .../Resources/xlf/Traces.zh-Hant.xlf | 20 ++-- .../Properties/xlf/Resources.cs.xlf | 22 ++-- .../Properties/xlf/Resources.de.xlf | 22 ++-- .../Properties/xlf/Resources.es.xlf | 22 ++-- .../Properties/xlf/Resources.fr.xlf | 22 ++-- .../Properties/xlf/Resources.it.xlf | 22 ++-- .../Properties/xlf/Resources.ja.xlf | 22 ++-- .../Properties/xlf/Resources.ko.xlf | 22 ++-- .../Properties/xlf/Resources.pl.xlf | 22 ++-- .../Properties/xlf/Resources.pt-BR.xlf | 22 ++-- .../Properties/xlf/Resources.ru.xlf | 22 ++-- .../Properties/xlf/Resources.tr.xlf | 22 ++-- .../Properties/xlf/Resources.zh-Hans.xlf | 22 ++-- .../Properties/xlf/Resources.zh-Hant.xlf | 22 ++-- 169 files changed, 2938 insertions(+), 2938 deletions(-) diff --git a/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.cs.xlf b/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.cs.xlf index e8218b25935..e2be6120c0b 100644 --- a/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.cs.xlf +++ b/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.cs.xlf @@ -4,7 +4,7 @@ Unable to display chart. - Unable to display chart. + Graf nelze zobrazit. diff --git a/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.de.xlf b/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.de.xlf index e9c3065cd7e..848d2b1a684 100644 --- a/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.de.xlf +++ b/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.de.xlf @@ -4,7 +4,7 @@ Unable to display chart. - Unable to display chart. + Diagramm kann nicht angezeigt werden. diff --git a/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.es.xlf b/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.es.xlf index 76a8de88640..b33c91a99a5 100644 --- a/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.es.xlf +++ b/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.es.xlf @@ -4,7 +4,7 @@ Unable to display chart. - Unable to display chart. + No se puede mostrar el gráfico. diff --git a/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.fr.xlf b/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.fr.xlf index 545f6236e8b..b70bfad36bd 100644 --- a/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.fr.xlf +++ b/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.fr.xlf @@ -4,7 +4,7 @@ Unable to display chart. - Unable to display chart. + Désolé... Nous ne pouvons pas afficher le graphique. diff --git a/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.it.xlf b/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.it.xlf index e4b552e0b6e..d023d9e03ab 100644 --- a/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.it.xlf +++ b/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.it.xlf @@ -4,7 +4,7 @@ Unable to display chart. - Unable to display chart. + Non è possibile visualizzare il grafico. diff --git a/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.ja.xlf b/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.ja.xlf index 13a25eab623..c65ab2c7089 100644 --- a/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.ja.xlf +++ b/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.ja.xlf @@ -4,7 +4,7 @@ Unable to display chart. - Unable to display chart. + グラフを表示できません。 diff --git a/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.ko.xlf b/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.ko.xlf index ce8a5f3cc81..a584c0d8af3 100644 --- a/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.ko.xlf +++ b/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.ko.xlf @@ -4,7 +4,7 @@ Unable to display chart. - Unable to display chart. + 차트를 표시할 수 없습니다. diff --git a/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.pl.xlf b/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.pl.xlf index 9baa2355683..47a04223058 100644 --- a/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.pl.xlf +++ b/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.pl.xlf @@ -4,7 +4,7 @@ Unable to display chart. - Unable to display chart. + Nie można wyświetlić wykresu. diff --git a/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.pt-BR.xlf b/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.pt-BR.xlf index 51c23bdf3c3..90b48dfb66c 100644 --- a/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.pt-BR.xlf +++ b/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.pt-BR.xlf @@ -4,7 +4,7 @@ Unable to display chart. - Unable to display chart. + Não é possível exibir o gráfico. diff --git a/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.ru.xlf b/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.ru.xlf index edef25518da..e051650a319 100644 --- a/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.ru.xlf +++ b/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.ru.xlf @@ -4,7 +4,7 @@ Unable to display chart. - Unable to display chart. + Не удалось показать график. diff --git a/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.tr.xlf b/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.tr.xlf index 8c753c13b8f..e378c83d89e 100644 --- a/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.tr.xlf +++ b/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.tr.xlf @@ -4,7 +4,7 @@ Unable to display chart. - Unable to display chart. + Liste görüntülenemiyor. diff --git a/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.zh-Hans.xlf b/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.zh-Hans.xlf index b9891580584..f6eb18e474a 100644 --- a/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.zh-Hans.xlf +++ b/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.zh-Hans.xlf @@ -4,7 +4,7 @@ Unable to display chart. - Unable to display chart. + 无法显示图表。 diff --git a/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.zh-Hant.xlf b/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.zh-Hant.xlf index f2dbb888c11..7760da9a97f 100644 --- a/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.zh-Hant.xlf +++ b/src/Aspire.Dashboard/Components/Controls/xlf/ControlsStrings.zh-Hant.xlf @@ -4,7 +4,7 @@ Unable to display chart. - Unable to display chart. + 無法顯示圖表。 diff --git a/src/Aspire.Dashboard/Resources/xlf/Columns.cs.xlf b/src/Aspire.Dashboard/Resources/xlf/Columns.cs.xlf index 88163e590ae..82ca8d3e7e3 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Columns.cs.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Columns.cs.xlf @@ -4,97 +4,97 @@ None - None + Žádné Starting... - Starting... + Spouštění… Exception details - Exception details + Podrobnosti o výjimce Container ID: {0} - Container ID: {0} + ID kontejneru: {0} {0} is an alphanumeric id Copy container ID to clipboard - Copy container ID to clipboard + Zkopírovat ID kontejneru do schránky Process ID: {0} - Process ID: {0} + ID procesu: {0} {0} is a numeric id Container args - Container args + Argumenty kontejneru Command: {0} - Command: {0} + Příkaz: {0} Container command - Container command + Příkaz kontejneru Copy full command to clipboard - Copy full command to clipboard + Zkopírovat celý příkaz do schránky Port: {0} - Port: {0} + Port: {0} {0} is a port number Ports: {0} - Ports: {0} + Porty: {0} {0} is a list of ports Working directory: {0} - Working directory: {0} + Pracovní adresář: {0} {0} is a path Copy image name and tag to clipboard - Copy image name and tag to clipboard + Zkopírovat název a značku image do schránky Copy file path to clipboard - Copy file path to clipboard + Zkopírovat cestu k souboru do schránky {0} is no longer running - {0} is no longer running + {0} už neběží. {0} is a resource type {0} exited unexpectedly with exit code {1} - {0} exited unexpectedly with exit code {1} + Prostředek {0} byl neočekávaně ukončen s ukončovacím kódem {1}. {0} is a resource type, {1} is a number {0} error logs - {0} error logs + Počet protokolů chyb: {0} {0} is a number 1 error log - 1 error log + 1 protokol chyb diff --git a/src/Aspire.Dashboard/Resources/xlf/Columns.de.xlf b/src/Aspire.Dashboard/Resources/xlf/Columns.de.xlf index d0f06a0a467..deb1d697839 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Columns.de.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Columns.de.xlf @@ -4,97 +4,97 @@ None - None + Keine Starting... - Starting... + Wird gestartet... Exception details - Exception details + Ausnahmedetails Container ID: {0} - Container ID: {0} + Container-ID: {0} {0} is an alphanumeric id Copy container ID to clipboard - Copy container ID to clipboard + Container-ID in Zwischenablage kopieren Process ID: {0} - Process ID: {0} + Prozess-ID: {0} {0} is a numeric id Container args - Container args + Containerargumente Command: {0} - Command: {0} + Befehl: {0} Container command - Container command + Containerbefehl Copy full command to clipboard - Copy full command to clipboard + Vollständigen Befehl in Zwischenablage kopieren Port: {0} - Port: {0} + Port: {0} {0} is a port number Ports: {0} - Ports: {0} + Ports: {0} {0} is a list of ports Working directory: {0} - Working directory: {0} + Arbeitsverzeichnis: {0} {0} is a path Copy image name and tag to clipboard - Copy image name and tag to clipboard + Bildnamen und -tag in Zwischenablage kopieren Copy file path to clipboard - Copy file path to clipboard + Vollständigen Dateipfad in Zwischenablage kopieren {0} is no longer running - {0} is no longer running + {0} wird nicht mehr ausgeführt. {0} is a resource type {0} exited unexpectedly with exit code {1} - {0} exited unexpectedly with exit code {1} + {0} wurde unerwartet mit dem Exitcode {1} beendet. {0} is a resource type, {1} is a number {0} error logs - {0} error logs + {0} Fehlerprotokolle {0} is a number 1 error log - 1 error log + 1 Fehlerprotokoll diff --git a/src/Aspire.Dashboard/Resources/xlf/Columns.es.xlf b/src/Aspire.Dashboard/Resources/xlf/Columns.es.xlf index 92ef1a036cc..fecf4d12b59 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Columns.es.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Columns.es.xlf @@ -4,97 +4,97 @@ None - None + Ninguno Starting... - Starting... + Iniciando... Exception details - Exception details + Detalles de la excepción Container ID: {0} - Container ID: {0} + Id. de contenedor: {0} {0} is an alphanumeric id Copy container ID to clipboard - Copy container ID to clipboard + Copiar id. de contenedor en el Portapapeles Process ID: {0} - Process ID: {0} + Id. de proceso: {0} {0} is a numeric id Container args - Container args + Argumentos de contenedor Command: {0} - Command: {0} + Comando: {0} Container command - Container command + Comando de contenedor Copy full command to clipboard - Copy full command to clipboard + Copiar el comando completo en el Portapapeles Port: {0} - Port: {0} + Puerto: {0} {0} is a port number Ports: {0} - Ports: {0} + Puertos: {0} {0} is a list of ports Working directory: {0} - Working directory: {0} + Directorio de trabajo: {0} {0} is a path Copy image name and tag to clipboard - Copy image name and tag to clipboard + Copiar el nombre y la etiqueta de la imagen en el Portapapeles Copy file path to clipboard - Copy file path to clipboard + Copiar la ruta de acceso del archivo al Portapapeles {0} is no longer running - {0} is no longer running + {0} ya no se está ejecutando {0} is a resource type {0} exited unexpectedly with exit code {1} - {0} exited unexpectedly with exit code {1} + {0} se cerró inesperadamente con el código de salida {1} {0} is a resource type, {1} is a number {0} error logs - {0} error logs + {0} registros de errores {0} is a number 1 error log - 1 error log + 1 registro de errores diff --git a/src/Aspire.Dashboard/Resources/xlf/Columns.fr.xlf b/src/Aspire.Dashboard/Resources/xlf/Columns.fr.xlf index bd8b851d76d..a7a2cc3b65b 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Columns.fr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Columns.fr.xlf @@ -4,97 +4,97 @@ None - None + Aucune Starting... - Starting... + Démarrage en cours... Merci de patienter. Exception details - Exception details + Détails de l’exception Container ID: {0} - Container ID: {0} + ID de conteneur : {0} {0} is an alphanumeric id Copy container ID to clipboard - Copy container ID to clipboard + Copier un ID de conteneur dans le Presse-papiers Process ID: {0} - Process ID: {0} + ID de processus : {0} {0} is a numeric id Container args - Container args + Arguments de conteneur Command: {0} - Command: {0} + Commande : {0} Container command - Container command + Commande de conteneur Copy full command to clipboard - Copy full command to clipboard + Copier la commande complète dans le Presse-papiers Port: {0} - Port: {0} + Port : {0} {0} is a port number Ports: {0} - Ports: {0} + Ports : {0} {0} is a list of ports Working directory: {0} - Working directory: {0} + Répertoire de travail : {0} {0} is a path Copy image name and tag to clipboard - Copy image name and tag to clipboard + Copier le nom et la balise de l’image dans le Presse-papiers Copy file path to clipboard - Copy file path to clipboard + Copier le chemin d’accès du fichier dans le Presse-papiers {0} is no longer running - {0} is no longer running + {0} n’est plus en cours d’exécution. {0} is a resource type {0} exited unexpectedly with exit code {1} - {0} exited unexpectedly with exit code {1} + {0} s’est arrêté de manière inattendue avec le code {1} {0} is a resource type, {1} is a number {0} error logs - {0} error logs + {0} journaux d’erreurs {0} is a number 1 error log - 1 error log + 1 journal des erreurs diff --git a/src/Aspire.Dashboard/Resources/xlf/Columns.it.xlf b/src/Aspire.Dashboard/Resources/xlf/Columns.it.xlf index 8868603fef5..3ef82bf99b4 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Columns.it.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Columns.it.xlf @@ -4,97 +4,97 @@ None - None + Nessuno Starting... - Starting... + Avvio in corso... Exception details - Exception details + Dettagli eccezione Container ID: {0} - Container ID: {0} + ID contenitore: {0} {0} is an alphanumeric id Copy container ID to clipboard - Copy container ID to clipboard + Copia ID contenitore negli Appunti Process ID: {0} - Process ID: {0} + ID processo: {0} {0} is a numeric id Container args - Container args + Argomenti contenitore Command: {0} - Command: {0} + Comando: {0} Container command - Container command + Comando contenitore Copy full command to clipboard - Copy full command to clipboard + Copia intero comando negli Appunti Port: {0} - Port: {0} + Porta: {0} {0} is a port number Ports: {0} - Ports: {0} + Porte: {0} {0} is a list of ports Working directory: {0} - Working directory: {0} + Directory di lavoro: {0} {0} is a path Copy image name and tag to clipboard - Copy image name and tag to clipboard + Copia nome e tag dell'immagine negli Appunti Copy file path to clipboard - Copy file path to clipboard + Copia percorso file negli Appunti {0} is no longer running - {0} is no longer running + {0} non è più in esecuzione {0} is a resource type {0} exited unexpectedly with exit code {1} - {0} exited unexpectedly with exit code {1} + La risorsa {0} è stata chiusa in modo imprevisto con codice di uscita {1} {0} is a resource type, {1} is a number {0} error logs - {0} error logs + {0} log degli errori {0} is a number 1 error log - 1 error log + 1 log degli errori diff --git a/src/Aspire.Dashboard/Resources/xlf/Columns.ja.xlf b/src/Aspire.Dashboard/Resources/xlf/Columns.ja.xlf index 7d8d60b6711..1ef54111b53 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Columns.ja.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Columns.ja.xlf @@ -4,97 +4,97 @@ None - None + なし Starting... - Starting... + 開始中... Exception details - Exception details + 例外の詳細 Container ID: {0} - Container ID: {0} + コンテナー ID: {0} {0} is an alphanumeric id Copy container ID to clipboard - Copy container ID to clipboard + コンテナー ID をクリップボードにコピーする Process ID: {0} - Process ID: {0} + プロセス ID: {0} {0} is a numeric id Container args - Container args + コンテナー 引数 Command: {0} - Command: {0} + コマンド: {0} Container command - Container command + コンテナー コマンド Copy full command to clipboard - Copy full command to clipboard + 完全なコマンドをクリップボードにコピーする Port: {0} - Port: {0} + ポート: {0} {0} is a port number Ports: {0} - Ports: {0} + ポート: {0} {0} is a list of ports Working directory: {0} - Working directory: {0} + 作業ディレクトリ: {0} {0} is a path Copy image name and tag to clipboard - Copy image name and tag to clipboard + イメージ名とタグをクリップボードにコピーする Copy file path to clipboard - Copy file path to clipboard + ファイル パスをクリップボードにコピーする {0} is no longer running - {0} is no longer running + {0} が実行されなくなりました {0} is a resource type {0} exited unexpectedly with exit code {1} - {0} exited unexpectedly with exit code {1} + {0} は終了コード{1} で予期せず終了しました {0} is a resource type, {1} is a number {0} error logs - {0} error logs + {0} 件のエラー ログ {0} is a number 1 error log - 1 error log + 1 件のエラー ログ diff --git a/src/Aspire.Dashboard/Resources/xlf/Columns.ko.xlf b/src/Aspire.Dashboard/Resources/xlf/Columns.ko.xlf index cefdd23afd0..6b8909edfff 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Columns.ko.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Columns.ko.xlf @@ -4,97 +4,97 @@ None - None + 없음 Starting... - Starting... + 시작 중... Exception details - Exception details + 예외 세부 정보 Container ID: {0} - Container ID: {0} + 컨테이너 ID: {0} {0} is an alphanumeric id Copy container ID to clipboard - Copy container ID to clipboard + 컨테이너 ID를 클립보드에 복사 Process ID: {0} - Process ID: {0} + 프로세스 ID: {0} {0} is a numeric id Container args - Container args + 컨테이너 인수 Command: {0} - Command: {0} + 명령: {0} Container command - Container command + 컨테이너 명령 Copy full command to clipboard - Copy full command to clipboard + 전체 명령을 클립보드에 복사 Port: {0} - Port: {0} + 포트: {0} {0} is a port number Ports: {0} - Ports: {0} + 포트: {0} {0} is a list of ports Working directory: {0} - Working directory: {0} + 작업 디렉터리: {0} {0} is a path Copy image name and tag to clipboard - Copy image name and tag to clipboard + 이미지 이름 및 태그를 클립보드에 복사 Copy file path to clipboard - Copy file path to clipboard + 파일 경로를 클립보드에 복사 {0} is no longer running - {0} is no longer running + {0}(이)가 더 이상 실행되고 있지 않습니다. {0} is a resource type {0} exited unexpectedly with exit code {1} - {0} exited unexpectedly with exit code {1} + {0}(이)가 종료 코드 {1}(으)로 예기치 않게 종료되었습니다. {0} is a resource type, {1} is a number {0} error logs - {0} error logs + {0}개의 오류 로그 {0} is a number 1 error log - 1 error log + 1개의 오류 로그 diff --git a/src/Aspire.Dashboard/Resources/xlf/Columns.pl.xlf b/src/Aspire.Dashboard/Resources/xlf/Columns.pl.xlf index 5dfa54d32fe..cf8326ab96f 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Columns.pl.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Columns.pl.xlf @@ -4,97 +4,97 @@ None - None + Brak Starting... - Starting... + Trwa uruchamianie... Exception details - Exception details + Szczegóły wyjątku Container ID: {0} - Container ID: {0} + Identyfikator kontenera: {0} {0} is an alphanumeric id Copy container ID to clipboard - Copy container ID to clipboard + Kopiuj identyfikator kontenera do schowka Process ID: {0} - Process ID: {0} + Identyfikator procesu: {0} {0} is a numeric id Container args - Container args + Argumenty kontenera Command: {0} - Command: {0} + Polecenie: {0} Container command - Container command + Polecenie kontenera Copy full command to clipboard - Copy full command to clipboard + Kopiuj polecenie do schowka Port: {0} - Port: {0} + Port: {0} {0} is a port number Ports: {0} - Ports: {0} + Porty: {0} {0} is a list of ports Working directory: {0} - Working directory: {0} + Katalog roboczy: {0} {0} is a path Copy image name and tag to clipboard - Copy image name and tag to clipboard + Kopiuj nazwę i tag obrazu do schowka Copy file path to clipboard - Copy file path to clipboard + Kopiuj ścieżkę pliku do schowka {0} is no longer running - {0} is no longer running + Wątek {0} już nie działa {0} is a resource type {0} exited unexpectedly with exit code {1} - {0} exited unexpectedly with exit code {1} + Nieoczekiwanie zakończony {0} z kodem zakończenia {1} {0} is a resource type, {1} is a number {0} error logs - {0} error logs + Dzienniki błędów: {0} {0} is a number 1 error log - 1 error log + 1 dziennik błędów diff --git a/src/Aspire.Dashboard/Resources/xlf/Columns.pt-BR.xlf b/src/Aspire.Dashboard/Resources/xlf/Columns.pt-BR.xlf index 207e6d588e3..0a8c1fbe752 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Columns.pt-BR.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Columns.pt-BR.xlf @@ -4,97 +4,97 @@ None - None + Nenhum Starting... - Starting... + Iniciando... Exception details - Exception details + Detalhes da exceção Container ID: {0} - Container ID: {0} + ID do Contêiner: {0} {0} is an alphanumeric id Copy container ID to clipboard - Copy container ID to clipboard + Copiar a ID do contêiner para a área de transferência Process ID: {0} - Process ID: {0} + ID do processo: {0} {0} is a numeric id Container args - Container args + Argumentos de contêiner Command: {0} - Command: {0} + Comando: {0} Container command - Container command + Comando de contêiner Copy full command to clipboard - Copy full command to clipboard + Copiar o comando completo para a área de transferência Port: {0} - Port: {0} + Porta: {0} {0} is a port number Ports: {0} - Ports: {0} + Portas: {0} {0} is a list of ports Working directory: {0} - Working directory: {0} + Diretório de trabalho: {0} {0} is a path Copy image name and tag to clipboard - Copy image name and tag to clipboard + Copie o nome e a marca da imagem para a área de transferência Copy file path to clipboard - Copy file path to clipboard + Copiar caminho do arquivo para a área de transferência {0} is no longer running - {0} is no longer running + {0} não está mais em execução {0} is a resource type {0} exited unexpectedly with exit code {1} - {0} exited unexpectedly with exit code {1} + {0} saiu inesperadamente com o código de saída {1} {0} is a resource type, {1} is a number {0} error logs - {0} error logs + {0} log de erros {0} is a number 1 error log - 1 error log + 1 log de erro diff --git a/src/Aspire.Dashboard/Resources/xlf/Columns.ru.xlf b/src/Aspire.Dashboard/Resources/xlf/Columns.ru.xlf index d1d02f5c434..3361803e734 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Columns.ru.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Columns.ru.xlf @@ -4,97 +4,97 @@ None - None + Нет Starting... - Starting... + Производится запуск… Exception details - Exception details + Подробности исключения Container ID: {0} - Container ID: {0} + ИД контейнера: {0} {0} is an alphanumeric id Copy container ID to clipboard - Copy container ID to clipboard + Копировать ИД контейнера в буфер обмена Process ID: {0} - Process ID: {0} + Идентификатор процесса: {0} {0} is a numeric id Container args - Container args + Аргументы контейнера Command: {0} - Command: {0} + Команда: {0} Container command - Container command + Команда контейнера Copy full command to clipboard - Copy full command to clipboard + Копировать команду полностью в буфер обмена Port: {0} - Port: {0} + Порт: {0} {0} is a port number Ports: {0} - Ports: {0} + Порты: {0} {0} is a list of ports Working directory: {0} - Working directory: {0} + Рабочий каталог: {0} {0} is a path Copy image name and tag to clipboard - Copy image name and tag to clipboard + Копировать имя и тег изображения в буфер обмена Copy file path to clipboard - Copy file path to clipboard + Копировать путь файла в буфер обмена {0} is no longer running - {0} is no longer running + {0} больше не выполняется. {0} is a resource type {0} exited unexpectedly with exit code {1} - {0} exited unexpectedly with exit code {1} + {0} неожиданно завершила работу, вернув код {1}. {0} is a resource type, {1} is a number {0} error logs - {0} error logs + Журналов ошибок: {0} {0} is a number 1 error log - 1 error log + 1 журнал ошибок diff --git a/src/Aspire.Dashboard/Resources/xlf/Columns.tr.xlf b/src/Aspire.Dashboard/Resources/xlf/Columns.tr.xlf index 9d1bc067b75..9b0a91b9857 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Columns.tr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Columns.tr.xlf @@ -4,97 +4,97 @@ None - None + Yok Starting... - Starting... + Başlatılıyor... Exception details - Exception details + Özel durum ayrıntıları Container ID: {0} - Container ID: {0} + Kapsayıcı Kimliği: {0} {0} is an alphanumeric id Copy container ID to clipboard - Copy container ID to clipboard + Kapsayıcı kimliğini panoya kopyala Process ID: {0} - Process ID: {0} + İşlem kimliği: {0} {0} is a numeric id Container args - Container args + Kapsayıcı bağımsız değişkenleri Command: {0} - Command: {0} + Komut: {0} Container command - Container command + Kapsayıcı komutu Copy full command to clipboard - Copy full command to clipboard + Tüm komutu panoya kopyalayın Port: {0} - Port: {0} + Bağlantı noktası: {0} {0} is a port number Ports: {0} - Ports: {0} + Bağlantı Noktaları: {0} {0} is a list of ports Working directory: {0} - Working directory: {0} + Çalışma dizini: {0} {0} is a path Copy image name and tag to clipboard - Copy image name and tag to clipboard + Resim adını ve etiketini panoya kopyala Copy file path to clipboard - Copy file path to clipboard + Dosya yolunu panoya kopyala {0} is no longer running - {0} is no longer running + {0} artık çalışmıyor {0} is a resource type {0} exited unexpectedly with exit code {1} - {0} exited unexpectedly with exit code {1} + {0} {1} çıkış kodu ile beklenmedik bir şekilde çıktı {0} is a resource type, {1} is a number {0} error logs - {0} error logs + {0} hata günlükleri {0} is a number 1 error log - 1 error log + 1 hata günlüğü diff --git a/src/Aspire.Dashboard/Resources/xlf/Columns.zh-Hans.xlf b/src/Aspire.Dashboard/Resources/xlf/Columns.zh-Hans.xlf index 5bdecd2d340..0c2e6745c18 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Columns.zh-Hans.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Columns.zh-Hans.xlf @@ -4,97 +4,97 @@ None - None + Starting... - Starting... + 正在启动... Exception details - Exception details + 异常详细信息 Container ID: {0} - Container ID: {0} + 容器 ID: {0} {0} is an alphanumeric id Copy container ID to clipboard - Copy container ID to clipboard + 将容器 ID 复制到剪贴板 Process ID: {0} - Process ID: {0} + 进程 ID: {0} {0} is a numeric id Container args - Container args + 容器参数 Command: {0} - Command: {0} + 命令: {0} Container command - Container command + 容器命令 Copy full command to clipboard - Copy full command to clipboard + 将完整命令复制到剪贴板 Port: {0} - Port: {0} + 端口: {0} {0} is a port number Ports: {0} - Ports: {0} + 端口: {0} {0} is a list of ports Working directory: {0} - Working directory: {0} + 工作目录: {0} {0} is a path Copy image name and tag to clipboard - Copy image name and tag to clipboard + 将图像名称和标记复制到剪贴板 Copy file path to clipboard - Copy file path to clipboard + 将文件路径复制到剪贴板 {0} is no longer running - {0} is no longer running + {0} 已不再运行 {0} is a resource type {0} exited unexpectedly with exit code {1} - {0} exited unexpectedly with exit code {1} + {0} 已意外退出,退出代码为 {1} {0} is a resource type, {1} is a number {0} error logs - {0} error logs + {0} 个错误日志 {0} is a number 1 error log - 1 error log + 1 个错误日志 diff --git a/src/Aspire.Dashboard/Resources/xlf/Columns.zh-Hant.xlf b/src/Aspire.Dashboard/Resources/xlf/Columns.zh-Hant.xlf index 8ae7ba1a439..b855b8fea9d 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Columns.zh-Hant.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Columns.zh-Hant.xlf @@ -4,97 +4,97 @@ None - None + Starting... - Starting... + 正在開始... Exception details - Exception details + 例外狀況詳細資料 Container ID: {0} - Container ID: {0} + 容器識別碼: {0} {0} is an alphanumeric id Copy container ID to clipboard - Copy container ID to clipboard + 將容器識別碼複製至剪貼簿 Process ID: {0} - Process ID: {0} + 處理序識別碼: {0} {0} is a numeric id Container args - Container args + 容器引數 Command: {0} - Command: {0} + 命令: {0} Container command - Container command + 容器命令 Copy full command to clipboard - Copy full command to clipboard + 將完整命令複製至剪貼簿 Port: {0} - Port: {0} + 連接埠: {0} {0} is a port number Ports: {0} - Ports: {0} + 連接埠: {0} {0} is a list of ports Working directory: {0} - Working directory: {0} + 工作目錄: {0} {0} is a path Copy image name and tag to clipboard - Copy image name and tag to clipboard + 將影像名稱和標籤複製至剪貼簿 Copy file path to clipboard - Copy file path to clipboard + 將檔案路徑複製至剪貼簿 {0} is no longer running - {0} is no longer running + {0} 不再執行 {0} is a resource type {0} exited unexpectedly with exit code {1} - {0} exited unexpectedly with exit code {1} + {0} 意外結束,結束代碼: {1} {0} is a resource type, {1} is a number {0} error logs - {0} error logs + {0} 錯誤記錄 {0} is a number 1 error log - 1 error log + 1 個錯誤記錄 diff --git a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.cs.xlf b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.cs.xlf index 0bab7e5e74d..dc747c725b9 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.cs.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.cs.xlf @@ -4,57 +4,57 @@ Failed to initialize - Failed to initialize + Nepovedlo se inicializovat. Finished watching logs - Finished watching logs + Dokončeno sledování protokolů Console logs - Console logs + Protokoly konzoly Initializing log viewer... - Initializing log viewer... + Inicializuje se prohlížeč protokolů... Loading resources ... - Loading resources ... + Načítají se prostředky... Logs not yet available - Logs not yet available + Protokoly ještě nejsou k dispozici. No resource selected - No resource selected + Nevybrán žádný prostředek {0} console logs - {0} console logs + Protokoly konzoly aplikace {0} {0} is an application name Unknown state - Unknown state + Neznámý stav Watching logs... - Watching logs... + Sledují se protokoly... Service log status - Service log status + Stav protokolu služby diff --git a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.de.xlf b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.de.xlf index 3c70b6c5990..ce1eff95a86 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.de.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.de.xlf @@ -4,57 +4,57 @@ Failed to initialize - Failed to initialize + Fehler beim Initialisieren. Finished watching logs - Finished watching logs + Die Protokollwiedergabe ist abgeschlossen. Console logs - Console logs + Konsolenprotokolle Initializing log viewer... - Initializing log viewer... + Protokollanzeige wird initialisiert... Loading resources ... - Loading resources ... + Ressourcen werden geladen... Logs not yet available - Logs not yet available + Protokolle noch nicht verfügbar No resource selected - No resource selected + Keine Ressource ausgewählt {0} console logs - {0} console logs + {0} Konsolenprotokolle {0} is an application name Unknown state - Unknown state + Unbekannter Status Watching logs... - Watching logs... + Protokolle werden überwacht... Service log status - Service log status + Status des Dienstprotokolls diff --git a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.es.xlf b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.es.xlf index d4f31a90df7..bbc079e07d0 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.es.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.es.xlf @@ -4,57 +4,57 @@ Failed to initialize - Failed to initialize + No se pudo inicializar Finished watching logs - Finished watching logs + Finalizó la visualización de registros Console logs - Console logs + Registros de consola Initializing log viewer... - Initializing log viewer... + Inicializando el visor de registros... Loading resources ... - Loading resources ... + Cargando recursos... Logs not yet available - Logs not yet available + Los registros aún no están disponibles No resource selected - No resource selected + No hay ningún recurso seleccionado {0} console logs - {0} console logs + Registros de consola de {0} {0} is an application name Unknown state - Unknown state + Estado desconocido Watching logs... - Watching logs... + Visualizando registros... Service log status - Service log status + Estado del registro de servicio diff --git a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.fr.xlf b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.fr.xlf index 9b6454c4bca..96e8a0ebcf2 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.fr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.fr.xlf @@ -4,57 +4,57 @@ Failed to initialize - Failed to initialize + Impossible d’effectuer l’initialisation Finished watching logs - Finished watching logs + Fin de la surveillance des journaux Console logs - Console logs + Journaux de console Initializing log viewer... - Initializing log viewer... + Initialisation en cours de la visionneuse du journal... Merci de patienter. Loading resources ... - Loading resources ... + Chargement en cours des ressources... Merci de patienter. Logs not yet available - Logs not yet available + Journaux non disponibles pour le moment No resource selected - No resource selected + Aucune ressource sélectionnée {0} console logs - {0} console logs + Journaux de console {0} {0} is an application name Unknown state - Unknown state + État inconnu Watching logs... - Watching logs... + Surveillance en cours des journaux... Merci de patienter. Service log status - Service log status + État du journal de service diff --git a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.it.xlf b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.it.xlf index 24eb6af70b6..889560e1044 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.it.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.it.xlf @@ -4,57 +4,57 @@ Failed to initialize - Failed to initialize + Inizializzazione non riuscita Finished watching logs - Finished watching logs + Controllo dei log terminato Console logs - Console logs + Log della console Initializing log viewer... - Initializing log viewer... + Inizializzazione del visualizzatore log in corso... Loading resources ... - Loading resources ... + Caricamento delle risorse in corso... Logs not yet available - Logs not yet available + Log non ancora disponibili No resource selected - No resource selected + Nessuna risorsa selezionata {0} console logs - {0} console logs + {0} log della console {0} is an application name Unknown state - Unknown state + Stato sconosciuto Watching logs... - Watching logs... + Controllo dei log in corso... Service log status - Service log status + Stato del log del servizio diff --git a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.ja.xlf b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.ja.xlf index ce948b11ce5..72247b7c62c 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.ja.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.ja.xlf @@ -4,57 +4,57 @@ Failed to initialize - Failed to initialize + 初期化できませんでした Finished watching logs - Finished watching logs + ログの監視が完了しました Console logs - Console logs + コンソール ログ Initializing log viewer... - Initializing log viewer... + ログ ビューアーを初期化しています... Loading resources ... - Loading resources ... + リソースを読み込んでいます ... Logs not yet available - Logs not yet available + ログはまだ使用できません No resource selected - No resource selected + リソースが選択されていません {0} console logs - {0} console logs + {0} のコンソール ログ {0} is an application name Unknown state - Unknown state + 不明な状態 Watching logs... - Watching logs... + ログを監視しています... Service log status - Service log status + サービス ログの状態 diff --git a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.ko.xlf b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.ko.xlf index 7f47de9df19..0efc83b96a4 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.ko.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.ko.xlf @@ -4,57 +4,57 @@ Failed to initialize - Failed to initialize + 초기화 실패 Finished watching logs - Finished watching logs + 로그 보기 완료 Console logs - Console logs + 콘솔 로그 Initializing log viewer... - Initializing log viewer... + 로그 뷰어를 초기화하는 중... Loading resources ... - Loading resources ... + 리소스를 로드하는 중... Logs not yet available - Logs not yet available + 로그를 아직 사용할 수 없음 No resource selected - No resource selected + 리소스를 선택하지 않음 {0} console logs - {0} console logs + {0} 콘솔 로그 {0} is an application name Unknown state - Unknown state + 알 수 없는 상태 Watching logs... - Watching logs... + 로그를 보는 중... Service log status - Service log status + 서비스 로그 상태 diff --git a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.pl.xlf b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.pl.xlf index 78811a3b250..c3ed26dd39f 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.pl.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.pl.xlf @@ -4,57 +4,57 @@ Failed to initialize - Failed to initialize + Nie można zainicjować Finished watching logs - Finished watching logs + Zakończono oglądanie dzienników Console logs - Console logs + Dzienniki konsoli Initializing log viewer... - Initializing log viewer... + Trwa inicjowanie podglądu dziennika... Loading resources ... - Loading resources ... + Trwa ładowanie zasobów... Logs not yet available - Logs not yet available + Dzienniki nie są jeszcze dostępne No resource selected - No resource selected + Nie wybrano zasobu {0} console logs - {0} console logs + Dzienniki konsoli: {0} {0} is an application name Unknown state - Unknown state + Nieznany stan Watching logs... - Watching logs... + Trwa oglądanie dzienników... Service log status - Service log status + Stan dziennika usługi diff --git a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.pt-BR.xlf b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.pt-BR.xlf index 04b6d5acd0a..606808b2193 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.pt-BR.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.pt-BR.xlf @@ -4,57 +4,57 @@ Failed to initialize - Failed to initialize + Falha ao inicializar Finished watching logs - Finished watching logs + Concluída a observação dos logs Console logs - Console logs + Logs do console Initializing log viewer... - Initializing log viewer... + Inicializando visualizador de log... Loading resources ... - Loading resources ... + Carregando recursos ... Logs not yet available - Logs not yet available + Logs ainda não disponíveis No resource selected - No resource selected + Não há nenhum recurso selecionado {0} console logs - {0} console logs + {0} logs do console {0} is an application name Unknown state - Unknown state + Estado desconhecido Watching logs... - Watching logs... + Observando logs... Service log status - Service log status + Status de log de serviço diff --git a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.ru.xlf b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.ru.xlf index 7448a32446c..940dcd238fd 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.ru.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.ru.xlf @@ -4,57 +4,57 @@ Failed to initialize - Failed to initialize + Не удалось выполнить инициализацию Finished watching logs - Finished watching logs + Просмотр журналов завершен Console logs - Console logs + Журналы консоли Initializing log viewer... - Initializing log viewer... + Инициализируется средство просмотра журналов... Loading resources ... - Loading resources ... + Производится загрузка ресурсов... Logs not yet available - Logs not yet available + Журналы пока недоступны No resource selected - No resource selected + Нет выбранных ресурсов {0} console logs - {0} console logs + Журналов консоли: {0} {0} is an application name Unknown state - Unknown state + Неизвестное состояние Watching logs... - Watching logs... + Просмотр журналов... Service log status - Service log status + Состояние журнала службы diff --git a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.tr.xlf b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.tr.xlf index 33b1354eef1..dc66f8bf0af 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.tr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.tr.xlf @@ -4,57 +4,57 @@ Failed to initialize - Failed to initialize + Başlatılamadı Finished watching logs - Finished watching logs + Günlükleri izleme tamamlandı Console logs - Console logs + Konsol günlükleri Initializing log viewer... - Initializing log viewer... + Günlük görüntüleyicisi başlatılıyor... Loading resources ... - Loading resources ... + Kaynaklar yükleniyor ... Logs not yet available - Logs not yet available + Günlükler henüz kullanılamıyor No resource selected - No resource selected + Hiçbir kaynak seçilmedi {0} console logs - {0} console logs + {0} konsol günlükleri {0} is an application name Unknown state - Unknown state + Bilinmeyen durum Watching logs... - Watching logs... + Günlükler izleniyor... Service log status - Service log status + Hizmet günlüğü durumu diff --git a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.zh-Hans.xlf b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.zh-Hans.xlf index 8a7490629a8..28b7c3eaf96 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.zh-Hans.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.zh-Hans.xlf @@ -4,57 +4,57 @@ Failed to initialize - Failed to initialize + 无法初始化 Finished watching logs - Finished watching logs + 已完成监视日志 Console logs - Console logs + 控制台日志 Initializing log viewer... - Initializing log viewer... + 正在初始化日志查看器... Loading resources ... - Loading resources ... + 正在加载资源... Logs not yet available - Logs not yet available + 日志尚不可用 No resource selected - No resource selected + 未选择资源 {0} console logs - {0} console logs + {0} 控制台日志 {0} is an application name Unknown state - Unknown state + 未知状态 Watching logs... - Watching logs... + 正在监视日志... Service log status - Service log status + 服务日志状态 diff --git a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.zh-Hant.xlf b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.zh-Hant.xlf index 43d193a7cfb..6889615f680 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.zh-Hant.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.zh-Hant.xlf @@ -4,57 +4,57 @@ Failed to initialize - Failed to initialize + 無法初始化 Finished watching logs - Finished watching logs + 已完成監看記錄 Console logs - Console logs + 主控台記錄 Initializing log viewer... - Initializing log viewer... + 正在初始化記錄檢視器... Loading resources ... - Loading resources ... + 正在載入資源 ... Logs not yet available - Logs not yet available + 記錄尚不可用 No resource selected - No resource selected + 未選取任何資源 {0} console logs - {0} console logs + {0} 主控台記錄 {0} is an application name Unknown state - Unknown state + 未知狀態 Watching logs... - Watching logs... + 正在監看記錄... Service log status - Service log status + 服務記錄狀態 diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.cs.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.cs.xlf index d0079447bc6..4a815faef6c 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.cs.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.cs.xlf @@ -4,262 +4,262 @@ All - All + Vše All tags - All tags + Všechny značky Filtered tags - Filtered tags + Vyfiltrované značky Filters - Filters + Filtry Graph. For an accessible view please navigate to the Table tab - Graph. For an accessible view please navigate to the Table tab + Graf. Pokud chcete použít přístupné zobrazení, přejděte prosím na kartu Tabulka. Graph - Graph + Graf (None) - (None) + (žádné) Options - Options + Možnosti Select filters - Select filters + Vybrat filtry Show count - Show count + Zobrazit počet Table - Table + Tabulka Unable to display chart - Unable to display chart + Graf nelze zobrazit. Details - Details + Podrobnosti Duration - Duration + Doba trvání Event - Event + Událost Filter... - Filter... + Filtrovat… Show all - Show all + Zobrazit vše Show spec only - Show spec only + Zobrazit jen specifikaci Spec means from the resource specification Hide values - Hide values + Skrýt hodnoty Show values - Show values + Zobrazit hodnoty Copied! - Copied! + Zkopírováno! Copy to clipboard - Copy to clipboard + Zkopírovat do schránky Hide value - Hide value + Skrýt hodnotu Show value - Show value + Zobrazit hodnotu Loading... - Loading... + Načítání… No metrics data found - No metrics data found + Nenašla se žádná data metrik. Show latest 10 values - Show latest 10 values + Zobrazit nejnovějších 10 hodnot Only show value updates - Only show value updates + Zobrazit jenom aktualizace hodnot Time - Time + Čas Value decreased - Value decreased + Hodnota se snížila. Value increased - Value increased + Hodnota se zvýšila. Value did not change - Value did not change + Hodnota se nezměnila. Name - Name + Název Count - Count + Počet Count is the name of a plot y-axis label Length - Length + Délka Length is the name of a plot y-axis label Value - Value + Hodnota Value is the name of a plot y-axis label Value - Value + Hodnota replica set - replica set + sada replik {0} (replica of {1}) - {0} (replica of {1}) + {0} (replika sady {1}) {0} is a resource name, {1} is a replica set name Select a resource - Select a resource + Vybrat prostředek Select an application - Select an application + Vybrat aplikaci Duration <strong>{0}</strong> - Duration <strong>{0}</strong> + Doba trvání: <strong>{0}</strong> {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified Service <strong>{0}</strong> - Service <strong>{0}</strong> + Služba <strong>{0}</strong> {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified Start time <strong>{0}</strong> - Start time <strong>{0}</strong> + Čas spuštění: <strong>{0}</strong> {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified Time offset - Time offset + Časový posun Timestamp - Timestamp + Časové razítko View - View + Zobrazit View logs - View logs + Zobrazit protokoly Close - Close + Zavřít Split horizontal - Split horizontal + Rozdělit vodorovně Split vertical - Split vertical + Rozdělit svisle Total: <strong>{0} results found</strong> - Total: <strong>{0} results found</strong> + Celkem: <strong>nalezené výsledky: {0} </strong> {0} is a number. This is raw markup, so <strong> and </strong> should not be modified diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.de.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.de.xlf index 00de4fa75c2..fb9a99f93a4 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.de.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.de.xlf @@ -4,262 +4,262 @@ All - All + Alle All tags - All tags + Alle Tags Filtered tags - Filtered tags + Gefilterte Tags Filters - Filters + Filter Graph. For an accessible view please navigate to the Table tab - Graph. For an accessible view please navigate to the Table tab + Graph. Navigieren Sie zur Registerkarte "Tabelle", um eine barrierefreie Ansicht anzuzeigen. Graph - Graph + Graph (None) - (None) + (Keine) Options - Options + Optionen Select filters - Select filters + Filter auswählen Show count - Show count + Anzahl anzeigen Table - Table + Tabelle Unable to display chart - Unable to display chart + Diagramm kann nicht angezeigt werden Details - Details + Details Duration - Duration + Dauer Event - Event + Ereignis Filter... - Filter... + Filtern... Show all - Show all + Alle anzeigen Show spec only - Show spec only + Nur Spezifikation anzeigen Spec means from the resource specification Hide values - Hide values + Werte ausblenden Show values - Show values + Werte anzeigen Copied! - Copied! + Kopiert! Copy to clipboard - Copy to clipboard + In Zwischenablage kopieren Hide value - Hide value + Wert ausblenden Show value - Show value + Wert anzeigen Loading... - Loading... + Wird geladen... No metrics data found - No metrics data found + Keine Metrikdaten gefunden. Show latest 10 values - Show latest 10 values + Neueste 10 Werte anzeigen Only show value updates - Only show value updates + Nur Wertaktualisierungen anzeigen Time - Time + Zeit Value decreased - Value decreased + Wert verringert Value increased - Value increased + Wert erhöht Value did not change - Value did not change + Wert wurde nicht geändert Name - Name + Name Count - Count + Anzahl Count is the name of a plot y-axis label Length - Length + Länge Length is the name of a plot y-axis label Value - Value + Wert Value is the name of a plot y-axis label Value - Value + Wert replica set - replica set + Replikatgruppe {0} (replica of {1}) - {0} (replica of {1}) + {0} (Replikat von {1}) {0} is a resource name, {1} is a replica set name Select a resource - Select a resource + Ressource auswählen Select an application - Select an application + Anwendung auswählen Duration <strong>{0}</strong> - Duration <strong>{0}</strong> + Dauer <strong>{0}</strong> {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified Service <strong>{0}</strong> - Service <strong>{0}</strong> + Dienst <strong>{0}</strong> {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified Start time <strong>{0}</strong> - Start time <strong>{0}</strong> + Startzeit <strong>{0}</strong> {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified Time offset - Time offset + Zeitoffset Timestamp - Timestamp + Zeitstempel View - View + Anzeigen View logs - View logs + Protokolle anzeigen Close - Close + Schließen Split horizontal - Split horizontal + Horizontal teilen Split vertical - Split vertical + Vertikal teilen Total: <strong>{0} results found</strong> - Total: <strong>{0} results found</strong> + Gesamt: <strong>{0} Ergebnisse gefunden</strong> {0} is a number. This is raw markup, so <strong> and </strong> should not be modified diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.es.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.es.xlf index 1780d272584..6146f02bd00 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.es.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.es.xlf @@ -4,262 +4,262 @@ All - All + Todo All tags - All tags + Todas las etiquetas Filtered tags - Filtered tags + Etiquetas filtradas Filters - Filters + Filtros Graph. For an accessible view please navigate to the Table tab - Graph. For an accessible view please navigate to the Table tab + Graph. Para obtener una vista accesible, vaya a la pestaña Tabla. Graph - Graph + Gráfico (None) - (None) + (Ninguno) Options - Options + Opciones Select filters - Select filters + Seleccione filtros. Show count - Show count + Mostrar recuento Table - Table + Tabla Unable to display chart - Unable to display chart + No se puede mostrar el gráfico Details - Details + Detalles Duration - Duration + Duración Event - Event + Evento Filter... - Filter... + Filtrar... Show all - Show all + Mostrar todo Show spec only - Show spec only + Mostrar solo especificación Spec means from the resource specification Hide values - Hide values + Ocultar los valores Show values - Show values + Mostrar valores Copied! - Copied! + Copia realizada. Copy to clipboard - Copy to clipboard + Copiar al portapapeles Hide value - Hide value + Ocultar valor Show value - Show value + Mostrar valor Loading... - Loading... + Cargando... No metrics data found - No metrics data found + No se encontraron datos de métricas Show latest 10 values - Show latest 10 values + Mostrar los 10 valores más recientes Only show value updates - Only show value updates + Mostrar solo las actualizaciones del valor Time - Time + Hora Value decreased - Value decreased + Valor reducido Value increased - Value increased + Valor aumentado Value did not change - Value did not change + El valor no cambió Name - Name + Nombre Count - Count + Recuento Count is the name of a plot y-axis label Length - Length + Longitud Length is the name of a plot y-axis label Value - Value + Valor Value is the name of a plot y-axis label Value - Value + Valor replica set - replica set + conjunto de réplicas {0} (replica of {1}) - {0} (replica of {1}) + {0} (réplica de {1}) {0} is a resource name, {1} is a replica set name Select a resource - Select a resource + Seleccionar un recurso Select an application - Select an application + Seleccionar una aplicación Duration <strong>{0}</strong> - Duration <strong>{0}</strong> + Duración <strong>{0}</strong> {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified Service <strong>{0}</strong> - Service <strong>{0}</strong> + Servicio <strong>{0}</strong> {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified Start time <strong>{0}</strong> - Start time <strong>{0}</strong> + Hora de inicio <strong>{0}</strong> {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified Time offset - Time offset + Diferencia de tiempo Timestamp - Timestamp + Marca de tiempo View - View + Ver View logs - View logs + Ver registros Close - Close + Cerrar Split horizontal - Split horizontal + Dividir horizontalmente Split vertical - Split vertical + Dividir verticalmente Total: <strong>{0} results found</strong> - Total: <strong>{0} results found</strong> + Total: <strong>{0} resultados encontrados</strong> {0} is a number. This is raw markup, so <strong> and </strong> should not be modified diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.fr.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.fr.xlf index 6481483e4a2..e74821bce87 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.fr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.fr.xlf @@ -4,262 +4,262 @@ All - All + Tous All tags - All tags + Toutes les balises Filtered tags - Filtered tags + Balises filtrées Filters - Filters + Filtres Graph. For an accessible view please navigate to the Table tab - Graph. For an accessible view please navigate to the Table tab + Graph. Si vous souhaitez obtenir un affichage accessible, veuillez accéder à l’onglet Tableau Graph - Graph + Graph (None) - (None) + (Aucun) Options - Options + Options Select filters - Select filters + Sélectionner des filtres Show count - Show count + Afficher le nombre Table - Table + Table Unable to display chart - Unable to display chart + Impossible d’afficher le graphique Details - Details + Détails Duration - Duration + Durée Event - Event + Événement Filter... - Filter... + Filtrer... Show all - Show all + Tout afficher Show spec only - Show spec only + Afficher uniquement la spécification Spec means from the resource specification Hide values - Hide values + Masquer les valeurs Show values - Show values + Afficher les valeurs Copied! - Copied! + Copié ! Copy to clipboard - Copy to clipboard + Copier dans le Presse-papiers Hide value - Hide value + Masquer une valeur Show value - Show value + Afficher une valeur Loading... - Loading... + Chargement... No metrics data found - No metrics data found + Données de métriques introuvables Show latest 10 values - Show latest 10 values + Afficher les 10 dernières valeurs Only show value updates - Only show value updates + Afficher uniquement les mises à jour de valeurs Time - Time + Heure Value decreased - Value decreased + Valeur réduite Value increased - Value increased + Valeur augmentée Value did not change - Value did not change + La valeur n’a pas changé Name - Name + Nom Count - Count + Nombre Count is the name of a plot y-axis label Length - Length + Longueur Length is the name of a plot y-axis label Value - Value + Valeur Value is the name of a plot y-axis label Value - Value + Valeur replica set - replica set + jeu de réplicas {0} (replica of {1}) - {0} (replica of {1}) + {0} (réplica de {1}) {0} is a resource name, {1} is a replica set name Select a resource - Select a resource + Sélectionner une ressource Select an application - Select an application + Sélectionner une application Duration <strong>{0}</strong> - Duration <strong>{0}</strong> + Durée <strong>{0}</strong> {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified Service <strong>{0}</strong> - Service <strong>{0}</strong> + Service <strong>{0}</strong> {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified Start time <strong>{0}</strong> - Start time <strong>{0}</strong> + <strong>Heure de début : </strong> {0} {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified Time offset - Time offset + Décalage de l’heure Timestamp - Timestamp + Horodatage View - View + Afficher View logs - View logs + Afficher les journaux Close - Close + Fermer Split horizontal - Split horizontal + Fractionner horizontalement Split vertical - Split vertical + Fractionner verticalement Total: <strong>{0} results found</strong> - Total: <strong>{0} results found</strong> + Total : <strong>{0} résultats trouvés</strong> {0} is a number. This is raw markup, so <strong> and </strong> should not be modified diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.it.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.it.xlf index 96035a8c043..030d2b8409b 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.it.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.it.xlf @@ -4,262 +4,262 @@ All - All + Tutto All tags - All tags + Tutti i tag Filtered tags - Filtered tags + Tag filtrati Filters - Filters + Filtri Graph. For an accessible view please navigate to the Table tab - Graph. For an accessible view please navigate to the Table tab + Grafo. Per una visualizzazione accessibile, passa alla scheda Tabella Graph - Graph + Grafo (None) - (None) + (Nessuno) Options - Options + Opzioni Select filters - Select filters + Seleziona filtri Show count - Show count + Mostra conteggio Table - Table + Tabella Unable to display chart - Unable to display chart + Non è possibile visualizzare il grafico Details - Details + Dettagli Duration - Duration + Durata Event - Event + Evento Filter... - Filter... + Filtro... Show all - Show all + Mostra tutto Show spec only - Show spec only + Mostra solo specifiche Spec means from the resource specification Hide values - Hide values + Nascondi valori Show values - Show values + Mostra valori Copied! - Copied! + Copia completata. Copy to clipboard - Copy to clipboard + Copia negli Appunti Hide value - Hide value + Nascondi valore Show value - Show value + Mostra valore Loading... - Loading... + Caricamento in corso... No metrics data found - No metrics data found + Nessun dato delle metriche trovato Show latest 10 values - Show latest 10 values + Mostra i 10 valori più recenti Only show value updates - Only show value updates + Mostra solo gli aggiornamenti dei valori Time - Time + Orario Value decreased - Value decreased + Valore diminuito Value increased - Value increased + Valore aumentato Value did not change - Value did not change + Il valore non è stato modificato Name - Name + Nome Count - Count + Numero Count is the name of a plot y-axis label Length - Length + Lunghezza Length is the name of a plot y-axis label Value - Value + Valore Value is the name of a plot y-axis label Value - Value + Valore replica set - replica set + set di repliche {0} (replica of {1}) - {0} (replica of {1}) + {0} (replica di {1}) {0} is a resource name, {1} is a replica set name Select a resource - Select a resource + Seleziona una risorsa Select an application - Select an application + Seleziona un'applicazione Duration <strong>{0}</strong> - Duration <strong>{0}</strong> + Durata <strong>{0}</strong> {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified Service <strong>{0}</strong> - Service <strong>{0}</strong> + Servizio <strong>{0}</strong> {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified Start time <strong>{0}</strong> - Start time <strong>{0}</strong> + Ora di inizio <strong>{0}</strong> {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified Time offset - Time offset + Differenza di tempo Timestamp - Timestamp + Timestamp View - View + Visualizza View logs - View logs + Visualizza log Close - Close + Chiudi Split horizontal - Split horizontal + Dividi in orizzontale Split vertical - Split vertical + Dividi in verticale Total: <strong>{0} results found</strong> - Total: <strong>{0} results found</strong> + Totale: <strong>{0} risultati trovati</strong> {0} is a number. This is raw markup, so <strong> and </strong> should not be modified diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ja.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ja.xlf index 5f4c0cb5ac7..b6901976927 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ja.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ja.xlf @@ -4,262 +4,262 @@ All - All + すべて All tags - All tags + すべてのタグ Filtered tags - Filtered tags + フィルター処理されたタグ Filters - Filters + フィルター Graph. For an accessible view please navigate to the Table tab - Graph. For an accessible view please navigate to the Table tab + グラフ。アクセス可能なビューについては、[テーブル] タブに移動してください Graph - Graph + グラフ (None) - (None) + (なし) Options - Options + オプション​​ Select filters - Select filters + フィルターの選択 Show count - Show count + カウントの表示 Table - Table + テーブル Unable to display chart - Unable to display chart + グラフを表示できません Details - Details + 詳細 Duration - Duration + 継続時間 Event - Event + イベント Filter... - Filter... + フィルター... Show all - Show all + すべてを表示 Show spec only - Show spec only + 仕様のみを表示 Spec means from the resource specification Hide values - Hide values + 値を非表示にする Show values - Show values + 値を表示する Copied! - Copied! + コピーしました。 Copy to clipboard - Copy to clipboard + クリップボードにコピーする Hide value - Hide value + 値を非表示にする Show value - Show value + 値の表示 Loading... - Loading... + 読み込み中... No metrics data found - No metrics data found + メトリック データが見つかりません Show latest 10 values - Show latest 10 values + 最新の 10 個の値を表示する Only show value updates - Only show value updates + 値の更新のみを表示する Time - Time + 時間 Value decreased - Value decreased + 値が減少しました Value increased - Value increased + 値が増加しました Value did not change - Value did not change + 値は変更されませんでした Name - Name + 名前 Count - Count + カウント Count is the name of a plot y-axis label Length - Length + 長さ Length is the name of a plot y-axis label Value - Value + Value is the name of a plot y-axis label Value - Value + replica set - replica set + レプリカ セット {0} (replica of {1}) - {0} (replica of {1}) + {0} ({1} のレプリカ) {0} is a resource name, {1} is a replica set name Select a resource - Select a resource + リソースの選択 Select an application - Select an application + アプリケーションを選択する Duration <strong>{0}</strong> - Duration <strong>{0}</strong> + 期間 <strong>{0}</strong> {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified Service <strong>{0}</strong> - Service <strong>{0}</strong> + サービス <strong>{0}</strong> {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified Start time <strong>{0}</strong> - Start time <strong>{0}</strong> + 開始時刻 <strong>{0}</strong> {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified Time offset - Time offset + 時刻オフセット Timestamp - Timestamp + タイムスタンプ View - View + 表示 View logs - View logs + ログの表示 Close - Close + 閉じる Split horizontal - Split horizontal + 左右に分割する Split vertical - Split vertical + 上下に分割する Total: <strong>{0} results found</strong> - Total: <strong>{0} results found</strong> + 合計: <strong>{0} 件の結果が見つかりました</strong> {0} is a number. This is raw markup, so <strong> and </strong> should not be modified diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ko.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ko.xlf index 5326c24a1ef..803a1953d67 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ko.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ko.xlf @@ -4,262 +4,262 @@ All - All + 모두 All tags - All tags + 모든 태그 Filtered tags - Filtered tags + 필터링된 태그 Filters - Filters + 필터 Graph. For an accessible view please navigate to the Table tab - Graph. For an accessible view please navigate to the Table tab + 그래프. 액세스 가능한 보기를 보려면 표 탭으로 이동하세요. Graph - Graph + 그래프 (None) - (None) + (없음) Options - Options + 옵션 Select filters - Select filters + 필터 선택 Show count - Show count + 개수 표시 Table - Table + Unable to display chart - Unable to display chart + 차트를 표시할 수 없음 Details - Details + 세부 정보 Duration - Duration + 기간 Event - Event + 이벤트 Filter... - Filter... + 필터... Show all - Show all + 모두 표시 Show spec only - Show spec only + 사양만 표시 Spec means from the resource specification Hide values - Hide values + 값 숨기기 Show values - Show values + 값 표시 Copied! - Copied! + 복사됨! Copy to clipboard - Copy to clipboard + 클립보드로 복사 Hide value - Hide value + 값 숨기기 Show value - Show value + 값 표시 Loading... - Loading... + 로드 중... No metrics data found - No metrics data found + 메트릭 데이터를 찾을 수 없음 Show latest 10 values - Show latest 10 values + 최신 10개의 값 표시 Only show value updates - Only show value updates + 값 업데이트만 표시 Time - Time + 시간 Value decreased - Value decreased + 값 감소됨 Value increased - Value increased + 값 증가됨 Value did not change - Value did not change + 값이 변경되지 않음 Name - Name + 이름 Count - Count + 개수 Count is the name of a plot y-axis label Length - Length + 길이 Length is the name of a plot y-axis label Value - Value + Value is the name of a plot y-axis label Value - Value + replica set - replica set + 복제본 세트 {0} (replica of {1}) - {0} (replica of {1}) + {0}({1}의 복제본) {0} is a resource name, {1} is a replica set name Select a resource - Select a resource + 리소스 선택 Select an application - Select an application + 애플리케이션 선택 Duration <strong>{0}</strong> - Duration <strong>{0}</strong> + 기간 <strong>{0}</strong> {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified Service <strong>{0}</strong> - Service <strong>{0}</strong> + 서비스 <strong>{0}</strong> {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified Start time <strong>{0}</strong> - Start time <strong>{0}</strong> + 시작 시간 <strong>{0}</strong> {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified Time offset - Time offset + 시간 오프셋 Timestamp - Timestamp + 타임스탬프 View - View + 보기 View logs - View logs + 로그 보기 Close - Close + 닫기 Split horizontal - Split horizontal + 가로 분할 Split vertical - Split vertical + 세로 분할 Total: <strong>{0} results found</strong> - Total: <strong>{0} results found</strong> + 합계: <strong>{0}개 검색 결과를 찾음</strong> {0} is a number. This is raw markup, so <strong> and </strong> should not be modified diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.pl.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.pl.xlf index 309434e90b5..119f20480e9 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.pl.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.pl.xlf @@ -4,262 +4,262 @@ All - All + Wszystko All tags - All tags + Wszystkie tagi Filtered tags - Filtered tags + Filtrowane tagi Filters - Filters + Filtry Graph. For an accessible view please navigate to the Table tab - Graph. For an accessible view please navigate to the Table tab + Wykres. Aby uzyskać dostępny widok, przejdź do karty Tabela Graph - Graph + Graph (None) - (None) + (Brak) Options - Options + Opcje Select filters - Select filters + Wybierz filtry Show count - Show count + Pokaż liczbę Table - Table + Tabela Unable to display chart - Unable to display chart + Nie można wyświetlić wykresu Details - Details + Szczegóły Duration - Duration + Czas trwania Event - Event + Zdarzenie Filter... - Filter... + Filtruj... Show all - Show all + Pokaż wszystkie Show spec only - Show spec only + Pokaż tylko specyfikację Spec means from the resource specification Hide values - Hide values + Ukryj wartości Show values - Show values + Pokaż wartości Copied! - Copied! + Skopiowano! Copy to clipboard - Copy to clipboard + Kopiuj do schowka Hide value - Hide value + Ukryj wartość Show value - Show value + Pokaż wartość Loading... - Loading... + Trwa ładowanie... No metrics data found - No metrics data found + Nie znaleziono danych metryk Show latest 10 values - Show latest 10 values + Pokaż ostatnie 10 wartości Only show value updates - Only show value updates + Pokaż tylko aktualizacje wartości Time - Time + Czas Value decreased - Value decreased + Zmniejszono wartość Value increased - Value increased + Zwiększono wartość Value did not change - Value did not change + Wartość nie została zmieniona Name - Name + Nazwa Count - Count + Liczba Count is the name of a plot y-axis label Length - Length + Długość Length is the name of a plot y-axis label Value - Value + Wartość Value is the name of a plot y-axis label Value - Value + Wartość replica set - replica set + zestaw replik {0} (replica of {1}) - {0} (replica of {1}) + {0} (replika {1}) {0} is a resource name, {1} is a replica set name Select a resource - Select a resource + Wybierz zasób Select an application - Select an application + Wybierz aplikację Duration <strong>{0}</strong> - Duration <strong>{0}</strong> + Czas trwania: <strong>{0}</strong> {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified Service <strong>{0}</strong> - Service <strong>{0}</strong> + Usługa <strong>{0}</strong> {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified Start time <strong>{0}</strong> - Start time <strong>{0}</strong> + Czas rozpoczęcia: <strong>{0}</strong> {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified Time offset - Time offset + Przesunięcie czasu Timestamp - Timestamp + Znacznik czasu View - View + Wyświetl View logs - View logs + Wyświetl dzienniki Close - Close + Zamknij Split horizontal - Split horizontal + Podziel w poziomie Split vertical - Split vertical + Podziel w pionie Total: <strong>{0} results found</strong> - Total: <strong>{0} results found</strong> + Łączna liczba <strong>znalezionych wyników {0}</strong> {0} is a number. This is raw markup, so <strong> and </strong> should not be modified diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.pt-BR.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.pt-BR.xlf index 7b9f7e2eaf2..4b74e93908a 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.pt-BR.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.pt-BR.xlf @@ -4,262 +4,262 @@ All - All + Tudo All tags - All tags + Todas as marcas Filtered tags - Filtered tags + Marcas filtradas Filters - Filters + Filtros Graph. For an accessible view please navigate to the Table tab - Graph. For an accessible view please navigate to the Table tab + Microsoft Azure Active Directory Graph. Para obter uma exibição acessível, navegue até a guia Tabela Graph - Graph + Microsoft Azure Active Directory Graph (None) - (None) + (Nenhum) Options - Options + Opções Select filters - Select filters + Selecionar filtros Show count - Show count + Mostrar contagem Table - Table + Tabela Unable to display chart - Unable to display chart + Não é possível exibir o gráfico Details - Details + Detalhes Duration - Duration + Duração Event - Event + Evento Filter... - Filter... + Filtro... Show all - Show all + Mostrar tudo Show spec only - Show spec only + Mostrar somente especificação Spec means from the resource specification Hide values - Hide values + Ocultar valores Show values - Show values + Mostrar valores Copied! - Copied! + Copiado. Copy to clipboard - Copy to clipboard + Copiar para a área de transferência Hide value - Hide value + Ocultar valor Show value - Show value + Exibir valor Loading... - Loading... + Carregando... No metrics data found - No metrics data found + Nenhum dado de métrica encontrado Show latest 10 values - Show latest 10 values + Mostrar os 10 valores mais recentes Only show value updates - Only show value updates + Mostrar somente atualizações de valor Time - Time + Tempo Value decreased - Value decreased + Valor diminuído Value increased - Value increased + Valor aumentado Value did not change - Value did not change + O valor não foi alterado Name - Name + Nome Count - Count + Número Count is the name of a plot y-axis label Length - Length + Comprimento Length is the name of a plot y-axis label Value - Value + Valor Value is the name of a plot y-axis label Value - Value + Valor replica set - replica set + conjunto de réplicas {0} (replica of {1}) - {0} (replica of {1}) + {0} (réplica de {1}) {0} is a resource name, {1} is a replica set name Select a resource - Select a resource + Selecionar um recurso Select an application - Select an application + Selecionar um aplicativo Duration <strong>{0}</strong> - Duration <strong>{0}</strong> + Duração <strong>{0}</strong> {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified Service <strong>{0}</strong> - Service <strong>{0}</strong> + Serviço <strong>{0}</strong> {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified Start time <strong>{0}</strong> - Start time <strong>{0}</strong> + Horário de início <strong>{0}</strong> {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified Time offset - Time offset + Deslocamento de tempo Timestamp - Timestamp + Carimbo de data/hora View - View + Exibir View logs - View logs + Exibir registros Close - Close + Fechar Split horizontal - Split horizontal + Dividir horizontalmente Split vertical - Split vertical + Dividir verticalmente Total: <strong>{0} results found</strong> - Total: <strong>{0} results found</strong> + Total: <strong>{0} resultados encontrados</strong> {0} is a number. This is raw markup, so <strong> and </strong> should not be modified diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ru.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ru.xlf index 0c5a6d21436..80937b85695 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ru.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ru.xlf @@ -4,262 +4,262 @@ All - All + Все All tags - All tags + Все теги Filtered tags - Filtered tags + Отфильтрованные теги Filters - Filters + Фильтры Graph. For an accessible view please navigate to the Table tab - Graph. For an accessible view please navigate to the Table tab + График. Чтобы открыть представление с поддержкой специальных возможностей, перейдите на вкладку "Таблица" Graph - Graph + График (None) - (None) + (Нет) Options - Options + Параметры Select filters - Select filters + Выбор фильтров Show count - Show count + Показать счетчик Table - Table + Таблица Unable to display chart - Unable to display chart + Не удалось показать график Details - Details + Сведения Duration - Duration + Длительность Event - Event + Событие Filter... - Filter... + Фильтр... Show all - Show all + Показать все Show spec only - Show spec only + Показать только спецификацию Spec means from the resource specification Hide values - Hide values + Скрыть значения Show values - Show values + Показать значения Copied! - Copied! + Скопировано! Copy to clipboard - Copy to clipboard + Копировать в буфер обмена Hide value - Hide value + Скрыть значение Show value - Show value + Показать значение Loading... - Loading... + Идет загрузка... No metrics data found - No metrics data found + Данные метрик не найдены Show latest 10 values - Show latest 10 values + Показать последние 10 значений Only show value updates - Only show value updates + Показывать только изменения значений Time - Time + Время Value decreased - Value decreased + Значение уменьшено Value increased - Value increased + Значение увеличено Value did not change - Value did not change + Значение не изменилось Name - Name + Имя Count - Count + Количество Count is the name of a plot y-axis label Length - Length + Длина Length is the name of a plot y-axis label Value - Value + Значение Value is the name of a plot y-axis label Value - Value + Значение replica set - replica set + набор реплик {0} (replica of {1}) - {0} (replica of {1}) + {0} (реплика {1}) {0} is a resource name, {1} is a replica set name Select a resource - Select a resource + Выберите ресурс Select an application - Select an application + Выберите приложение Duration <strong>{0}</strong> - Duration <strong>{0}</strong> + Длительность: <strong>{0}</strong> {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified Service <strong>{0}</strong> - Service <strong>{0}</strong> + Служба <strong>{0}</strong> {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified Start time <strong>{0}</strong> - Start time <strong>{0}</strong> + <strong>Время начала: </strong> {0} {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified Time offset - Time offset + Смещение времени Timestamp - Timestamp + Метка времени View - View + Просмотр View logs - View logs + Просмотреть журналы Close - Close + Закрыть Split horizontal - Split horizontal + Разделить по горизонтали Split vertical - Split vertical + Разделить по вертикали Total: <strong>{0} results found</strong> - Total: <strong>{0} results found</strong> + Всего найдено результатов: <strong>{0}</strong> {0} is a number. This is raw markup, so <strong> and </strong> should not be modified diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.tr.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.tr.xlf index c0eafa08ff3..00f63e3764a 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.tr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.tr.xlf @@ -4,262 +4,262 @@ All - All + Tümü All tags - All tags + Tüm etiketler Filtered tags - Filtered tags + Filtrelenmiş etiketler Filters - Filters + Filtreler Graph. For an accessible view please navigate to the Table tab - Graph. For an accessible view please navigate to the Table tab + Graph. Erişilebilir bir görünüm için lütfen Tablo sekmesine gidin Graph - Graph + Graf (None) - (None) + (Yok) Options - Options + Seçenekler Select filters - Select filters + Filtreleri seçin Show count - Show count + Sayıyı göster Table - Table + Tablo Unable to display chart - Unable to display chart + Liste görüntülenemiyor Details - Details + Ayrıntılar Duration - Duration + Süre Event - Event + Olay Filter... - Filter... + Filtrele... Show all - Show all + Tümünü göster Show spec only - Show spec only + Yalnızca belirtimi göster Spec means from the resource specification Hide values - Hide values + Değerleri gizle Show values - Show values + Değerleri göster Copied! - Copied! + Kopyalandı! Copy to clipboard - Copy to clipboard + Panoya kopyala Hide value - Hide value + Değeri gizle Show value - Show value + Değeri göster Loading... - Loading... + Yükleniyor... No metrics data found - No metrics data found + Ölçüm verisi bulunamadı Show latest 10 values - Show latest 10 values + Son 10 değeri göster Only show value updates - Only show value updates + Yalnızca değer güncelleştirmelerini göster Time - Time + Zaman Value decreased - Value decreased + Değer azaltıldı Value increased - Value increased + Değer artırıldı Value did not change - Value did not change + Değer değişmedi Name - Name + Ad Count - Count + Sayı Count is the name of a plot y-axis label Length - Length + Uzunluk Length is the name of a plot y-axis label Value - Value + Değer Value is the name of a plot y-axis label Value - Value + Değer replica set - replica set + çoğaltma kümesi {0} (replica of {1}) - {0} (replica of {1}) + {0} ({1} çoğaltması) {0} is a resource name, {1} is a replica set name Select a resource - Select a resource + Kaynak seçin Select an application - Select an application + Uygulama seçin Duration <strong>{0}</strong> - Duration <strong>{0}</strong> + Süre <strong>{0}</strong> {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified Service <strong>{0}</strong> - Service <strong>{0}</strong> + Hizmet <strong>{0}</strong> {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified Start time <strong>{0}</strong> - Start time <strong>{0}</strong> + Başlangıç zamanı <strong>{0}</strong> {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified Time offset - Time offset + Zaman farkı Timestamp - Timestamp + Zaman damgası View - View + Görüntüle View logs - View logs + Günlükleri görüntüle Close - Close + Kapat Split horizontal - Split horizontal + Yatay bölme Split vertical - Split vertical + Dikey bölme Total: <strong>{0} results found</strong> - Total: <strong>{0} results found</strong> + Toplam: <strong>{0} sonuç bulundu</strong> {0} is a number. This is raw markup, so <strong> and </strong> should not be modified diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.zh-Hans.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.zh-Hans.xlf index c7c2ab4a4bc..17122aae01e 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.zh-Hans.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.zh-Hans.xlf @@ -4,262 +4,262 @@ All - All + 所有 All tags - All tags + 所有标记 Filtered tags - Filtered tags + 已筛选的标记 Filters - Filters + 筛选器 Graph. For an accessible view please navigate to the Table tab - Graph. For an accessible view please navigate to the Table tab + Graph。有关可访问的视图,请导航到“表”选项卡 Graph - Graph + Graph (None) - (None) + (无) Options - Options + 选项 Select filters - Select filters + 选择筛选器 Show count - Show count + 显示计数 Table - Table + Unable to display chart - Unable to display chart + 无法显示图表 Details - Details + 详细信息 Duration - Duration + 持续时间 Event - Event + 事件 Filter... - Filter... + 筛选... Show all - Show all + 全部显示 Show spec only - Show spec only + 仅显示规范 Spec means from the resource specification Hide values - Hide values + 隐藏值 Show values - Show values + 显示值 Copied! - Copied! + 已复制! Copy to clipboard - Copy to clipboard + 复制到剪贴板 Hide value - Hide value + 隐藏值 Show value - Show value + 显示值 Loading... - Loading... + 正在加载... No metrics data found - No metrics data found + 未找到指标数据 Show latest 10 values - Show latest 10 values + 显示最新的 10 个值 Only show value updates - Only show value updates + 仅显示值更新 Time - Time + 时间 Value decreased - Value decreased + 已减小值 Value increased - Value increased + 已增加值 Value did not change - Value did not change + 值未更改 Name - Name + 名称 Count - Count + 计数 Count is the name of a plot y-axis label Length - Length + 长度 Length is the name of a plot y-axis label Value - Value + Value is the name of a plot y-axis label Value - Value + replica set - replica set + 副本集 {0} (replica of {1}) - {0} (replica of {1}) + {0} ({1} 的副本) {0} is a resource name, {1} is a replica set name Select a resource - Select a resource + 选择资源 Select an application - Select an application + 选择应用程序 Duration <strong>{0}</strong> - Duration <strong>{0}</strong> + 持续时间 <strong>{0}</strong> {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified Service <strong>{0}</strong> - Service <strong>{0}</strong> + 服务 <strong>{0}</strong> {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified Start time <strong>{0}</strong> - Start time <strong>{0}</strong> + 开始时间 <strong>{0}</strong> {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified Time offset - Time offset + 时间偏移 Timestamp - Timestamp + 时间戳 View - View + 查看 View logs - View logs + 查看日志 Close - Close + 关闭 Split horizontal - Split horizontal + 水平拆分 Split vertical - Split vertical + 垂直拆分 Total: <strong>{0} results found</strong> - Total: <strong>{0} results found</strong> + 总计: 找到<strong>{0} 个结果</strong> {0} is a number. This is raw markup, so <strong> and </strong> should not be modified diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.zh-Hant.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.zh-Hant.xlf index d9d34eb17c1..1d10f0103e9 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.zh-Hant.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.zh-Hant.xlf @@ -4,262 +4,262 @@ All - All + 全部 All tags - All tags + 所有標籤 Filtered tags - Filtered tags + 已篩選的標籤 Filters - Filters + 篩選 Graph. For an accessible view please navigate to the Table tab - Graph. For an accessible view please navigate to the Table tab + 圖表如需可存取的檢視,請瀏覽至 [資料表] 索引標籤 Graph - Graph + 圖表 (None) - (None) + (無) Options - Options + 選項 Select filters - Select filters + 選取篩選 Show count - Show count + 顯示計數 Table - Table + 資料表 Unable to display chart - Unable to display chart + 無法顯示圖表 Details - Details + 詳細資料 Duration - Duration + 持續時間 Event - Event + 事件 Filter... - Filter... + 篩選... Show all - Show all + 全部顯示 Show spec only - Show spec only + 僅顯示規格 Spec means from the resource specification Hide values - Hide values + 隱藏值 Show values - Show values + 顯示值 Copied! - Copied! + 已複製! Copy to clipboard - Copy to clipboard + 複製至剪貼簿 Hide value - Hide value + 隱藏值 Show value - Show value + 顯示值 Loading... - Loading... + 正在載入... No metrics data found - No metrics data found + 未找到計量資料 Show latest 10 values - Show latest 10 values + 顯示最新的 10 個值 Only show value updates - Only show value updates + 僅顯示值更新 Time - Time + 時間 Value decreased - Value decreased + 值已減少 Value increased - Value increased + 值已增加 Value did not change - Value did not change + 值未變更 Name - Name + 名稱 Count - Count + 計數 Count is the name of a plot y-axis label Length - Length + 長度 Length is the name of a plot y-axis label Value - Value + Value is the name of a plot y-axis label Value - Value + replica set - replica set + 複本集 {0} (replica of {1}) - {0} (replica of {1}) + {0} ({1} 的複本) {0} is a resource name, {1} is a replica set name Select a resource - Select a resource + 選取資源 Select an application - Select an application + 選取應用程式 Duration <strong>{0}</strong> - Duration <strong>{0}</strong> + 期間 <strong>{0}</strong> {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified Service <strong>{0}</strong> - Service <strong>{0}</strong> + 服務 <strong>{0}</strong> {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified Start time <strong>{0}</strong> - Start time <strong>{0}</strong> + 開始時間 <strong>{0}</strong> {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified Time offset - Time offset + 時間位移 Timestamp - Timestamp + 時間戳記 View - View + 檢視 View logs - View logs + 檢視記錄 Close - Close + 關閉 Split horizontal - Split horizontal + 水平分割 Split vertical - Split vertical + 垂直分割 Total: <strong>{0} results found</strong> - Total: <strong>{0} results found</strong> + 總共<strong>找到了 {0} 個結果</strong> {0} is a number. This is raw markup, so <strong> and </strong> should not be modified diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.cs.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.cs.xlf index 736f93659b2..6177d18476b 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.cs.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.cs.xlf @@ -4,142 +4,142 @@ Apply filter - Apply filter + Použít filtr Cancel - Cancel + Zrušit Select a filter condition - Select a filter condition + Vyberte podmínku filtru. Field - Field + Pole Remove filter - Remove filter + Odebrat filtr Value - Value + Hodnota Site-wide navigation - Site-wide navigation + Navigace na úrovni webu Page navigation - Page navigation + Navigace po stránkách Panels - Panels + Panely Decrease panel size - Decrease panel size + Zmenšit velikost panelu Go to Microsoft Learn documentation - Go to Microsoft Learn documentation + Přejít na dokumentaci Microsoft Learn Go to Console Logs - Go to Console Logs + Přejít na Protokoly konzoly Go to Help - Go to Help + Přejít na Nápověda Go to Metrics - Go to Metrics + Přejít na Metriky Go to Resources - Go to Resources + Přejít na Zdroje informací Go to Settings - Go to Settings + Přejít na Nastavení Go to Structured Logs - Go to Structured Logs + Přejít na Strukturované protokoly Go to Traces - Go to Traces + Přejít na Trasování Increase panel size - Increase panel size + Zvětšit velikost panelu Keyboard Shortcuts - Keyboard Shortcuts + Klávesové zkratky Reset panel sizes - Reset panel sizes + Obnovit velikosti panelu Close panel - Close panel + Zavřít panel Toggle panel orientation - Toggle panel orientation + Přepnout orientaci panelu Dark - Dark + Tmavý Light - Light + Světlý System - System + Systém Theme - Theme + Motiv Version: {0} - Version: {0} + Verze: {0} {0} is the dashboard version string diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.de.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.de.xlf index 179ea269b69..9d05395d5f1 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.de.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.de.xlf @@ -4,142 +4,142 @@ Apply filter - Apply filter + Filter anwenden Cancel - Cancel + Abbrechen Select a filter condition - Select a filter condition + Filterbedingung auswählen Field - Field + Feld Remove filter - Remove filter + Filter entfernen Value - Value + Wert Site-wide navigation - Site-wide navigation + Websiteweite Navigation Page navigation - Page navigation + Seitennavigation Panels - Panels + Bereiche Decrease panel size - Decrease panel size + Bereichsgröße verringern Go to Microsoft Learn documentation - Go to Microsoft Learn documentation + Zur Microsoft Learn-Dokumentation wechseln Go to Console Logs - Go to Console Logs + Zu Konsolenprotokollen wechseln Go to Help - Go to Help + Zur Hilfe wechseln Go to Metrics - Go to Metrics + Zu Metriken wechseln Go to Resources - Go to Resources + Zu Ressourcen wechseln Go to Settings - Go to Settings + Zu Einstellungen wechseln Go to Structured Logs - Go to Structured Logs + Zu strukturierten Protokollen wechseln Go to Traces - Go to Traces + Zu Ablaufverfolgungen wechseln Increase panel size - Increase panel size + Bereichsgröße erhöhen Keyboard Shortcuts - Keyboard Shortcuts + Tastenkombinationen Reset panel sizes - Reset panel sizes + Bereichsgrößen zurücksetzen Close panel - Close panel + Bereich schließen Toggle panel orientation - Toggle panel orientation + Bereichsausrichtung umschalten Dark - Dark + Dunkel Light - Light + Hell System - System + System Theme - Theme + Design Version: {0} - Version: {0} + Version: {0} {0} is the dashboard version string diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.es.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.es.xlf index 9c24cab6bdd..944a526702c 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.es.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.es.xlf @@ -4,142 +4,142 @@ Apply filter - Apply filter + Aplicar filtro Cancel - Cancel + Cancelar Select a filter condition - Select a filter condition + Seleccionar una condición de filtro Field - Field + Campo Remove filter - Remove filter + Quitar filtro Value - Value + Valor Site-wide navigation - Site-wide navigation + Navegación en todo el sitio Page navigation - Page navigation + Navegación de páginas Panels - Panels + Paneles Decrease panel size - Decrease panel size + Reducir el tamaño del panel Go to Microsoft Learn documentation - Go to Microsoft Learn documentation + Ir a la documentación de Microsoft Learn Go to Console Logs - Go to Console Logs + Ir a Registros de consola Go to Help - Go to Help + Ir a Ayuda Go to Metrics - Go to Metrics + Ir a Métricas Go to Resources - Go to Resources + Ir a Recursos Go to Settings - Go to Settings + Ir a Configuración Go to Structured Logs - Go to Structured Logs + Ir a Registros estructurados Go to Traces - Go to Traces + Ir a Seguimientos Increase panel size - Increase panel size + Aumentar el tamaño del panel Keyboard Shortcuts - Keyboard Shortcuts + Métodos abreviados de teclado Reset panel sizes - Reset panel sizes + Restablecer tamaños del panel Close panel - Close panel + Cerrar panel Toggle panel orientation - Toggle panel orientation + Alternar orientación del panel Dark - Dark + Oscuro Light - Light + Claro System - System + Sistema Theme - Theme + Tema Version: {0} - Version: {0} + Versión: {0} {0} is the dashboard version string diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.fr.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.fr.xlf index 8f38710a06c..55bed021abb 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.fr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.fr.xlf @@ -4,142 +4,142 @@ Apply filter - Apply filter + Appliquer le filtre Cancel - Cancel + Annuler Select a filter condition - Select a filter condition + Sélectionner une condition de filtre Field - Field + Champ Remove filter - Remove filter + Supprimer le filtre Value - Value + Valeur Site-wide navigation - Site-wide navigation + Navigation à l’échelle du site Page navigation - Page navigation + Navigation entre les pages Panels - Panels + Panneaux Decrease panel size - Decrease panel size + Diminuer la taille du panneau Go to Microsoft Learn documentation - Go to Microsoft Learn documentation + Accédez à la documentation Microsoft Learn Go to Console Logs - Go to Console Logs + Accéder aux journaux de console Go to Help - Go to Help + Accéder à l’aide Go to Metrics - Go to Metrics + Accéder aux métriques Go to Resources - Go to Resources + Accéder aux ressources Go to Settings - Go to Settings + Accéder aux paramètres Go to Structured Logs - Go to Structured Logs + Accéder aux journaux structurés Go to Traces - Go to Traces + Accéder aux traces Increase panel size - Increase panel size + Augmenter la taille du panneau Keyboard Shortcuts - Keyboard Shortcuts + Raccourcis clavier Reset panel sizes - Reset panel sizes + Réinitialiser les tailles de panneau Close panel - Close panel + Fermer le panneau Toggle panel orientation - Toggle panel orientation + Activer/désactiver l’orientation du panneau Dark - Dark + Sombre Light - Light + Clair System - System + Système Theme - Theme + Thème Version: {0} - Version: {0} + Version : {0} {0} is the dashboard version string diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.it.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.it.xlf index ea2c870f7f0..6881fd5147f 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.it.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.it.xlf @@ -4,142 +4,142 @@ Apply filter - Apply filter + Applica filtro Cancel - Cancel + Annulla Select a filter condition - Select a filter condition + Seleziona una condizione di filtro Field - Field + Campo Remove filter - Remove filter + Rimuovi filtro Value - Value + Valore Site-wide navigation - Site-wide navigation + Spostamento a livello di sito Page navigation - Page navigation + Spostamento tra le pagine Panels - Panels + Pannelli Decrease panel size - Decrease panel size + Riduci dimensioni del pannello Go to Microsoft Learn documentation - Go to Microsoft Learn documentation + Vai alla documentazione di Microsoft Learn Go to Console Logs - Go to Console Logs + Vai a Log della console Go to Help - Go to Help + Vai a Guida Go to Metrics - Go to Metrics + Vai a Metriche Go to Resources - Go to Resources + Vai a Risorse Go to Settings - Go to Settings + Vai a Impostazioni Go to Structured Logs - Go to Structured Logs + Vai a Log strutturati Go to Traces - Go to Traces + Vai a Tracce Increase panel size - Increase panel size + Aumenta dimensioni del pannello Keyboard Shortcuts - Keyboard Shortcuts + Scelte rapide da tastiera Reset panel sizes - Reset panel sizes + Ripristina dimensioni del pannello Close panel - Close panel + Chiudi pannello Toggle panel orientation - Toggle panel orientation + Attiva/Disattiva orientamento del pannello Dark - Dark + Scuro Light - Light + Chiaro System - System + Sistema Theme - Theme + Tema Version: {0} - Version: {0} + Versione: {0} {0} is the dashboard version string diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.ja.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.ja.xlf index 693442c451a..689fa453aad 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.ja.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.ja.xlf @@ -4,142 +4,142 @@ Apply filter - Apply filter + フィルターの適用 Cancel - Cancel + キャンセル Select a filter condition - Select a filter condition + フィルター条件の選択 Field - Field + フィールド Remove filter - Remove filter + フィルターの削除 Value - Value + Site-wide navigation - Site-wide navigation + サイト全体のナビゲーション Page navigation - Page navigation + ページの移動 Panels - Panels + パネル Decrease panel size - Decrease panel size + パネルのサイズを小さくする Go to Microsoft Learn documentation - Go to Microsoft Learn documentation + Microsoft Learn ドキュメントに移動 Go to Console Logs - Go to Console Logs + コンソール ログに移動 Go to Help - Go to Help + ヘルプに移動 Go to Metrics - Go to Metrics + メトリックに移動 Go to Resources - Go to Resources + リソースに移動 Go to Settings - Go to Settings + 設定に移動 Go to Structured Logs - Go to Structured Logs + 構造化ログに移動 Go to Traces - Go to Traces + トレースに移動 Increase panel size - Increase panel size + パネルのサイズを大きくする Keyboard Shortcuts - Keyboard Shortcuts + キーボード ショートカット Reset panel sizes - Reset panel sizes + パネル サイズのリセット Close panel - Close panel + パネルを閉じる Toggle panel orientation - Toggle panel orientation + パネルの向きを切り替える Dark - Dark + ダーク Light - Light + ライト System - System + システム Theme - Theme + テーマ Version: {0} - Version: {0} + バージョン: {0} {0} is the dashboard version string diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.ko.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.ko.xlf index 51389c28450..fc1037b4e55 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.ko.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.ko.xlf @@ -4,142 +4,142 @@ Apply filter - Apply filter + 필터 적용 Cancel - Cancel + 취소 Select a filter condition - Select a filter condition + 필터 조건 선택 Field - Field + 필드 Remove filter - Remove filter + 필터 제거 Value - Value + Site-wide navigation - Site-wide navigation + 사이트 전체 탐색 Page navigation - Page navigation + 페이지 탐색 Panels - Panels + 패널 Decrease panel size - Decrease panel size + 패널 크기 줄이기 Go to Microsoft Learn documentation - Go to Microsoft Learn documentation + Microsoft Learn 설명서로 이동 Go to Console Logs - Go to Console Logs + 콘솔 로그로 이동 Go to Help - Go to Help + 도움말로 이동 Go to Metrics - Go to Metrics + 메트릭으로 이동 Go to Resources - Go to Resources + 리소스로 이동 Go to Settings - Go to Settings + 설정으로 이동 Go to Structured Logs - Go to Structured Logs + 구조화된 로그로 이동 Go to Traces - Go to Traces + 추적으로 이동 Increase panel size - Increase panel size + 패널 크기 늘리기 Keyboard Shortcuts - Keyboard Shortcuts + 바로 가기 키 Reset panel sizes - Reset panel sizes + 패널 크기 다시 설정 Close panel - Close panel + 패널 닫기 Toggle panel orientation - Toggle panel orientation + 패널 방향 전환 Dark - Dark + 어두움 Light - Light + 밝게 System - System + 시스템 Theme - Theme + 테마 Version: {0} - Version: {0} + 버전: {0} {0} is the dashboard version string diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.pl.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.pl.xlf index e97a52dd5fd..45132185891 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.pl.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.pl.xlf @@ -4,142 +4,142 @@ Apply filter - Apply filter + Zastosuj filtr Cancel - Cancel + Anuluj Select a filter condition - Select a filter condition + Wybierz warunek filtru Field - Field + Pole Remove filter - Remove filter + Usuń filtr Value - Value + Wartość Site-wide navigation - Site-wide navigation + Nawigacja w całej witrynie Page navigation - Page navigation + Nawigacja między stronami Panels - Panels + Panele Decrease panel size - Decrease panel size + Zmniejsz rozmiar panelu Go to Microsoft Learn documentation - Go to Microsoft Learn documentation + Przejdź do dokumentacji usługi Microsoft Learn Go to Console Logs - Go to Console Logs + Przejdź do dzienników konsoli Go to Help - Go to Help + Przejdź do Pomocy Go to Metrics - Go to Metrics + Przejdź do metryk Go to Resources - Go to Resources + Przejdź do zasobów Go to Settings - Go to Settings + Przejdź do ustawień Go to Structured Logs - Go to Structured Logs + Przejdź do dzienników strukturalnych Go to Traces - Go to Traces + Przejdź do obszaru Ślady Increase panel size - Increase panel size + Zwiększ rozmiar panelu Keyboard Shortcuts - Keyboard Shortcuts + Skróty klawiaturowe Reset panel sizes - Reset panel sizes + Resetuj rozmiary paneli Close panel - Close panel + Zamknij panel Toggle panel orientation - Toggle panel orientation + Przełącz orientację panelu Dark - Dark + Ciemny Light - Light + Jasny System - System + System Theme - Theme + Motyw Version: {0} - Version: {0} + Wersja: {0} {0} is the dashboard version string diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.pt-BR.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.pt-BR.xlf index 02f57207bbf..80b63421fa9 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.pt-BR.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.pt-BR.xlf @@ -4,142 +4,142 @@ Apply filter - Apply filter + Aplicar filtro Cancel - Cancel + Cancelar Select a filter condition - Select a filter condition + Selecionar uma condição de filtro Field - Field + Campo Remove filter - Remove filter + Remover filtro Value - Value + Valor Site-wide navigation - Site-wide navigation + Navegação em todo o site Page navigation - Page navigation + Navegação na página Panels - Panels + Painéis Decrease panel size - Decrease panel size + Diminuir tamanho do painel Go to Microsoft Learn documentation - Go to Microsoft Learn documentation + Ir para a documentação do Microsoft Learn Go to Console Logs - Go to Console Logs + Ir para Logs do Console Go to Help - Go to Help + Ir para Ajuda Go to Metrics - Go to Metrics + Ir para Métricas Go to Resources - Go to Resources + Ir para Recursos Go to Settings - Go to Settings + Ir para Configurações Go to Structured Logs - Go to Structured Logs + Ir para Logs Estruturados Go to Traces - Go to Traces + Ir para Rastreamentos Increase panel size - Increase panel size + Aumentar o tamanho do painel Keyboard Shortcuts - Keyboard Shortcuts + Atalhos de Teclado Reset panel sizes - Reset panel sizes + Redefinir tamanhos do painel Close panel - Close panel + Fechar o painel Toggle panel orientation - Toggle panel orientation + Alternar orientação do painel Dark - Dark + Escuro Light - Light + Claro System - System + Sistema Theme - Theme + Tema Version: {0} - Version: {0} + Versão: {0} {0} is the dashboard version string diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.ru.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.ru.xlf index 9b79c108082..c0125d4a839 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.ru.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.ru.xlf @@ -4,142 +4,142 @@ Apply filter - Apply filter + Применить фильтр Cancel - Cancel + Отмена Select a filter condition - Select a filter condition + Выберите условие фильтра Field - Field + Поле Remove filter - Remove filter + Удалить фильтр Value - Value + Значение Site-wide navigation - Site-wide navigation + Навигация на уровне сайта Page navigation - Page navigation + Перемещение по страницам Panels - Panels + Панели Decrease panel size - Decrease panel size + Уменьшить размер панели Go to Microsoft Learn documentation - Go to Microsoft Learn documentation + Перейти к документации Microsoft Learn Go to Console Logs - Go to Console Logs + Перейти к журналам консоли Go to Help - Go to Help + Перейти к справке Go to Metrics - Go to Metrics + Перейти к метрикам Go to Resources - Go to Resources + Перейти к ресурсам Go to Settings - Go to Settings + Перейти к параметрам Go to Structured Logs - Go to Structured Logs + Перейти к структурированным журналам Go to Traces - Go to Traces + Перейти к трассировкам Increase panel size - Increase panel size + Увеличить размер панели Keyboard Shortcuts - Keyboard Shortcuts + Сочетания клавиш Reset panel sizes - Reset panel sizes + Сбросить размеры панели Close panel - Close panel + Закрыть панель Toggle panel orientation - Toggle panel orientation + Переключить ориентацию панели Dark - Dark + Темная Light - Light + Светлая System - System + Система Theme - Theme + Тема Version: {0} - Version: {0} + Версия: {0} {0} is the dashboard version string diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.tr.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.tr.xlf index f75e2fc4b32..d4593a23dda 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.tr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.tr.xlf @@ -4,142 +4,142 @@ Apply filter - Apply filter + Filtre uygula Cancel - Cancel + İptal Select a filter condition - Select a filter condition + Filtre koşulu seçin Field - Field + Alan Remove filter - Remove filter + Filtreyi kaldır Value - Value + Değer Site-wide navigation - Site-wide navigation + Site genelinde gezinti Page navigation - Page navigation + Sayfa gezintisi Panels - Panels + Paneller Decrease panel size - Decrease panel size + Panel boyutunu küçült Go to Microsoft Learn documentation - Go to Microsoft Learn documentation + Microsoft Learn belgelerine git Go to Console Logs - Go to Console Logs + Konsol Günlükleri'ne Git Go to Help - Go to Help + Yardım'a git Go to Metrics - Go to Metrics + Ölçümlere Git Go to Resources - Go to Resources + Kaynaklar'a git Go to Settings - Go to Settings + Ayarlar'a git Go to Structured Logs - Go to Structured Logs + Yapılandırılmış Günlükler'e git Go to Traces - Go to Traces + İzlemelere Git Increase panel size - Increase panel size + Panel boyutunu artır Keyboard Shortcuts - Keyboard Shortcuts + Klavye Kısayolları Reset panel sizes - Reset panel sizes + Panel boyutlarını sıfırla Close panel - Close panel + Paneli kapat Toggle panel orientation - Toggle panel orientation + Panelin yönünü değiştir Dark - Dark + Koyu Light - Light + Açık System - System + Sistem Theme - Theme + Tema Version: {0} - Version: {0} + Sürüm: {0} {0} is the dashboard version string diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.zh-Hans.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.zh-Hans.xlf index 1cef857a172..c0e48435d03 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.zh-Hans.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.zh-Hans.xlf @@ -4,142 +4,142 @@ Apply filter - Apply filter + 应用筛选器 Cancel - Cancel + 取消 Select a filter condition - Select a filter condition + 选择筛选条件 Field - Field + 字段 Remove filter - Remove filter + 移除筛选器 Value - Value + Site-wide navigation - Site-wide navigation + 网站范围导航 Page navigation - Page navigation + 网页导航 Panels - Panels + 面板 Decrease panel size - Decrease panel size + 减小面板大小 Go to Microsoft Learn documentation - Go to Microsoft Learn documentation + 转到 Microsoft Learn 文档 Go to Console Logs - Go to Console Logs + 转到“控制台日志” Go to Help - Go to Help + 转到“帮助” Go to Metrics - Go to Metrics + 转到“指标” Go to Resources - Go to Resources + 转到“资源” Go to Settings - Go to Settings + 转到“设置” Go to Structured Logs - Go to Structured Logs + 转到“结构化日志” Go to Traces - Go to Traces + 转到“跟踪” Increase panel size - Increase panel size + 增加面板大小 Keyboard Shortcuts - Keyboard Shortcuts + 键盘快捷方式 Reset panel sizes - Reset panel sizes + 重置面板大小 Close panel - Close panel + 关闭面板 Toggle panel orientation - Toggle panel orientation + 切换面板方向 Dark - Dark + 深色 Light - Light + 浅色 System - System + 系统 Theme - Theme + 主题 Version: {0} - Version: {0} + 版本: {0} {0} is the dashboard version string diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.zh-Hant.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.zh-Hant.xlf index 7b9d8af9fdd..22d404faa96 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.zh-Hant.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.zh-Hant.xlf @@ -4,142 +4,142 @@ Apply filter - Apply filter + 套用篩選 Cancel - Cancel + 取消 Select a filter condition - Select a filter condition + 選取篩選條件 Field - Field + 欄位 Remove filter - Remove filter + 移除篩選 Value - Value + Site-wide navigation - Site-wide navigation + 全網站瀏覽 Page navigation - Page navigation + 頁面瀏覽 Panels - Panels + 面板 Decrease panel size - Decrease panel size + 減少面板大小 Go to Microsoft Learn documentation - Go to Microsoft Learn documentation + 前往 Microsoft Learn 文件 Go to Console Logs - Go to Console Logs + 前往 [主控台記錄] Go to Help - Go to Help + 前往 [說明] Go to Metrics - Go to Metrics + 移至 [計量] Go to Resources - Go to Resources + 前往 [資源] Go to Settings - Go to Settings + 前往 [設定] Go to Structured Logs - Go to Structured Logs + 前往 [結構化記錄] Go to Traces - Go to Traces + 前往 [追蹤] Increase panel size - Increase panel size + 增加面板大小 Keyboard Shortcuts - Keyboard Shortcuts + 鍵盤快速鍵 Reset panel sizes - Reset panel sizes + 重設面板大小 Close panel - Close panel + 關閉面板 Toggle panel orientation - Toggle panel orientation + 切換面板方向 Dark - Dark + 深色 Light - Light + 淺色 System - System + 系統 Theme - Theme + 佈景主題 Version: {0} - Version: {0} + 版本: {0} {0} is the dashboard version string diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.cs.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.cs.xlf index 2e690405e73..16d0c7ef18d 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.cs.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.cs.xlf @@ -4,72 +4,72 @@ .NET Aspire - .NET Aspire + .NET Aspire Help - Help + Nápověda .NET Aspire repo - .NET Aspire repo + Úložiště .NET Aspire Launch settings - Launch settings + Nastavení pro spuštění Close - Close + Zavřít Settings - Settings + Nastavení An unhandled error has occurred. - An unhandled error has occurred. + Došlo k neošetřené chybě. Reload - Reload + Načíst znovu Console - Console + Konzola Metrics - Metrics + Metriky Monitoring - Monitoring + Monitorování Resources - Resources + Prostředky Structured - Structured + Strukturované Traces - Traces + Trasování diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.de.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.de.xlf index a97c67b4014..34f46721871 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.de.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.de.xlf @@ -4,72 +4,72 @@ .NET Aspire - .NET Aspire + .NET Aspire Help - Help + Hilfe .NET Aspire repo - .NET Aspire repo + .NET Aspire-Repository Launch settings - Launch settings + Starteinstellungen Close - Close + Schließen Settings - Settings + Einstellungen An unhandled error has occurred. - An unhandled error has occurred. + Es ist ein unbehandelter Fehler aufgetreten. Reload - Reload + Neu laden Console - Console + Konsole Metrics - Metrics + Metriken Monitoring - Monitoring + Überwachung Resources - Resources + Ressourcen Structured - Structured + Strukturiert Traces - Traces + Überwachungen diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.es.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.es.xlf index af2fe5d4de0..ec8f0fbc144 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.es.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.es.xlf @@ -4,72 +4,72 @@ .NET Aspire - .NET Aspire + .NET Aspire Help - Help + Ayuda .NET Aspire repo - .NET Aspire repo + Repositorio de .NET Aspire Launch settings - Launch settings + Configuración de inicio Close - Close + Cerrar Settings - Settings + Configuración An unhandled error has occurred. - An unhandled error has occurred. + Se ha producido un error no controlado. Reload - Reload + Recargar Console - Console + Consola Metrics - Metrics + Métricas Monitoring - Monitoring + Supervisión Resources - Resources + Recursos Structured - Structured + Estructurado Traces - Traces + Seguimientos diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.fr.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.fr.xlf index 2eed7b1ccc6..c7185bf2ca4 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.fr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.fr.xlf @@ -4,72 +4,72 @@ .NET Aspire - .NET Aspire + .NET Aspire Help - Help + Aide .NET Aspire repo - .NET Aspire repo + Référentiel .NET Aspire Launch settings - Launch settings + Paramètres de lancement Close - Close + Fermer Settings - Settings + Paramètres An unhandled error has occurred. - An unhandled error has occurred. + Une erreur non traitée s’est produite. Reload - Reload + Recharger Console - Console + Console Metrics - Metrics + Métriques Monitoring - Monitoring + Surveillance Resources - Resources + Ressources Structured - Structured + Structuré Traces - Traces + Traces diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.it.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.it.xlf index 230027a227e..ebee6cc99ad 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.it.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.it.xlf @@ -4,72 +4,72 @@ .NET Aspire - .NET Aspire + .NET Aspire Help - Help + Guida .NET Aspire repo - .NET Aspire repo + Repository .NET Aspire Launch settings - Launch settings + Impostazioni di avvio Close - Close + Chiudi Settings - Settings + Impostazioni An unhandled error has occurred. - An unhandled error has occurred. + Si è verificato un errore non gestito. Reload - Reload + Ricarica Console - Console + Console Metrics - Metrics + Metriche Monitoring - Monitoring + Monitoraggio Resources - Resources + Risorse Structured - Structured + Strutturato Traces - Traces + Tracce diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.ja.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.ja.xlf index 2b57f19eede..d030feb93ea 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.ja.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.ja.xlf @@ -4,72 +4,72 @@ .NET Aspire - .NET Aspire + .NET Aspire Help - Help + ヘルプ .NET Aspire repo - .NET Aspire repo + .NET Aspire リポジトリ Launch settings - Launch settings + 起動設定 Close - Close + 閉じる Settings - Settings + 設定 An unhandled error has occurred. - An unhandled error has occurred. + ハンドルされないエラーが発生しました。 Reload - Reload + 再読み込み Console - Console + コンソール Metrics - Metrics + メトリック Monitoring - Monitoring + モニター中 Resources - Resources + リソース Structured - Structured + 構造化 Traces - Traces + トレース diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.ko.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.ko.xlf index c63c4678462..d90d3c89d33 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.ko.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.ko.xlf @@ -4,72 +4,72 @@ .NET Aspire - .NET Aspire + .NET Aspire Help - Help + 도움말 .NET Aspire repo - .NET Aspire repo + .NET Aspire 리포지토리 Launch settings - Launch settings + 시작 설정 Close - Close + 닫기 Settings - Settings + 설정 An unhandled error has occurred. - An unhandled error has occurred. + 처리되지 않은 오류가 발생했습니다. Reload - Reload + 다시 로드 Console - Console + 콘솔 Metrics - Metrics + 메트릭 Monitoring - Monitoring + 모니터링 Resources - Resources + 리소스 Structured - Structured + 구조화 된 Traces - Traces + 추적 diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.pl.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.pl.xlf index 3fa5d33ce36..9cd5cf6f614 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.pl.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.pl.xlf @@ -4,72 +4,72 @@ .NET Aspire - .NET Aspire + Platforma .NET — Aspire Help - Help + Pomoc .NET Aspire repo - .NET Aspire repo + Repozytorium platformy .NET — Aspire Launch settings - Launch settings + Ustawienia uruchamiania Close - Close + Zamknij Settings - Settings + Ustawienia An unhandled error has occurred. - An unhandled error has occurred. + Wystąpił nieobsługiwany błąd. Reload - Reload + Załaduj ponownie Console - Console + Konsola Metrics - Metrics + Metryki Monitoring - Monitoring + Monitorowanie Resources - Resources + Zasoby Structured - Structured + Ustrukturyzowane Traces - Traces + Ślady diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.pt-BR.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.pt-BR.xlf index 77258e56423..d7391063163 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.pt-BR.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.pt-BR.xlf @@ -4,72 +4,72 @@ .NET Aspire - .NET Aspire + .NET Aspire Help - Help + Ajuda .NET Aspire repo - .NET Aspire repo + Repositório .NET Aspire Launch settings - Launch settings + Configurações de inicialização Close - Close + Fechar Settings - Settings + Configurações An unhandled error has occurred. - An unhandled error has occurred. + Ocorreu um erro sem tratamento. Reload - Reload + Recarregar Console - Console + Console Metrics - Metrics + Métricas Monitoring - Monitoring + Monitoramento Resources - Resources + Recursos Structured - Structured + Estruturado Traces - Traces + Rastreamentos diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.ru.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.ru.xlf index 4584a69b8d4..81a3304a2d5 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.ru.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.ru.xlf @@ -4,72 +4,72 @@ .NET Aspire - .NET Aspire + .NET Aspire Help - Help + Справка .NET Aspire repo - .NET Aspire repo + Репозиторий .NET Aspire Launch settings - Launch settings + Параметры запуска Close - Close + Закрыть Settings - Settings + Параметры An unhandled error has occurred. - An unhandled error has occurred. + Возникла необрабатываемая ошибка. Reload - Reload + Перезагрузить Console - Console + Консоль Metrics - Metrics + Метрики Monitoring - Monitoring + Мониторинг Resources - Resources + Ресурсы Structured - Structured + Структурированные Traces - Traces + Трассировки diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.tr.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.tr.xlf index e732ee49e08..353ea2a5e6a 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.tr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.tr.xlf @@ -4,72 +4,72 @@ .NET Aspire - .NET Aspire + .NET Aspire Help - Help + Yardım .NET Aspire repo - .NET Aspire repo + .NET Aspire depo Launch settings - Launch settings + Başlatma ayarları Close - Close + Kapat Settings - Settings + Ayarlar An unhandled error has occurred. - An unhandled error has occurred. + İşlenmemiş bir hata oluştu. Reload - Reload + Yeniden yükle Console - Console + Konsol Metrics - Metrics + Ölçümler Monitoring - Monitoring + İzleme Resources - Resources + Kaynaklar Structured - Structured + Yapılandırılmış Traces - Traces + İzlemeler diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.zh-Hans.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.zh-Hans.xlf index c65810722f0..670aad3d128 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.zh-Hans.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.zh-Hans.xlf @@ -4,72 +4,72 @@ .NET Aspire - .NET Aspire + .NET Aspire Help - Help + 帮助 .NET Aspire repo - .NET Aspire repo + .NET Aspire 存储库 Launch settings - Launch settings + 启动设置 Close - Close + 关闭 Settings - Settings + 设置 An unhandled error has occurred. - An unhandled error has occurred. + 出现未处理的错误。 Reload - Reload + 重新加载 Console - Console + 控制台 Metrics - Metrics + 指标 Monitoring - Monitoring + 监视 Resources - Resources + 资源 Structured - Structured + 结构化 Traces - Traces + 跟踪 diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.zh-Hant.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.zh-Hant.xlf index d337ad06b2a..c0113071cde 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.zh-Hant.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.zh-Hant.xlf @@ -4,72 +4,72 @@ .NET Aspire - .NET Aspire + .NET Aspire Help - Help + 說明 .NET Aspire repo - .NET Aspire repo + .NET Aspire 存放庫 Launch settings - Launch settings + 啟動設定 Close - Close + 關閉 Settings - Settings + 設定 An unhandled error has occurred. - An unhandled error has occurred. + 發生未處理的錯誤。 Reload - Reload + 重新載入 Console - Console + 主控台 Metrics - Metrics + 計量 Monitoring - Monitoring + 監視 Resources - Resources + 資源 Structured - Structured + 結構化 Traces - Traces + 追蹤 diff --git a/src/Aspire.Dashboard/Resources/xlf/Metrics.cs.xlf b/src/Aspire.Dashboard/Resources/xlf/Metrics.cs.xlf index 4e3fa1524c5..0e327136029 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Metrics.cs.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Metrics.cs.xlf @@ -4,87 +4,87 @@ Metrics - Metrics + Metriky Description - Description + Popis Instrument - Instrument + Nástroj Last 15 minutes - Last 15 minutes + Posledních 15 minut Last 5 minutes - Last 5 minutes + Posledních 5 minut Last hour - Last hour + Poslední hodina Last 1 minute - Last 1 minute + Poslední minuta Last 6 hours - Last 6 hours + Posledních 6 hodin Last 30 minutes - Last 30 minutes + Posledních 30 minut Last 3 hours - Last 3 hours + Poslední 3 hodiny Last 12 hours - Last 12 hours + Posledních 12 hodin Last 24 hours - Last 24 hours + Posledních 24 hodin No metrics for the selected resource - No metrics for the selected resource + Pro vybraný prostředek nejsou k dispozici žádné metriky. {0} metrics - {0} metrics + Metriky aplikace {0} {0} is an application name Select a duration - Select a duration + Vyberte dobu trvání Select a resource to view metrics - Select a resource to view metrics + Pokud si chcete zobrazit metriky, vyberte prostředek. Select instrument. - Select instrument. + Vyberte nástroj. diff --git a/src/Aspire.Dashboard/Resources/xlf/Metrics.de.xlf b/src/Aspire.Dashboard/Resources/xlf/Metrics.de.xlf index 941dc586327..0e1eb159751 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Metrics.de.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Metrics.de.xlf @@ -4,87 +4,87 @@ Metrics - Metrics + Metriken Description - Description + Beschreibung Instrument - Instrument + Instrumentieren Last 15 minutes - Last 15 minutes + Letzte 15 Minuten Last 5 minutes - Last 5 minutes + Letzte 5 Minuten Last hour - Last hour + Letzte Stunde Last 1 minute - Last 1 minute + Letzte 1 Minute Last 6 hours - Last 6 hours + Letzte 6 Stunden Last 30 minutes - Last 30 minutes + Letzte 30 Minuten Last 3 hours - Last 3 hours + Letzte 3 Stunden Last 12 hours - Last 12 hours + Letzte 12 Stunden Last 24 hours - Last 24 hours + Letzte 24 Stunden No metrics for the selected resource - No metrics for the selected resource + Keine Metriken für die ausgewählte Ressource {0} metrics - {0} metrics + {0} Metriken {0} is an application name Select a duration - Select a duration + Dauer auswählen Select a resource to view metrics - Select a resource to view metrics + Wählen Sie eine Ressource aus, um Metriken anzuzeigen. Select instrument. - Select instrument. + Instrument auswählen. diff --git a/src/Aspire.Dashboard/Resources/xlf/Metrics.es.xlf b/src/Aspire.Dashboard/Resources/xlf/Metrics.es.xlf index 0b7d680e663..8432b660205 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Metrics.es.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Metrics.es.xlf @@ -4,87 +4,87 @@ Metrics - Metrics + Métricas Description - Description + Descripción Instrument - Instrument + Instrumento Last 15 minutes - Last 15 minutes + Últimos 15 minutos Last 5 minutes - Last 5 minutes + Últimos 5 minutos Last hour - Last hour + Última hora Last 1 minute - Last 1 minute + Último minuto Last 6 hours - Last 6 hours + Últimas 6 horas Last 30 minutes - Last 30 minutes + Últimos 30 minutos Last 3 hours - Last 3 hours + Últimas 3 horas Last 12 hours - Last 12 hours + Últimas 12 horas Last 24 hours - Last 24 hours + Últimas 24 horas No metrics for the selected resource - No metrics for the selected resource + No hay métricas para el recurso seleccionado {0} metrics - {0} metrics + Métricas de {0} {0} is an application name Select a duration - Select a duration + Seleccionar una duración Select a resource to view metrics - Select a resource to view metrics + Seleccionar un recurso para ver las métricas Select instrument. - Select instrument. + Seleccione el instrumento. diff --git a/src/Aspire.Dashboard/Resources/xlf/Metrics.fr.xlf b/src/Aspire.Dashboard/Resources/xlf/Metrics.fr.xlf index 24e9154daf8..35ffec37545 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Metrics.fr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Metrics.fr.xlf @@ -4,87 +4,87 @@ Metrics - Metrics + Métriques Description - Description + Description Instrument - Instrument + Instrumenter Last 15 minutes - Last 15 minutes + 15 dernières minutes Last 5 minutes - Last 5 minutes + 5 dernières minutes Last hour - Last hour + Dernière heure Last 1 minute - Last 1 minute + Dernière minute Last 6 hours - Last 6 hours + 6 dernières heures Last 30 minutes - Last 30 minutes + 30 dernières minutes Last 3 hours - Last 3 hours + 3 dernières heures Last 12 hours - Last 12 hours + 12 dernières heures Last 24 hours - Last 24 hours + Dernières 24 heures No metrics for the selected resource - No metrics for the selected resource + Aucune métrique pour la ressource sélectionnée {0} metrics - {0} metrics + Métriques {0} {0} is an application name Select a duration - Select a duration + Sélectionner une durée Select a resource to view metrics - Select a resource to view metrics + Sélectionner une ressource pour afficher les métriques Select instrument. - Select instrument. + Sélectionnez un instrument. diff --git a/src/Aspire.Dashboard/Resources/xlf/Metrics.it.xlf b/src/Aspire.Dashboard/Resources/xlf/Metrics.it.xlf index a8707c5047c..13a27db7b4a 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Metrics.it.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Metrics.it.xlf @@ -4,87 +4,87 @@ Metrics - Metrics + Metriche Description - Description + Descrizione Instrument - Instrument + Strumento Last 15 minutes - Last 15 minutes + Ultimi 15 minuti Last 5 minutes - Last 5 minutes + Ultimi 5 minuti Last hour - Last hour + Ultima ora Last 1 minute - Last 1 minute + Ultimo minuto Last 6 hours - Last 6 hours + Ultime 6 ore Last 30 minutes - Last 30 minutes + Ultimi 30 minuti Last 3 hours - Last 3 hours + Ultime 3 ore Last 12 hours - Last 12 hours + Ultime 12 ore Last 24 hours - Last 24 hours + Ultime 24 ore No metrics for the selected resource - No metrics for the selected resource + Nessuna metrica disponibile per la risorsa selezionata {0} metrics - {0} metrics + Metriche {0} {0} is an application name Select a duration - Select a duration + Seleziona una durata Select a resource to view metrics - Select a resource to view metrics + Seleziona una risorsa per visualizzare le metriche Select instrument. - Select instrument. + Seleziona uno strumento. diff --git a/src/Aspire.Dashboard/Resources/xlf/Metrics.ja.xlf b/src/Aspire.Dashboard/Resources/xlf/Metrics.ja.xlf index a4e07d6d520..42a95640183 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Metrics.ja.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Metrics.ja.xlf @@ -4,87 +4,87 @@ Metrics - Metrics + メトリック Description - Description + 説明 Instrument - Instrument + インストルメント Last 15 minutes - Last 15 minutes + 直近 15 分間 Last 5 minutes - Last 5 minutes + 直近 5 分間 Last hour - Last hour + 過去 1 時間 Last 1 minute - Last 1 minute + 過去 1 分間 Last 6 hours - Last 6 hours + 過去 6 時間 Last 30 minutes - Last 30 minutes + 直近 30 分間 Last 3 hours - Last 3 hours + 過去 3 時間 Last 12 hours - Last 12 hours + 過去 12 時間 Last 24 hours - Last 24 hours + 過去 24 時間 No metrics for the selected resource - No metrics for the selected resource + 選択したリソースのメトリックがありません {0} metrics - {0} metrics + {0} のメトリック {0} is an application name Select a duration - Select a duration + 期間を選択する Select a resource to view metrics - Select a resource to view metrics + メトリックを表示するリソースの選択 Select instrument. - Select instrument. + インストルメントを選択します。 diff --git a/src/Aspire.Dashboard/Resources/xlf/Metrics.ko.xlf b/src/Aspire.Dashboard/Resources/xlf/Metrics.ko.xlf index 16ab03ff21d..9b190f929fa 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Metrics.ko.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Metrics.ko.xlf @@ -4,87 +4,87 @@ Metrics - Metrics + 메트릭 Description - Description + 설명 Instrument - Instrument + 계측 Last 15 minutes - Last 15 minutes + 지난 15분 Last 5 minutes - Last 5 minutes + 지난 5분 Last hour - Last hour + 지난 시간 Last 1 minute - Last 1 minute + 지난 1분 Last 6 hours - Last 6 hours + 지난 6시간 Last 30 minutes - Last 30 minutes + 지난 30분 Last 3 hours - Last 3 hours + 지난 3시간 Last 12 hours - Last 12 hours + 지난 12시간 Last 24 hours - Last 24 hours + 지난 24시간 No metrics for the selected resource - No metrics for the selected resource + 선택한 리소스에 대한 메트릭 없음 {0} metrics - {0} metrics + {0} 메트릭 {0} is an application name Select a duration - Select a duration + 기간 선택 Select a resource to view metrics - Select a resource to view metrics + 메트릭을 볼 리소스 선택 Select instrument. - Select instrument. + 계측을 선택합니다. diff --git a/src/Aspire.Dashboard/Resources/xlf/Metrics.pl.xlf b/src/Aspire.Dashboard/Resources/xlf/Metrics.pl.xlf index 4c694fc44dc..d6a42f47857 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Metrics.pl.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Metrics.pl.xlf @@ -4,87 +4,87 @@ Metrics - Metrics + Metryki Description - Description + Opis Instrument - Instrument + Instrument Last 15 minutes - Last 15 minutes + Ostatnie 15 minut Last 5 minutes - Last 5 minutes + Ostatnie 5 minut Last hour - Last hour + Ostatnia godzina Last 1 minute - Last 1 minute + Ostatnia 1 minuta Last 6 hours - Last 6 hours + Ostatnie 6 godzin Last 30 minutes - Last 30 minutes + Ostatnie 30 minut Last 3 hours - Last 3 hours + Ostatnie 3 godziny Last 12 hours - Last 12 hours + Ostatnie 12 godzin Last 24 hours - Last 24 hours + Ostatnie 24 godziny No metrics for the selected resource - No metrics for the selected resource + Brak metryk dla wybranego zasobu {0} metrics - {0} metrics + Metryki {0} {0} is an application name Select a duration - Select a duration + Wybierz czas trwania Select a resource to view metrics - Select a resource to view metrics + Wybierz zasób, aby wyświetlić metryki Select instrument. - Select instrument. + Wybierz instrument. diff --git a/src/Aspire.Dashboard/Resources/xlf/Metrics.pt-BR.xlf b/src/Aspire.Dashboard/Resources/xlf/Metrics.pt-BR.xlf index 92fbfa1439b..be08ecd8315 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Metrics.pt-BR.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Metrics.pt-BR.xlf @@ -4,87 +4,87 @@ Metrics - Metrics + Métricas Description - Description + Descrição Instrument - Instrument + Instrumento Last 15 minutes - Last 15 minutes + Últimos 15 minutos Last 5 minutes - Last 5 minutes + Últimos 5 minutos Last hour - Last hour + Última hora Last 1 minute - Last 1 minute + Último 1 minuto Last 6 hours - Last 6 hours + Últimas 6 horas Last 30 minutes - Last 30 minutes + Últimos 30 minutos Last 3 hours - Last 3 hours + Últimas 3 horas Last 12 hours - Last 12 hours + Últimas 12 horas Last 24 hours - Last 24 hours + Últimas 24 horas No metrics for the selected resource - No metrics for the selected resource + Nenhuma métrica para o recurso selecionado {0} metrics - {0} metrics + {0} métricas {0} is an application name Select a duration - Select a duration + Selecionar uma duração Select a resource to view metrics - Select a resource to view metrics + Selecionar um recurso para exibir métricas Select instrument. - Select instrument. + Selecionar instrumento. diff --git a/src/Aspire.Dashboard/Resources/xlf/Metrics.ru.xlf b/src/Aspire.Dashboard/Resources/xlf/Metrics.ru.xlf index 909f371b0fa..0d76acd777a 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Metrics.ru.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Metrics.ru.xlf @@ -4,87 +4,87 @@ Metrics - Metrics + Метрики Description - Description + Описание Instrument - Instrument + Инструмент Last 15 minutes - Last 15 minutes + Последние 15 минут Last 5 minutes - Last 5 minutes + Последние 5 минут Last hour - Last hour + Последний час Last 1 minute - Last 1 minute + Последняя минута Last 6 hours - Last 6 hours + Последние 6 часов Last 30 minutes - Last 30 minutes + Последние 30 минут Last 3 hours - Last 3 hours + Последние 3 часа Last 12 hours - Last 12 hours + Последние 12 часов Last 24 hours - Last 24 hours + Последние 24 часа No metrics for the selected resource - No metrics for the selected resource + Нет метрик для выбранного ресурса {0} metrics - {0} metrics + Метрик: {0} {0} is an application name Select a duration - Select a duration + Выберите длительность Select a resource to view metrics - Select a resource to view metrics + Выберите ресурс для просмотра метрик Select instrument. - Select instrument. + Выберите инструмент. diff --git a/src/Aspire.Dashboard/Resources/xlf/Metrics.tr.xlf b/src/Aspire.Dashboard/Resources/xlf/Metrics.tr.xlf index 039eab0a9a5..d6b00078102 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Metrics.tr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Metrics.tr.xlf @@ -4,87 +4,87 @@ Metrics - Metrics + Ölçümler Description - Description + Açıklama Instrument - Instrument + Araç Last 15 minutes - Last 15 minutes + Son 15 dakika Last 5 minutes - Last 5 minutes + Son 5 dakika Last hour - Last hour + Son saat Last 1 minute - Last 1 minute + Son 1 dakika Last 6 hours - Last 6 hours + Son 6 saat Last 30 minutes - Last 30 minutes + Son 30 dakika Last 3 hours - Last 3 hours + Son 3 saat Last 12 hours - Last 12 hours + Son 12 saat Last 24 hours - Last 24 hours + Son 24 saat No metrics for the selected resource - No metrics for the selected resource + Seçili kaynak için ölçüm yok {0} metrics - {0} metrics + {0} ölçümler {0} is an application name Select a duration - Select a duration + Süre seçin Select a resource to view metrics - Select a resource to view metrics + Ölçümleri görüntülemek için bir kaynak seçin Select instrument. - Select instrument. + Aracı seçin. diff --git a/src/Aspire.Dashboard/Resources/xlf/Metrics.zh-Hans.xlf b/src/Aspire.Dashboard/Resources/xlf/Metrics.zh-Hans.xlf index f70c924f71b..64baa11f1de 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Metrics.zh-Hans.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Metrics.zh-Hans.xlf @@ -4,87 +4,87 @@ Metrics - Metrics + 指标 Description - Description + 说明 Instrument - Instrument + 检测 Last 15 minutes - Last 15 minutes + 过去 15 分钟 Last 5 minutes - Last 5 minutes + 过去 5 分钟 Last hour - Last hour + 过去 1 小时 Last 1 minute - Last 1 minute + 过去 1 分钟 Last 6 hours - Last 6 hours + 过去 6 小时 Last 30 minutes - Last 30 minutes + 过去 30 分钟 Last 3 hours - Last 3 hours + 过去 3 小时 Last 12 hours - Last 12 hours + 过去 12 小时 Last 24 hours - Last 24 hours + 过去 24 小时 No metrics for the selected resource - No metrics for the selected resource + 所选资源没有指标 {0} metrics - {0} metrics + {0} 指标 {0} is an application name Select a duration - Select a duration + 选择持续时间 Select a resource to view metrics - Select a resource to view metrics + 选择资源以查看指标 Select instrument. - Select instrument. + 选择检测。 diff --git a/src/Aspire.Dashboard/Resources/xlf/Metrics.zh-Hant.xlf b/src/Aspire.Dashboard/Resources/xlf/Metrics.zh-Hant.xlf index 2cdff724095..70209fcd7f6 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Metrics.zh-Hant.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Metrics.zh-Hant.xlf @@ -4,87 +4,87 @@ Metrics - Metrics + 計量 Description - Description + 描述 Instrument - Instrument + 檢測 Last 15 minutes - Last 15 minutes + 過去 15 分鐘 Last 5 minutes - Last 5 minutes + 過去 5 分鐘 Last hour - Last hour + 過去 1 小時 Last 1 minute - Last 1 minute + 過去 1 分鐘 Last 6 hours - Last 6 hours + 過去 6 小時 Last 30 minutes - Last 30 minutes + 過去 30 分鐘 Last 3 hours - Last 3 hours + 過去 3 小時 Last 12 hours - Last 12 hours + 過去 12 小時 Last 24 hours - Last 24 hours + 過去 24 小時 No metrics for the selected resource - No metrics for the selected resource + 沒有所選資源的計量 {0} metrics - {0} metrics + {0} 計量 {0} is an application name Select a duration - Select a duration + 選取期間 Select a resource to view metrics - Select a resource to view metrics + 選取資源以檢視計量 Select instrument. - Select instrument. + 選取檢測。 diff --git a/src/Aspire.Dashboard/Resources/xlf/Resources.cs.xlf b/src/Aspire.Dashboard/Resources/xlf/Resources.cs.xlf index 49a17bb052f..135048e5559 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Resources.cs.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Resources.cs.xlf @@ -4,182 +4,182 @@ "{0}" command failed - "{0}" command failed + Provedení příkazu "{0}" se nezdařilo {0} is the display name of the command that failed "{0}" command succeeded - "{0}" command succeeded + Příkaz "{0}" proběhl úspěšně. {0} is the display name of the command that succeeded View Logs - View Logs + Zobrazit protokoly Endpoint URL - Endpoint URL + Adresa URL koncového bodu Proxy URL - Proxy URL + Adresa URL proxy serveru Commands - Commands + Příkazy Details - Details + Podrobnosti Container arguments - Container arguments + Argumenty kontejneru Container command - Container command + Příkaz kontejneru Container ID - Container ID + ID kontejneru Container image - Container image + Image kontejneru Container ports - Container ports + Porty kontejneru Display name - Display name + Zobrazovaný název Executable arguments - Executable arguments + Argumenty spustitelného souboru Executable path - Executable path + Cesta ke spustitelnému souboru Process ID - Process ID + ID procesu Working directory - Working directory + Pracovní adresář Exit code - Exit code + Ukončovací kód Project path - Project path + Cesta k projektu Start time - Start time + Čas zahájení State - State + Stav Endpoints - Endpoints + Koncové body Environment - Environment + Prostředí Logs - Logs + Protokoly Source - Source + Zdroj Start time - Start time + Čas zahájení State - State + Stav Environment variables for {0} - Environment variables for {0} + Proměnné prostředí pro prostředek {0} {0} is a resource Resources - Resources + Prostředky No environment variables - No environment variables + Žádné proměnné prostředí No resources found - No resources found + Nenašly se žádné prostředky. {0} resources - {0} resources + Počet prostředků: {0} {0} is an application name Resource types - Resource types + Typy prostředků Type - Type + Typ Type filter: All types visible - Type filter: All types visible + Filtr typů: Jsou viditelné všechny typy Type filter: Filtered - Type filter: Filtered + Filtr typů: Vyfiltrováno diff --git a/src/Aspire.Dashboard/Resources/xlf/Resources.de.xlf b/src/Aspire.Dashboard/Resources/xlf/Resources.de.xlf index 639e0aaeef6..c1783addf4e 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Resources.de.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Resources.de.xlf @@ -4,182 +4,182 @@ "{0}" command failed - "{0}" command failed + Befehl "{0}" fehlgeschlagen {0} is the display name of the command that failed "{0}" command succeeded - "{0}" command succeeded + Ausführen des Befehls "{0}" erfolgreich {0} is the display name of the command that succeeded View Logs - View Logs + Protokolle anzeigen Endpoint URL - Endpoint URL + Endpunkt-URL Proxy URL - Proxy URL + Proxy-URL Commands - Commands + Befehle Details - Details + Details Container arguments - Container arguments + Containerargumente Container command - Container command + Containerbefehl Container ID - Container ID + Container-ID Container image - Container image + Containerimage Container ports - Container ports + Containerports Display name - Display name + Anzeigename Executable arguments - Executable arguments + Ausführbare Argumente Executable path - Executable path + Pfad der ausführbaren Datei Process ID - Process ID + Prozess-ID Working directory - Working directory + Arbeitsverzeichnis Exit code - Exit code + Exitcode Project path - Project path + Projektpfad Start time - Start time + Startzeit State - State + Status Endpoints - Endpoints + Endpunkte Environment - Environment + Umgebung Logs - Logs + Protokolle Source - Source + Quelle Start time - Start time + Startzeit State - State + Status Environment variables for {0} - Environment variables for {0} + Umgebungsvariablen für {0} {0} is a resource Resources - Resources + Ressourcen No environment variables - No environment variables + Keine Umgebungsvariablen No resources found - No resources found + Keine Ressourcen gefunden {0} resources - {0} resources + {0} Ressourcen {0} is an application name Resource types - Resource types + Ressourcentypen Type - Type + Typ Type filter: All types visible - Type filter: All types visible + Typfilter: Alle Typen sichtbar Type filter: Filtered - Type filter: Filtered + Typfilter: Gefiltert diff --git a/src/Aspire.Dashboard/Resources/xlf/Resources.es.xlf b/src/Aspire.Dashboard/Resources/xlf/Resources.es.xlf index 0196cf99d77..2df056960cf 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Resources.es.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Resources.es.xlf @@ -4,182 +4,182 @@ "{0}" command failed - "{0}" command failed + Error del comando "{0}" {0} is the display name of the command that failed "{0}" command succeeded - "{0}" command succeeded + El comando "{0}" se ejecutó correctamente {0} is the display name of the command that succeeded View Logs - View Logs + Ver registros Endpoint URL - Endpoint URL + Dirección URL del punto de conexión Proxy URL - Proxy URL + URL del proxy Commands - Commands + Comandos Details - Details + Detalles Container arguments - Container arguments + Argumentos de contenedor Container command - Container command + Comando de contenedor Container ID - Container ID + Id. de contenedor Container image - Container image + Imagen de contenedor Container ports - Container ports + Puertos de contenedor Display name - Display name + Nombre para mostrar Executable arguments - Executable arguments + Argumentos ejecutables Executable path - Executable path + Ruta de acceso ejecutable Process ID - Process ID + Id. del proceso Working directory - Working directory + Directorio de trabajo Exit code - Exit code + Código de salida Project path - Project path + Ruta de acceso del proyecto Start time - Start time + Hora de inicio State - State + Estado Endpoints - Endpoints + Puntos de conexión Environment - Environment + Entorno Logs - Logs + Registros Source - Source + Origen Start time - Start time + Hora de inicio State - State + Estado Environment variables for {0} - Environment variables for {0} + Variables de entorno para {0} {0} is a resource Resources - Resources + Recursos No environment variables - No environment variables + No hay variables de entorno. No resources found - No resources found + No se encontraron recursos {0} resources - {0} resources + {0} recursos {0} is an application name Resource types - Resource types + Tipos de recursos Type - Type + Tipo Type filter: All types visible - Type filter: All types visible + Filtro de tipo: todos los tipos visibles Type filter: Filtered - Type filter: Filtered + Filtro de tipo: filtrado diff --git a/src/Aspire.Dashboard/Resources/xlf/Resources.fr.xlf b/src/Aspire.Dashboard/Resources/xlf/Resources.fr.xlf index a7735fc037a..2b17871bab6 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Resources.fr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Resources.fr.xlf @@ -4,182 +4,182 @@ "{0}" command failed - "{0}" command failed + Échec de la commande « {0} » {0} is the display name of the command that failed "{0}" command succeeded - "{0}" command succeeded + Commande « {0} » réussie {0} is the display name of the command that succeeded View Logs - View Logs + Afficher les journaux Endpoint URL - Endpoint URL + URL de point de terminaison Proxy URL - Proxy URL + URL du proxy Commands - Commands + Commandes Details - Details + Détails Container arguments - Container arguments + Arguments de conteneur Container command - Container command + Commande de conteneur Container ID - Container ID + ID de conteneur Container image - Container image + Image conteneur Container ports - Container ports + Ports de conteneur Display name - Display name + Nom d’affichage Executable arguments - Executable arguments + Arguments exécutables Executable path - Executable path + Chemin d’accès de l’exécutable Process ID - Process ID + ID de processus Working directory - Working directory + Répertoire de travail Exit code - Exit code + Code de sortie Project path - Project path + Chemin de projet Start time - Start time + Heure de début State - State + État Endpoints - Endpoints + Points de terminaison Environment - Environment + Environnement Logs - Logs + Journaux Source - Source + Source Start time - Start time + Heure de début State - State + État Environment variables for {0} - Environment variables for {0} + Variables d’environnement pour {0} {0} is a resource Resources - Resources + Ressources No environment variables - No environment variables + Aucune variable d’environnement No resources found - No resources found + Aucune ressource trouvée {0} resources - {0} resources + Ressources {0} {0} is an application name Resource types - Resource types + Types de ressource Type - Type + Type Type filter: All types visible - Type filter: All types visible + Filtre de type : tous les types sont visibles Type filter: Filtered - Type filter: Filtered + Filtre de type : filtré diff --git a/src/Aspire.Dashboard/Resources/xlf/Resources.it.xlf b/src/Aspire.Dashboard/Resources/xlf/Resources.it.xlf index 404516c5245..0be3b0c7d0a 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Resources.it.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Resources.it.xlf @@ -4,182 +4,182 @@ "{0}" command failed - "{0}" command failed + Comando "{0}" non riuscito {0} is the display name of the command that failed "{0}" command succeeded - "{0}" command succeeded + Comando "{0}" riuscito {0} is the display name of the command that succeeded View Logs - View Logs + Visualizzare i log Endpoint URL - Endpoint URL + URL endpoint Proxy URL - Proxy URL + URL proxy Commands - Commands + Comandi Details - Details + Dettagli Container arguments - Container arguments + Argomenti contenitore Container command - Container command + Comando contenitore Container ID - Container ID + ID contenitore Container image - Container image + Immagine contenitore Container ports - Container ports + Porte contenitore Display name - Display name + Nome visualizzato Executable arguments - Executable arguments + Argomenti eseguibili Executable path - Executable path + Percorso eseguibile Process ID - Process ID + ID processo Working directory - Working directory + Directory di lavoro Exit code - Exit code + Codice di uscita Project path - Project path + Percorso del progetto Start time - Start time + Ora di inizio State - State + Stato Endpoints - Endpoints + Endpoint Environment - Environment + Ambiente Logs - Logs + Log Source - Source + Origine Start time - Start time + Ora di inizio State - State + Stato Environment variables for {0} - Environment variables for {0} + Variabili di ambiente per {0} {0} is a resource Resources - Resources + Risorse No environment variables - No environment variables + Nessuna variabile di ambiente No resources found - No resources found + Nessuna risorsa trovata {0} resources - {0} resources + {0} risorse {0} is an application name Resource types - Resource types + Tipi di risorsa Type - Type + Tipo Type filter: All types visible - Type filter: All types visible + Filtro tipo: tutti i tipi sono visibili Type filter: Filtered - Type filter: Filtered + Filtro tipo: filtro applicato diff --git a/src/Aspire.Dashboard/Resources/xlf/Resources.ja.xlf b/src/Aspire.Dashboard/Resources/xlf/Resources.ja.xlf index 3fdc600b5e0..0c84f0c9b2d 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Resources.ja.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Resources.ja.xlf @@ -4,182 +4,182 @@ "{0}" command failed - "{0}" command failed + "{0}" コマンドが失敗しました {0} is the display name of the command that failed "{0}" command succeeded - "{0}" command succeeded + "{0}" コマンドが成功しました {0} is the display name of the command that succeeded View Logs - View Logs + ログの表示 Endpoint URL - Endpoint URL + エンドポイント URL Proxy URL - Proxy URL + プロキシ URL Commands - Commands + コマンド Details - Details + 詳細 Container arguments - Container arguments + コンテナー引数 Container command - Container command + コンテナー コマンド Container ID - Container ID + コンテナー ID Container image - Container image + コンテナー イメージ Container ports - Container ports + コンテナー ポート Display name - Display name + 表示名 Executable arguments - Executable arguments + 実行可能な引数 Executable path - Executable path + 実行可能ファイルのパス Process ID - Process ID + プロセス ID Working directory - Working directory + 作業ディレクトリ Exit code - Exit code + 終了コード Project path - Project path + プロジェクト パス Start time - Start time + 開始時刻 State - State + 状態 Endpoints - Endpoints + エンドポイント Environment - Environment + 環境 Logs - Logs + ログ Source - Source + ソース Start time - Start time + 開始時刻 State - State + 状態 Environment variables for {0} - Environment variables for {0} + {0} の環境変数 {0} is a resource Resources - Resources + リソース No environment variables - No environment variables + 環境変数はありません No resources found - No resources found + リソースが見つかりません {0} resources - {0} resources + {0} のリソース {0} is an application name Resource types - Resource types + リソースの種類 Type - Type + 種類 Type filter: All types visible - Type filter: All types visible + 型フィルター: すべての型が表示されます Type filter: Filtered - Type filter: Filtered + 型フィルター: フィルター処理済み diff --git a/src/Aspire.Dashboard/Resources/xlf/Resources.ko.xlf b/src/Aspire.Dashboard/Resources/xlf/Resources.ko.xlf index 361571abedb..fdef712896f 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Resources.ko.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Resources.ko.xlf @@ -4,182 +4,182 @@ "{0}" command failed - "{0}" command failed + "{0}" 명령이 실패했습니다. {0} is the display name of the command that failed "{0}" command succeeded - "{0}" command succeeded + "{0}" 명령이 성공했습니다. {0} is the display name of the command that succeeded View Logs - View Logs + 로그 보기 Endpoint URL - Endpoint URL + 엔드포인트 URL Proxy URL - Proxy URL + 프록시 URL Commands - Commands + 명령 Details - Details + 세부 정보 Container arguments - Container arguments + 컨테이너 인수 Container command - Container command + 컨테이너 명령 Container ID - Container ID + 컨테이너 ID Container image - Container image + 컨테이너 이미지 Container ports - Container ports + 컨테이너 포트 Display name - Display name + 표시 이름 Executable arguments - Executable arguments + 실행 파일 인수 Executable path - Executable path + 실행 파일 경로 Process ID - Process ID + 프로세스 ID Working directory - Working directory + 작업 디렉터리 Exit code - Exit code + 종료 코드 Project path - Project path + 프로젝트 경로 Start time - Start time + 시작 시각 State - State + 상태 Endpoints - Endpoints + 엔드포인트 Environment - Environment + 환경 Logs - Logs + 로그 Source - Source + 원본 Start time - Start time + 시작 시각 State - State + 상태 Environment variables for {0} - Environment variables for {0} + {0}에 대한 환경 변수 {0} is a resource Resources - Resources + 리소스 No environment variables - No environment variables + 환경 변수 없음 No resources found - No resources found + 리소스를 찾을 수 없음 {0} resources - {0} resources + {0} 리소스 {0} is an application name Resource types - Resource types + 리소스 종류 Type - Type + 형식 Type filter: All types visible - Type filter: All types visible + 형식 필터: 모든 형식 표시 Type filter: Filtered - Type filter: Filtered + 형식 필터: 필터링됨 diff --git a/src/Aspire.Dashboard/Resources/xlf/Resources.pl.xlf b/src/Aspire.Dashboard/Resources/xlf/Resources.pl.xlf index 2be77b0863c..9ef1788fc55 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Resources.pl.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Resources.pl.xlf @@ -4,182 +4,182 @@ "{0}" command failed - "{0}" command failed + Niepowodzenie polecenia „{0}” {0} is the display name of the command that failed "{0}" command succeeded - "{0}" command succeeded + Polecenie „{0}” zostało pomyślnie wykonane {0} is the display name of the command that succeeded View Logs - View Logs + Wyświetl dzienniki Endpoint URL - Endpoint URL + Adres URL punktu końcowego Proxy URL - Proxy URL + Adres URL serwera proxy Commands - Commands + Polecenia Details - Details + Szczegóły Container arguments - Container arguments + Argumenty kontenera Container command - Container command + Polecenie kontenera Container ID - Container ID + Identyfikator kontenera Container image - Container image + Obraz kontenera Container ports - Container ports + Porty kontenerów Display name - Display name + Nazwa wyświetlana Executable arguments - Executable arguments + Argumenty pliku wykonywalnego Executable path - Executable path + Ścieżka pliku wykonywalnego Process ID - Process ID + Identyfikator procesu Working directory - Working directory + Katalog roboczy Exit code - Exit code + Kod zakończenia Project path - Project path + Ścieżka projektu Start time - Start time + Godzina rozpoczęcia State - State + Stan Endpoints - Endpoints + Punkty końcowe Environment - Environment + Środowisko Logs - Logs + Dzienniki Source - Source + Źródło Start time - Start time + Godzina rozpoczęcia State - State + Stan Environment variables for {0} - Environment variables for {0} + Zmienne środowiskowe dla {0} {0} is a resource Resources - Resources + Zasoby No environment variables - No environment variables + Brak zmiennych środowiskowych No resources found - No resources found + Nie znaleziono żadnych zasobów {0} resources - {0} resources + Liczba zasobów: {0} {0} is an application name Resource types - Resource types + Typy zasobów Type - Type + Typ Type filter: All types visible - Type filter: All types visible + Filtr typu: wszystkie typy są widoczne Type filter: Filtered - Type filter: Filtered + Filtr typu: filtrowany diff --git a/src/Aspire.Dashboard/Resources/xlf/Resources.pt-BR.xlf b/src/Aspire.Dashboard/Resources/xlf/Resources.pt-BR.xlf index d4af841c3bc..eae4670310b 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Resources.pt-BR.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Resources.pt-BR.xlf @@ -4,182 +4,182 @@ "{0}" command failed - "{0}" command failed + O comando “{0}” falhou {0} is the display name of the command that failed "{0}" command succeeded - "{0}" command succeeded + O comando “{0}” foi bem-sucedido {0} is the display name of the command that succeeded View Logs - View Logs + Exibir os Logs Endpoint URL - Endpoint URL + URL do ponto de extremidade Proxy URL - Proxy URL + URL do Proxy Commands - Commands + Comandos Details - Details + Detalhes Container arguments - Container arguments + Argumentos de contêiner Container command - Container command + Comando de contêiner Container ID - Container ID + ID do Contêiner Container image - Container image + Imagem de contêiner Container ports - Container ports + Portas de contêiner Display name - Display name + Nome de exibição Executable arguments - Executable arguments + Argumentos executáveis Executable path - Executable path + Caminho executável Process ID - Process ID + ID do Processo Working directory - Working directory + Diretório de trabalho Exit code - Exit code + Código de saída Project path - Project path + Caminho do projeto Start time - Start time + Hora de início State - State + Estado Endpoints - Endpoints + Pontos de extremidade Environment - Environment + Ambiente Logs - Logs + Logs Source - Source + Origem Start time - Start time + Hora de início State - State + Estado Environment variables for {0} - Environment variables for {0} + Variáveis de ambiente para {0} {0} is a resource Resources - Resources + Recursos No environment variables - No environment variables + Nenhuma variável de ambiente No resources found - No resources found + Nenhum recurso encontrado {0} resources - {0} resources + {0} recursos {0} is an application name Resource types - Resource types + Tipos de recurso Type - Type + Tipo Type filter: All types visible - Type filter: All types visible + Filtro de tipo: todos os tipos visíveis Type filter: Filtered - Type filter: Filtered + Filtro de tipo: filtrado diff --git a/src/Aspire.Dashboard/Resources/xlf/Resources.ru.xlf b/src/Aspire.Dashboard/Resources/xlf/Resources.ru.xlf index 83643409251..bc5d6937881 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Resources.ru.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Resources.ru.xlf @@ -4,182 +4,182 @@ "{0}" command failed - "{0}" command failed + Сбой команды "{0}" {0} is the display name of the command that failed "{0}" command succeeded - "{0}" command succeeded + Команда "{0}" успешно выполнена {0} is the display name of the command that succeeded View Logs - View Logs + Просмотреть журналы Endpoint URL - Endpoint URL + URL-адрес конечной точки Proxy URL - Proxy URL + URL-адрес прокси-сервера Commands - Commands + Команды Details - Details + Сведения Container arguments - Container arguments + Аргументы контейнера Container command - Container command + Команда контейнера Container ID - Container ID + ИД контейнера Container image - Container image + Образ контейнера Container ports - Container ports + Порты контейнера Display name - Display name + Видимое имя Executable arguments - Executable arguments + Аргументы исполняемого файла Executable path - Executable path + Путь исполняемого файла Process ID - Process ID + Идентификатор процесса Working directory - Working directory + Рабочий каталог Exit code - Exit code + Код завершения Project path - Project path + Путь к проекту Start time - Start time + Время начала State - State + Состояние Endpoints - Endpoints + Конечные точки Environment - Environment + Среда Logs - Logs + Журналы Source - Source + Источник Start time - Start time + Время начала State - State + Состояние Environment variables for {0} - Environment variables for {0} + Переменные среды для {0} {0} is a resource Resources - Resources + Ресурсы No environment variables - No environment variables + Нет переменных среды No resources found - No resources found + Ресурсы не найдены {0} resources - {0} resources + Ресурсов: {0} {0} is an application name Resource types - Resource types + Типы ресурсов Type - Type + Тип Type filter: All types visible - Type filter: All types visible + Фильтр типов: все типы видимы Type filter: Filtered - Type filter: Filtered + Фильтр типов: отфильтрованный diff --git a/src/Aspire.Dashboard/Resources/xlf/Resources.tr.xlf b/src/Aspire.Dashboard/Resources/xlf/Resources.tr.xlf index 0184e713824..aee3f3a2379 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Resources.tr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Resources.tr.xlf @@ -4,182 +4,182 @@ "{0}" command failed - "{0}" command failed + "{0}" komutu başarısız oldu {0} is the display name of the command that failed "{0}" command succeeded - "{0}" command succeeded + "{0}" komutu başarılı oldu {0} is the display name of the command that succeeded View Logs - View Logs + Günlükleri Görüntüle Endpoint URL - Endpoint URL + Uç Nokta URL'si Proxy URL - Proxy URL + Ara sunucu URL'si Commands - Commands + Komutlar Details - Details + Ayrıntılar Container arguments - Container arguments + Kapsayıcı bağımsız değişkenleri Container command - Container command + Kapsayıcı komutu Container ID - Container ID + Kapsayıcı Kimliği Container image - Container image + Kapsayıcı görüntüsü Container ports - Container ports + Kapsayıcı bağlantı noktaları Display name - Display name + Görünen ad Executable arguments - Executable arguments + Yürütülebilir bağımsız değişkenler Executable path - Executable path + Yürütülebilir yol Process ID - Process ID + İşlem Kimliği Working directory - Working directory + Çalışma dizini Exit code - Exit code + Çıkış kodu Project path - Project path + Proje yolu Start time - Start time + Başlangıç zamanı State - State + Durum Endpoints - Endpoints + Uç Noktalar Environment - Environment + Ortam Logs - Logs + Günlükler Source - Source + Kaynak Start time - Start time + Başlangıç zamanı State - State + Durum Environment variables for {0} - Environment variables for {0} + {0} için ortam değişkenleri {0} is a resource Resources - Resources + Kaynaklar No environment variables - No environment variables + Ortam değişkeni yok No resources found - No resources found + Kaynak bulunamadı {0} resources - {0} resources + {0} kaynak {0} is an application name Resource types - Resource types + Kaynak türleri Type - Type + Tür Type filter: All types visible - Type filter: All types visible + Tür filtresi: Tüm türler görünür Type filter: Filtered - Type filter: Filtered + Tür filtresi: Filtrelendi diff --git a/src/Aspire.Dashboard/Resources/xlf/Resources.zh-Hans.xlf b/src/Aspire.Dashboard/Resources/xlf/Resources.zh-Hans.xlf index 49a352a2d6f..78fa9b791f1 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Resources.zh-Hans.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Resources.zh-Hans.xlf @@ -4,182 +4,182 @@ "{0}" command failed - "{0}" command failed + “{0}”命令失败 {0} is the display name of the command that failed "{0}" command succeeded - "{0}" command succeeded + “{0}”命令已成功 {0} is the display name of the command that succeeded View Logs - View Logs + 查看日志 Endpoint URL - Endpoint URL + 终结点 URL Proxy URL - Proxy URL + 代理 URL Commands - Commands + 命令 Details - Details + 详细信息 Container arguments - Container arguments + 容器参数 Container command - Container command + 容器命令 Container ID - Container ID + 容器 ID Container image - Container image + 容器映像 Container ports - Container ports + 容器端口 Display name - Display name + 显示名称 Executable arguments - Executable arguments + 可执行参数 Executable path - Executable path + 可执行路径 Process ID - Process ID + 进程 ID Working directory - Working directory + 工作目录 Exit code - Exit code + 退出代码 Project path - Project path + 项目路径 Start time - Start time + 开始时间 State - State + 状态 Endpoints - Endpoints + 终结点 Environment - Environment + 环境 Logs - Logs + 日志 Source - Source + Start time - Start time + 开始时间 State - State + 状态 Environment variables for {0} - Environment variables for {0} + {0} 的环境变量 {0} is a resource Resources - Resources + 资源 No environment variables - No environment variables + 无环境变量 No resources found - No resources found + 未找到任何资源 {0} resources - {0} resources + {0} 资源 {0} is an application name Resource types - Resource types + 资源类型 Type - Type + 类型 Type filter: All types visible - Type filter: All types visible + 类型筛选器: 所有类型可见 Type filter: Filtered - Type filter: Filtered + 类型筛选器: 已筛选 diff --git a/src/Aspire.Dashboard/Resources/xlf/Resources.zh-Hant.xlf b/src/Aspire.Dashboard/Resources/xlf/Resources.zh-Hant.xlf index ef1dfc12740..848b6cc0ff3 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Resources.zh-Hant.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Resources.zh-Hant.xlf @@ -4,182 +4,182 @@ "{0}" command failed - "{0}" command failed + "{0}" 命令失敗 {0} is the display name of the command that failed "{0}" command succeeded - "{0}" command succeeded + "{0}" 命令成功 {0} is the display name of the command that succeeded View Logs - View Logs + 檢視記錄 Endpoint URL - Endpoint URL + 端點 URL Proxy URL - Proxy URL + Proxy URL Commands - Commands + 命令 Details - Details + 詳細資料 Container arguments - Container arguments + 容器引數 Container command - Container command + 容器命令 Container ID - Container ID + 容器識別碼 Container image - Container image + 容器映像 Container ports - Container ports + 容器連接埠 Display name - Display name + 顯示名稱 Executable arguments - Executable arguments + 可執行檔變數 Executable path - Executable path + 可執行檔路徑 Process ID - Process ID + 處理序識別碼 Working directory - Working directory + 工作目錄 Exit code - Exit code + 結束代碼 Project path - Project path + 專案路徑 Start time - Start time + 開始時間 State - State + 狀態 Endpoints - Endpoints + 端點 Environment - Environment + 環境 Logs - Logs + 記錄 Source - Source + 來源 Start time - Start time + 開始時間 State - State + 狀態 Environment variables for {0} - Environment variables for {0} + {0} 的環境變數 {0} is a resource Resources - Resources + 資源 No environment variables - No environment variables + 沒有任何環境變數 No resources found - No resources found + 找不到任何資源 {0} resources - {0} resources + {0} 資源 {0} is an application name Resource types - Resource types + 資源類型 Type - Type + 類型 Type filter: All types visible - Type filter: All types visible + 類型篩選: 所有類型都可見 Type filter: Filtered - Type filter: Filtered + 類型篩選: 已篩選 diff --git a/src/Aspire.Dashboard/Resources/xlf/Routes.cs.xlf b/src/Aspire.Dashboard/Resources/xlf/Routes.cs.xlf index 7c4ffe79cbb..5c92274c01c 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Routes.cs.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Routes.cs.xlf @@ -4,12 +4,12 @@ Sorry, there's nothing at this address. - Sorry, there's nothing at this address. + Omlouváme se, ale na této adrese nic není. Not found - Not found + Nenalezeno diff --git a/src/Aspire.Dashboard/Resources/xlf/Routes.de.xlf b/src/Aspire.Dashboard/Resources/xlf/Routes.de.xlf index 7d670d4adff..628064634b9 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Routes.de.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Routes.de.xlf @@ -4,12 +4,12 @@ Sorry, there's nothing at this address. - Sorry, there's nothing at this address. + An dieser Adresse befindet sich leider nichts. Not found - Not found + Nicht gefunden diff --git a/src/Aspire.Dashboard/Resources/xlf/Routes.es.xlf b/src/Aspire.Dashboard/Resources/xlf/Routes.es.xlf index 5080bdfab40..77e8a087b7c 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Routes.es.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Routes.es.xlf @@ -4,12 +4,12 @@ Sorry, there's nothing at this address. - Sorry, there's nothing at this address. + Lo sentimos, no hay nada en esta dirección. Not found - Not found + No encontrado diff --git a/src/Aspire.Dashboard/Resources/xlf/Routes.fr.xlf b/src/Aspire.Dashboard/Resources/xlf/Routes.fr.xlf index c8f0ecbb3d7..f09bbc9c283 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Routes.fr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Routes.fr.xlf @@ -4,12 +4,12 @@ Sorry, there's nothing at this address. - Sorry, there's nothing at this address. + Désolé... Cette adresse est vide. Not found - Not found + Introuvable diff --git a/src/Aspire.Dashboard/Resources/xlf/Routes.it.xlf b/src/Aspire.Dashboard/Resources/xlf/Routes.it.xlf index 7a0ba4aa7ea..3baf42d31e1 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Routes.it.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Routes.it.xlf @@ -4,12 +4,12 @@ Sorry, there's nothing at this address. - Sorry, there's nothing at this address. + Questo indirizzo non contiene nulla. Not found - Not found + Non trovata diff --git a/src/Aspire.Dashboard/Resources/xlf/Routes.ja.xlf b/src/Aspire.Dashboard/Resources/xlf/Routes.ja.xlf index f95d2c43639..239926383b4 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Routes.ja.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Routes.ja.xlf @@ -4,12 +4,12 @@ Sorry, there's nothing at this address. - Sorry, there's nothing at this address. + このアドレスには何もありません。 Not found - Not found + 見つかりません diff --git a/src/Aspire.Dashboard/Resources/xlf/Routes.ko.xlf b/src/Aspire.Dashboard/Resources/xlf/Routes.ko.xlf index d6a0b0a02de..ebbbdaec448 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Routes.ko.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Routes.ko.xlf @@ -4,12 +4,12 @@ Sorry, there's nothing at this address. - Sorry, there's nothing at this address. + 죄송하지만 이 주소에는 아무것도 없습니다. Not found - Not found + 찾을 수 없음 diff --git a/src/Aspire.Dashboard/Resources/xlf/Routes.pl.xlf b/src/Aspire.Dashboard/Resources/xlf/Routes.pl.xlf index a2cdbe252ff..8fd22171005 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Routes.pl.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Routes.pl.xlf @@ -4,12 +4,12 @@ Sorry, there's nothing at this address. - Sorry, there's nothing at this address. + Niestety, pod tym adresem nic nie ma. Not found - Not found + Nie znaleziono diff --git a/src/Aspire.Dashboard/Resources/xlf/Routes.pt-BR.xlf b/src/Aspire.Dashboard/Resources/xlf/Routes.pt-BR.xlf index e600b921f88..ef92ea5199b 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Routes.pt-BR.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Routes.pt-BR.xlf @@ -4,12 +4,12 @@ Sorry, there's nothing at this address. - Sorry, there's nothing at this address. + Não há nada neste endereço. Not found - Not found + Não encontrado diff --git a/src/Aspire.Dashboard/Resources/xlf/Routes.ru.xlf b/src/Aspire.Dashboard/Resources/xlf/Routes.ru.xlf index 9dbb0521fcc..1178a682acf 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Routes.ru.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Routes.ru.xlf @@ -4,12 +4,12 @@ Sorry, there's nothing at this address. - Sorry, there's nothing at this address. + По адресу ничего не найдено. Not found - Not found + Не найдено diff --git a/src/Aspire.Dashboard/Resources/xlf/Routes.tr.xlf b/src/Aspire.Dashboard/Resources/xlf/Routes.tr.xlf index 0f5c007bf5a..e6f1260810f 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Routes.tr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Routes.tr.xlf @@ -4,12 +4,12 @@ Sorry, there's nothing at this address. - Sorry, there's nothing at this address. + Ne yazık ki bu adreste içerik yok. Not found - Not found + Bulunamadı diff --git a/src/Aspire.Dashboard/Resources/xlf/Routes.zh-Hans.xlf b/src/Aspire.Dashboard/Resources/xlf/Routes.zh-Hans.xlf index d5424d59df7..843b52f8598 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Routes.zh-Hans.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Routes.zh-Hans.xlf @@ -4,12 +4,12 @@ Sorry, there's nothing at this address. - Sorry, there's nothing at this address. + 抱歉,此地址中没有任何内容。 Not found - Not found + 未找到 diff --git a/src/Aspire.Dashboard/Resources/xlf/Routes.zh-Hant.xlf b/src/Aspire.Dashboard/Resources/xlf/Routes.zh-Hant.xlf index 7b8f65eed5b..0b43a87f3c7 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Routes.zh-Hant.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Routes.zh-Hant.xlf @@ -4,12 +4,12 @@ Sorry, there's nothing at this address. - Sorry, there's nothing at this address. + 很抱歉,此位址沒有任何內容。 Not found - Not found + 找不到 diff --git a/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.cs.xlf b/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.cs.xlf index 514d32c8a38..851625ef816 100644 --- a/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.cs.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.cs.xlf @@ -4,92 +4,92 @@ Add filter - Add filter + Přidat filtr (All) - (All) + (Vše) All - All + Vše Edit filter - Edit filter + Upravit filtr Log entry details - Log entry details + Podrobnosti položky protokolu Filters: - Filters: + Filtry: Structured logs - Structured logs + Strukturované protokoly Level - Level + Úroveň Message - Message + Zpráva Message filter - Message filter + Filtr zpráv Minimum log level filter - Minimum log level filter + Filtr minimální úrovně protokolování No filters - No filters + Žádné filtry No structured logs found - No structured logs found + Nenašly se žádné strukturované protokoly. {0} structured logs - {0} structured logs + Strukturované protokoly aplikace {0} {0} is an application name Resource - Resource + Prostředek Select a minimum log level - Select a minimum log level + Vyberte minimální úroveň protokolování. Timestamp - Timestamp + Časové razítko Trace - Trace + Trasování diff --git a/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.de.xlf b/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.de.xlf index a14178b03c7..6367eac9522 100644 --- a/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.de.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.de.xlf @@ -4,92 +4,92 @@ Add filter - Add filter + Filter hinzufügen (All) - (All) + (Alle) All - All + Alle Edit filter - Edit filter + Filter bearbeiten Log entry details - Log entry details + Protokolleintragsdetails Filters: - Filters: + Filter: Structured logs - Structured logs + Strukturierte Protokolle Level - Level + Ebene Message - Message + Nachricht Message filter - Message filter + Nachrichtenfilter Minimum log level filter - Minimum log level filter + Mindestprotokolliergradfilter No filters - No filters + Keine Filter No structured logs found - No structured logs found + Keine strukturierten Protokolle gefunden. {0} structured logs - {0} structured logs + Strukturierte Protokolle von {0} {0} is an application name Resource - Resource + Ressource Select a minimum log level - Select a minimum log level + Mindestprotokolliergrad auswählen Timestamp - Timestamp + Zeitstempel Trace - Trace + Ablaufverfolgung diff --git a/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.es.xlf b/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.es.xlf index 51b8ce50342..2d29e343e9b 100644 --- a/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.es.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.es.xlf @@ -4,92 +4,92 @@ Add filter - Add filter + Agregar filtro (All) - (All) + (Todas) All - All + Todo Edit filter - Edit filter + Editar filtro Log entry details - Log entry details + Detalles de la entrada de registro Filters: - Filters: + Filtros: Structured logs - Structured logs + Registros estructurados Level - Level + Nivel Message - Message + Mensaje Message filter - Message filter + Filtro de mensajes Minimum log level filter - Minimum log level filter + Filtro de nivel de registro mínimo No filters - No filters + No hay ningún filtro. No structured logs found - No structured logs found + No se encontraron registros estructurados {0} structured logs - {0} structured logs + Registros estructurados de {0} {0} is an application name Resource - Resource + Recurso Select a minimum log level - Select a minimum log level + Seleccionar un nivel de registro mínimo Timestamp - Timestamp + Marca de tiempo Trace - Trace + Seguimiento diff --git a/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.fr.xlf b/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.fr.xlf index 1143addac1c..fd3e8758c6a 100644 --- a/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.fr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.fr.xlf @@ -4,92 +4,92 @@ Add filter - Add filter + Ajouter un filtre (All) - (All) + (Tout) All - All + Tous Edit filter - Edit filter + Modifier le filtre Log entry details - Log entry details + Détails de l’entrée de journal Filters: - Filters: + Filtres : Structured logs - Structured logs + Journaux structurés Level - Level + Niveau Message - Message + Message Message filter - Message filter + Filtre de messages Minimum log level filter - Minimum log level filter + Filtre de niveau de journalisation minimal No filters - No filters + Aucun filtre No structured logs found - No structured logs found + Journaux structurés introuvables {0} structured logs - {0} structured logs + Journaux structurés {0} {0} is an application name Resource - Resource + Ressource Select a minimum log level - Select a minimum log level + Sélectionner un niveau de journalisation minimal Timestamp - Timestamp + Timestamp Trace - Trace + Trace diff --git a/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.it.xlf b/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.it.xlf index 0a305c86ee8..add543e2f13 100644 --- a/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.it.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.it.xlf @@ -4,92 +4,92 @@ Add filter - Add filter + Aggiungi filtro (All) - (All) + (Tutti) All - All + Tutto Edit filter - Edit filter + Modifica filtro Log entry details - Log entry details + Dettagli della voce di log Filters: - Filters: + Filtri: Structured logs - Structured logs + Log strutturati Level - Level + Livello Message - Message + Messaggio Message filter - Message filter + Filtro messaggi Minimum log level filter - Minimum log level filter + Filtro livello di log minimo No filters - No filters + Nessun filtro No structured logs found - No structured logs found + Nessun log strutturato trovato {0} structured logs - {0} structured logs + {0} log strutturati {0} is an application name Resource - Resource + Risorsa Select a minimum log level - Select a minimum log level + Seleziona un livello di log minimo Timestamp - Timestamp + Timestamp Trace - Trace + Traccia diff --git a/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.ja.xlf b/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.ja.xlf index 4f4f0f21d79..aeb0dbcd974 100644 --- a/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.ja.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.ja.xlf @@ -4,92 +4,92 @@ Add filter - Add filter + フィルターの追加 (All) - (All) + (すべて) All - All + すべて Edit filter - Edit filter + フィルターの編集 Log entry details - Log entry details + ログ エントリの詳細 Filters: - Filters: + フィルター: Structured logs - Structured logs + 構造化ログ Level - Level + レベル Message - Message + メッセージ Message filter - Message filter + メッセージ フィルター Minimum log level filter - Minimum log level filter + 最小のログ レベル フィルター No filters - No filters + フィルターなし No structured logs found - No structured logs found + 構造化ログが見つかりません {0} structured logs - {0} structured logs + {0} 件の構造化ログ {0} is an application name Resource - Resource + リソース Select a minimum log level - Select a minimum log level + 最小ログ レベルを選択してください Timestamp - Timestamp + タイムスタンプ Trace - Trace + トレース diff --git a/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.ko.xlf b/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.ko.xlf index d0fef57ca09..3111e14fa95 100644 --- a/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.ko.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.ko.xlf @@ -4,92 +4,92 @@ Add filter - Add filter + 필터 추가 (All) - (All) + (모두) All - All + 모두 Edit filter - Edit filter + 편집 필터 Log entry details - Log entry details + 로그 항목 세부 정보 Filters: - Filters: + 필터: Structured logs - Structured logs + 구조화된 로그 Level - Level + 수준 Message - Message + 메시지 Message filter - Message filter + 메시지 필터 Minimum log level filter - Minimum log level filter + 최소 로그 수준 필터 No filters - No filters + 필터 없음 No structured logs found - No structured logs found + 구조화된 로그를 찾을 수 없음 {0} structured logs - {0} structured logs + {0} 구조화된 로그 {0} is an application name Resource - Resource + 리소스 Select a minimum log level - Select a minimum log level + 최소 로그 수준 선택 Timestamp - Timestamp + 타임스탬프 Trace - Trace + 추적 diff --git a/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.pl.xlf b/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.pl.xlf index 58dc44a74a4..475c0d52865 100644 --- a/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.pl.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.pl.xlf @@ -4,92 +4,92 @@ Add filter - Add filter + Dodaj filtr (All) - (All) + (Wszystko) All - All + Wszystko Edit filter - Edit filter + Edytuj filtr Log entry details - Log entry details + Szczegóły wpisu dziennika Filters: - Filters: + Filtry: Structured logs - Structured logs + Dzienniki strukturalne Level - Level + Poziom Message - Message + Komunikat Message filter - Message filter + Filtr wiadomości Minimum log level filter - Minimum log level filter + Filtr minimalnego poziomu dziennika No filters - No filters + Brak filtrów No structured logs found - No structured logs found + Nie znaleziono dzienników strukturalnych {0} structured logs - {0} structured logs + {0} dzienników strukturalnych {0} is an application name Resource - Resource + Zasób Select a minimum log level - Select a minimum log level + Wybierz minimalny poziom dziennika Timestamp - Timestamp + Znacznik czasu Trace - Trace + Ślad diff --git a/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.pt-BR.xlf b/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.pt-BR.xlf index c1bbe8fa174..615ea4d4caf 100644 --- a/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.pt-BR.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.pt-BR.xlf @@ -4,92 +4,92 @@ Add filter - Add filter + Adicionar filtro (All) - (All) + (Tudo) All - All + Tudo Edit filter - Edit filter + Editar filtro Log entry details - Log entry details + Detalhes da entrada de log Filters: - Filters: + Filtros: Structured logs - Structured logs + Logs estruturados Level - Level + Nível Message - Message + Mensagem Message filter - Message filter + Filtro de mensagem Minimum log level filter - Minimum log level filter + Filtro de nível de log mínimo No filters - No filters + Sem filtros No structured logs found - No structured logs found + Nenhum log estruturado encontrado {0} structured logs - {0} structured logs + {0} logs estruturados {0} is an application name Resource - Resource + Recurso Select a minimum log level - Select a minimum log level + Selecione um nível de log mínimo Timestamp - Timestamp + Carimbo de data/hora Trace - Trace + Rastreamento diff --git a/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.ru.xlf b/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.ru.xlf index 6ecb5f77e7f..2c6ca82250e 100644 --- a/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.ru.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.ru.xlf @@ -4,92 +4,92 @@ Add filter - Add filter + Добавить фильтр (All) - (All) + (Все) All - All + Все Edit filter - Edit filter + Изменить фильтр Log entry details - Log entry details + Подробности записи журнала Filters: - Filters: + Фильтры: Structured logs - Structured logs + Структурированные журналы Level - Level + Уровень Message - Message + Сообщение Message filter - Message filter + Фильтр сообщений Minimum log level filter - Minimum log level filter + Фильтр минимального уровня ведения журнала No filters - No filters + Нет фильтров No structured logs found - No structured logs found + Структурированные журналы не найдены {0} structured logs - {0} structured logs + Структурированных журналов: {0} {0} is an application name Resource - Resource + Ресурс Select a minimum log level - Select a minimum log level + Выберите минимальный уровень ведения журнала Timestamp - Timestamp + Метка времени Trace - Trace + Трассировка diff --git a/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.tr.xlf b/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.tr.xlf index 01dc53364ba..ee244a2e58b 100644 --- a/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.tr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.tr.xlf @@ -4,92 +4,92 @@ Add filter - Add filter + Filtre ekle (All) - (All) + (Tümü) All - All + Tümü Edit filter - Edit filter + Filtreyi düzenle Log entry details - Log entry details + Günlük girişi ayrıntıları Filters: - Filters: + Filtreler: Structured logs - Structured logs + Yapılandırılmış günlükler Level - Level + Düzey Message - Message + İleti Message filter - Message filter + İleti filtresi Minimum log level filter - Minimum log level filter + En düşük günlük düzeyi filtresi No filters - No filters + Filtre bulunamadı No structured logs found - No structured logs found + Yapılandırılmış günlük bulunamadı {0} structured logs - {0} structured logs + {0} yapılandırılmış günlükler {0} is an application name Resource - Resource + Kaynak Select a minimum log level - Select a minimum log level + En düşük günlük düzeyini seçin Timestamp - Timestamp + Zaman damgası Trace - Trace + İzleme diff --git a/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.zh-Hans.xlf b/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.zh-Hans.xlf index 3212bd78318..273cbe095a0 100644 --- a/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.zh-Hans.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.zh-Hans.xlf @@ -4,92 +4,92 @@ Add filter - Add filter + 添加筛选器 (All) - (All) + (全部) All - All + 所有 Edit filter - Edit filter + 编辑筛选器 Log entry details - Log entry details + 日志条目详细信息 Filters: - Filters: + 筛选器: Structured logs - Structured logs + 结构化日志 Level - Level + 级别 Message - Message + 消息 Message filter - Message filter + 消息筛选器 Minimum log level filter - Minimum log level filter + 最低日志级别筛选器 No filters - No filters + 无筛选器 No structured logs found - No structured logs found + 找不到结构化日志 {0} structured logs - {0} structured logs + {0} 结构化日志 {0} is an application name Resource - Resource + 资源 Select a minimum log level - Select a minimum log level + 选择最低日志级别 Timestamp - Timestamp + 时间戳 Trace - Trace + 跟踪 diff --git a/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.zh-Hant.xlf b/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.zh-Hant.xlf index 63716b78f01..67573cb2547 100644 --- a/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.zh-Hant.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/StructuredLogs.zh-Hant.xlf @@ -4,92 +4,92 @@ Add filter - Add filter + 新增篩選 (All) - (All) + (全部) All - All + 全部 Edit filter - Edit filter + 編輯篩選 Log entry details - Log entry details + 記錄項目詳細資料 Filters: - Filters: + 篩選: Structured logs - Structured logs + 結構化記錄 Level - Level + 層級 Message - Message + 訊息 Message filter - Message filter + 訊息篩選 Minimum log level filter - Minimum log level filter + 最低記錄層級篩選 No filters - No filters + 沒有篩選 No structured logs found - No structured logs found + 找不到結構化記錄 {0} structured logs - {0} structured logs + {0} 結構化記錄 {0} is an application name Resource - Resource + 資源 Select a minimum log level - Select a minimum log level + 選取最小記錄層級 Timestamp - Timestamp + 時間戳記 Trace - Trace + 追蹤 diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.cs.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.cs.xlf index 44034bee62b..f3d3556672b 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.cs.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.cs.xlf @@ -4,37 +4,37 @@ Depth - Depth + Hloubka Duration - Duration + Doba trvání {0} Traces - {0} Traces + Trasování aplikace {0} {0} is an application name Resources - Resources + Prostředky Total spans - Total spans + Celkový počet rozsahů Trace "{0}" not found - Trace "{0}" not found + Trasování {0} nebylo nalezeno. {0} is an identifier Trace detail - Trace detail + Podrobnosti trasování diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.de.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.de.xlf index f14af951715..176209e84bf 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.de.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.de.xlf @@ -4,37 +4,37 @@ Depth - Depth + Tiefe Duration - Duration + Dauer {0} Traces - {0} Traces + {0} Ablaufverfolgungen {0} is an application name Resources - Resources + Ressourcen Total spans - Total spans + Bereiche gesamt Trace "{0}" not found - Trace "{0}" not found + Die Ablaufverfolgung "{0}" wurde nicht gefunden. {0} is an identifier Trace detail - Trace detail + Ablaufverfolgungsdetails diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.es.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.es.xlf index c97c9d87471..6f9bd0f0726 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.es.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.es.xlf @@ -4,37 +4,37 @@ Depth - Depth + Profundidad Duration - Duration + Duración {0} Traces - {0} Traces + Seguimientos de {0} {0} is an application name Resources - Resources + Recursos Total spans - Total spans + Intervalos totales Trace "{0}" not found - Trace "{0}" not found + Seguimiento "{0}" no encontrado {0} is an identifier Trace detail - Trace detail + Detalle del seguimiento diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.fr.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.fr.xlf index ce26915f500..13970e71b84 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.fr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.fr.xlf @@ -4,37 +4,37 @@ Depth - Depth + Profondeur Duration - Duration + Durée {0} Traces - {0} Traces + Traces {0} {0} is an application name Resources - Resources + Ressources Total spans - Total spans + Nombre total d’étendues Trace "{0}" not found - Trace "{0}" not found + Trace « {0} » introuvable {0} is an identifier Trace detail - Trace detail + Détails de la trace diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.it.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.it.xlf index 8d9d6e9a5e3..7aef00a25be 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.it.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.it.xlf @@ -4,37 +4,37 @@ Depth - Depth + Profondità Duration - Duration + Durata {0} Traces - {0} Traces + {0} tracce {0} is an application name Resources - Resources + Risorse Total spans - Total spans + Intervalli totali Trace "{0}" not found - Trace "{0}" not found + Traccia "{0}" non trovata {0} is an identifier Trace detail - Trace detail + Dettagli traccia diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ja.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ja.xlf index b9f6c8e1653..30d727d8573 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ja.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ja.xlf @@ -4,37 +4,37 @@ Depth - Depth + 深さ Duration - Duration + 期間 {0} Traces - {0} Traces + {0} のトレース {0} is an application name Resources - Resources + リソース Total spans - Total spans + 合計範囲 Trace "{0}" not found - Trace "{0}" not found + トレース "{0}" が見つかりません {0} is an identifier Trace detail - Trace detail + トレースの詳細 diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ko.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ko.xlf index a412dc4ee2d..6fde03342ab 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ko.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ko.xlf @@ -4,37 +4,37 @@ Depth - Depth + 깊이 Duration - Duration + 기간 {0} Traces - {0} Traces + {0} 추적 {0} is an application name Resources - Resources + 리소스 Total spans - Total spans + 총 범위 Trace "{0}" not found - Trace "{0}" not found + 추적 "{0}"을(를) 찾을 수 없음 {0} is an identifier Trace detail - Trace detail + 추적 세부 정보 diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.pl.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.pl.xlf index db4f61deb9c..2903346fcad 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.pl.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.pl.xlf @@ -4,37 +4,37 @@ Depth - Depth + Głębokość Duration - Duration + Czas trwania {0} Traces - {0} Traces + Ślady {0} {0} is an application name Resources - Resources + Zasoby Total spans - Total spans + Łączna liczba zakresów Trace "{0}" not found - Trace "{0}" not found + Nie znaleziono śladu „{0}” {0} is an identifier Trace detail - Trace detail + Szczegóły śledzenia diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.pt-BR.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.pt-BR.xlf index 75e9194fd6f..c77fc3f7854 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.pt-BR.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.pt-BR.xlf @@ -4,37 +4,37 @@ Depth - Depth + Profundidade Duration - Duration + Duração {0} Traces - {0} Traces + {0} Rastreamentos {0} is an application name Resources - Resources + Recursos Total spans - Total spans + Intervalos totais Trace "{0}" not found - Trace "{0}" not found + Rastreamento"{0}" não encontrado {0} is an identifier Trace detail - Trace detail + Detalhes do rastreamento diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ru.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ru.xlf index a0a68be70bd..a982f349a69 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ru.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ru.xlf @@ -4,37 +4,37 @@ Depth - Depth + Глубина Duration - Duration + Длительность {0} Traces - {0} Traces + Трассировок: {0} {0} is an application name Resources - Resources + Ресурсы Total spans - Total spans + Всего диапазонов Trace "{0}" not found - Trace "{0}" not found + Трассировка "{0}" не найдена {0} is an identifier Trace detail - Trace detail + Детали трассировки diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.tr.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.tr.xlf index 0da5d07633f..2e1a8e977f3 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.tr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.tr.xlf @@ -4,37 +4,37 @@ Depth - Depth + Derinlik Duration - Duration + Süre {0} Traces - {0} Traces + {0} İzlemeleri {0} is an application name Resources - Resources + Kaynaklar Total spans - Total spans + Toplam yayılma sayısı Trace "{0}" not found - Trace "{0}" not found + "{0}" izi bulunamadı {0} is an identifier Trace detail - Trace detail + İzleme ayrıntıları diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.zh-Hans.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.zh-Hans.xlf index 9f154c9b657..8ade4ecb89f 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.zh-Hans.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.zh-Hans.xlf @@ -4,37 +4,37 @@ Depth - Depth + 深度 Duration - Duration + 持续时间 {0} Traces - {0} Traces + {0} 跟踪 {0} is an application name Resources - Resources + 资源 Total spans - Total spans + 总跨度 Trace "{0}" not found - Trace "{0}" not found + 找不到跟踪“{0}” {0} is an identifier Trace detail - Trace detail + 跟踪详细信息 diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.zh-Hant.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.zh-Hant.xlf index 13cbfbe6b27..ce255572af3 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.zh-Hant.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.zh-Hant.xlf @@ -4,37 +4,37 @@ Depth - Depth + 深度 Duration - Duration + 期間 {0} Traces - {0} Traces + {0} 追蹤 {0} is an application name Resources - Resources + 資源 Total spans - Total spans + 總範圍 Trace "{0}" not found - Trace "{0}" not found + 找不到追蹤 "{0}" {0} is an identifier Trace detail - Trace detail + 追蹤詳細資料 diff --git a/src/Aspire.Dashboard/Resources/xlf/Traces.cs.xlf b/src/Aspire.Dashboard/Resources/xlf/Traces.cs.xlf index abbafc8b080..642e99e1ce3 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Traces.cs.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Traces.cs.xlf @@ -4,52 +4,52 @@ Name: {0} - Name: {0} + Název: {0} Traces - Traces + Trasování Name filter - Name filter + Filtr názvů No traces found - No traces found + Nenašla se žádná trasování. {0} traces - {0} traces + Trasování aplikace {0} {0} is an application name {0} spans - {0} spans + Rozsahy prostředku {0} {0} is a resource name Spans - Spans + Rozsahy Errored: {0} - Errored: {0} + S chybami: {0} {0} is a number Total: {0} - Total: {0} + Celkem: {0} Trace Id: {0} - Trace Id: {0} + ID trasování: {0} diff --git a/src/Aspire.Dashboard/Resources/xlf/Traces.de.xlf b/src/Aspire.Dashboard/Resources/xlf/Traces.de.xlf index cc36781de26..078375a0cc0 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Traces.de.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Traces.de.xlf @@ -4,52 +4,52 @@ Name: {0} - Name: {0} + Name: {0} Traces - Traces + Überwachungen Name filter - Name filter + Namensfilter No traces found - No traces found + Keine Ablaufverfolgungen gefunden. {0} traces - {0} traces + {0} Ablaufverfolgungen {0} is an application name {0} spans - {0} spans + {0} Bereiche {0} is a resource name Spans - Spans + Bereiche Errored: {0} - Errored: {0} + Mit Fehler: {0} {0} is a number Total: {0} - Total: {0} + Gesamt: {0} Trace Id: {0} - Trace Id: {0} + Ablaufverfolgungs-ID: {0} diff --git a/src/Aspire.Dashboard/Resources/xlf/Traces.es.xlf b/src/Aspire.Dashboard/Resources/xlf/Traces.es.xlf index 38951119cbc..75b171efd45 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Traces.es.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Traces.es.xlf @@ -4,52 +4,52 @@ Name: {0} - Name: {0} + Nombre: {0} Traces - Traces + Seguimientos Name filter - Name filter + Filtro de nombres No traces found - No traces found + No se encontraron seguimientos {0} traces - {0} traces + Seguimientos de {0} {0} is an application name {0} spans - {0} spans + Intervalos de {0} {0} is a resource name Spans - Spans + Intervalos Errored: {0} - Errored: {0} + Con errores: {0} {0} is a number Total: {0} - Total: {0} + Total: {0} Trace Id: {0} - Trace Id: {0} + Id. de seguimiento: {0} diff --git a/src/Aspire.Dashboard/Resources/xlf/Traces.fr.xlf b/src/Aspire.Dashboard/Resources/xlf/Traces.fr.xlf index b033e077135..a1de62d0cb4 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Traces.fr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Traces.fr.xlf @@ -4,52 +4,52 @@ Name: {0} - Name: {0} + Nom : {0} Traces - Traces + Traces Name filter - Name filter + Nom du filtre No traces found - No traces found + Aucune trace trouvée {0} traces - {0} traces + Traces {0} {0} is an application name {0} spans - {0} spans + Étendues {0} {0} is a resource name Spans - Spans + Étendues Errored: {0} - Errored: {0} + En erreur : {0} {0} is a number Total: {0} - Total: {0} + Total : {0} Trace Id: {0} - Trace Id: {0} + ID de trace : {0} diff --git a/src/Aspire.Dashboard/Resources/xlf/Traces.it.xlf b/src/Aspire.Dashboard/Resources/xlf/Traces.it.xlf index 60db52b7df3..edef4785ad4 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Traces.it.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Traces.it.xlf @@ -4,52 +4,52 @@ Name: {0} - Name: {0} + Nome: {0} Traces - Traces + Tracce Name filter - Name filter + Nome del filtro No traces found - No traces found + Nessuna traccia trovata {0} traces - {0} traces + {0} tracce {0} is an application name {0} spans - {0} spans + {0} intervalli {0} is a resource name Spans - Spans + Intervalli Errored: {0} - Errored: {0} + In errore: {0} {0} is a number Total: {0} - Total: {0} + Totale: {0} Trace Id: {0} - Trace Id: {0} + ID traccia: {0} diff --git a/src/Aspire.Dashboard/Resources/xlf/Traces.ja.xlf b/src/Aspire.Dashboard/Resources/xlf/Traces.ja.xlf index 214a5dbb20b..4cd0177b262 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Traces.ja.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Traces.ja.xlf @@ -4,52 +4,52 @@ Name: {0} - Name: {0} + 名前: {0} Traces - Traces + トレース Name filter - Name filter + 名前フィルター No traces found - No traces found + トレースが見つかりません {0} traces - {0} traces + {0} のトレース {0} is an application name {0} spans - {0} spans + {0} の範囲 {0} is a resource name Spans - Spans + 範囲 Errored: {0} - Errored: {0} + エラーが発生しました: {0} {0} is a number Total: {0} - Total: {0} + 合計: {0} Trace Id: {0} - Trace Id: {0} + トレース ID: {0} diff --git a/src/Aspire.Dashboard/Resources/xlf/Traces.ko.xlf b/src/Aspire.Dashboard/Resources/xlf/Traces.ko.xlf index 2d6641eaa06..a019f2d33f2 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Traces.ko.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Traces.ko.xlf @@ -4,52 +4,52 @@ Name: {0} - Name: {0} + 이름: {0} Traces - Traces + 추적 Name filter - Name filter + 이름 필터 No traces found - No traces found + 추적을 찾을 수 없음 {0} traces - {0} traces + {0} 추적 {0} is an application name {0} spans - {0} spans + {0} 범위 {0} is a resource name Spans - Spans + 범위 Errored: {0} - Errored: {0} + 오류 발생: {0} {0} is a number Total: {0} - Total: {0} + 합계: {0} Trace Id: {0} - Trace Id: {0} + 추적 ID: {0} diff --git a/src/Aspire.Dashboard/Resources/xlf/Traces.pl.xlf b/src/Aspire.Dashboard/Resources/xlf/Traces.pl.xlf index 69ff95e32c4..6ab8322976d 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Traces.pl.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Traces.pl.xlf @@ -4,52 +4,52 @@ Name: {0} - Name: {0} + Nazwa: {0} Traces - Traces + Ślady Name filter - Name filter + Filtr nazw No traces found - No traces found + Nie znaleziono śladów {0} traces - {0} traces + Ślady {0} {0} is an application name {0} spans - {0} spans + {0} zakresy {0} is a resource name Spans - Spans + Zakresy Errored: {0} - Errored: {0} + Błąd: {0} {0} is a number Total: {0} - Total: {0} + Suma: {0} Trace Id: {0} - Trace Id: {0} + Identyfikator śledzenia: {0} diff --git a/src/Aspire.Dashboard/Resources/xlf/Traces.pt-BR.xlf b/src/Aspire.Dashboard/Resources/xlf/Traces.pt-BR.xlf index 28fd222be56..ae2873ef848 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Traces.pt-BR.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Traces.pt-BR.xlf @@ -4,52 +4,52 @@ Name: {0} - Name: {0} + Nome: {0} Traces - Traces + Rastreamentos Name filter - Name filter + Filtro de nome No traces found - No traces found + Nenhum rastreamento encontrado {0} traces - {0} traces + {0} rastreamentos {0} is an application name {0} spans - {0} spans + {0} intervalos {0} is a resource name Spans - Spans + Intervalos Errored: {0} - Errored: {0} + Erro: {0} {0} is a number Total: {0} - Total: {0} + Total: {0} Trace Id: {0} - Trace Id: {0} + ID de rastreamento: {0} diff --git a/src/Aspire.Dashboard/Resources/xlf/Traces.ru.xlf b/src/Aspire.Dashboard/Resources/xlf/Traces.ru.xlf index baba53d38b3..68ba8f86990 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Traces.ru.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Traces.ru.xlf @@ -4,52 +4,52 @@ Name: {0} - Name: {0} + Имя: {0} Traces - Traces + Трассировки Name filter - Name filter + Фильтр имен No traces found - No traces found + Трассировки не найдены {0} traces - {0} traces + Трассировок: {0} {0} is an application name {0} spans - {0} spans + Диапазонов: {0} {0} is a resource name Spans - Spans + Охватывает Errored: {0} - Errored: {0} + С ошибками: {0} {0} is a number Total: {0} - Total: {0} + Итого: {0} Trace Id: {0} - Trace Id: {0} + Идентификатор трассировки: {0} diff --git a/src/Aspire.Dashboard/Resources/xlf/Traces.tr.xlf b/src/Aspire.Dashboard/Resources/xlf/Traces.tr.xlf index dda3d23617d..c94d464d5b0 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Traces.tr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Traces.tr.xlf @@ -4,52 +4,52 @@ Name: {0} - Name: {0} + Ad: {0} Traces - Traces + İzlemeler Name filter - Name filter + Ad filtresi No traces found - No traces found + İzleme bulunamadı {0} traces - {0} traces + {0} izlemeleri {0} is an application name {0} spans - {0} spans + {0} yayılmaları {0} is a resource name Spans - Spans + Yayılmalar Errored: {0} - Errored: {0} + Hata oluştu: {0} {0} is a number Total: {0} - Total: {0} + Toplam: {0} Trace Id: {0} - Trace Id: {0} + İzleme Kimliği: {0} diff --git a/src/Aspire.Dashboard/Resources/xlf/Traces.zh-Hans.xlf b/src/Aspire.Dashboard/Resources/xlf/Traces.zh-Hans.xlf index 05e72fc9f51..d3669c5f42a 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Traces.zh-Hans.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Traces.zh-Hans.xlf @@ -4,52 +4,52 @@ Name: {0} - Name: {0} + 名称: {0} Traces - Traces + 跟踪 Name filter - Name filter + 名称筛选器 No traces found - No traces found + 未找到任何跟踪 {0} traces - {0} traces + {0} 跟踪 {0} is an application name {0} spans - {0} spans + {0} 范围 {0} is a resource name Spans - Spans + 范围 Errored: {0} - Errored: {0} + 出错: {0} 个 {0} is a number Total: {0} - Total: {0} + 总计: {0} 个 Trace Id: {0} - Trace Id: {0} + 跟踪 ID: {0} diff --git a/src/Aspire.Dashboard/Resources/xlf/Traces.zh-Hant.xlf b/src/Aspire.Dashboard/Resources/xlf/Traces.zh-Hant.xlf index 12cd578a819..ecdfbd36df9 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Traces.zh-Hant.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Traces.zh-Hant.xlf @@ -4,52 +4,52 @@ Name: {0} - Name: {0} + 名稱: {0} Traces - Traces + 追蹤 Name filter - Name filter + 名稱篩選 No traces found - No traces found + 找不到任何追蹤 {0} traces - {0} traces + {0} 追蹤 {0} is an application name {0} spans - {0} spans + {0} 範圍 {0} is a resource name Spans - Spans + 範圍 Errored: {0} - Errored: {0} + Errored: {0} {0} is a number Total: {0} - Total: {0} + 總計: {0} Trace Id: {0} - Trace Id: {0} + 追蹤識別碼 {0} diff --git a/src/Aspire.Hosting/Properties/xlf/Resources.cs.xlf b/src/Aspire.Hosting/Properties/xlf/Resources.cs.xlf index 437b39d731c..c5efe713652 100644 --- a/src/Aspire.Hosting/Properties/xlf/Resources.cs.xlf +++ b/src/Aspire.Hosting/Properties/xlf/Resources.cs.xlf @@ -4,57 +4,57 @@ Container runtime '{0}' could not be found. The error from the container runtime check was: {1}. - Container runtime '{0}' could not be found. The error from the container runtime check was: {1}. + Nepovedlo se najít modul runtime {0} kontejneru. Chyba z kontroly modulu runtime kontejneru byla: {1}. Container runtime '{0}' was found but appears to be unhealthy. The error from the container runtime check was {1}. - Container runtime '{0}' was found but appears to be unhealthy. The error from the container runtime check was {1}. + Modul runtime kontejneru {0} byl nalezen, ale zdá se, že není v pořádku. Chyba z kontroly modulu runtime kontejneru byla: {1}. Container runtime '{0}' was found but appears to be unresponsive. The command '{0}' did not return after {1} seconds. - Container runtime '{0}' was found but appears to be unresponsive. The command '{0}' did not return after {1} seconds. + Modul runtime kontejneru {0} byl nalezen, ale zdá se, že nereaguje. Příkaz {0} po {1} sekundách nevrátil žádný výsledek. Application orchestrator dependency check had an unexpected error {0}. - Application orchestrator dependency check had an unexpected error {0}. + Při kontrole závislosti orchestrátoru aplikace došlo k neočekávané chybě {0}. The application orchestrator version could not be determined. Continuing. - The application orchestrator version could not be determined. Continuing. + Nebylo možné určit verzi orchestrátoru aplikace. Pokračuje se ve zpracování. Found incompatible version of .NET Aspire workload (need application orchestrator version {0} to run the application). - Found incompatible version of .NET Aspire workload (need application orchestrator version {0} to run the application). + Byla nalezena nekompatibilní verze úlohy .NET Aspire (ke spuštění aplikace je potřeba orchestrátor aplikace ve verzi {0}). Newer version of .NET Aspire workload is required to run the application. Run 'dotnet workload update' to get it. - Newer version of .NET Aspire workload is required to run the application. Run 'dotnet workload update' to get it. + Ke spuštění aplikace se vyžaduje novější verze úlohy .NET Aspire. Pokud ji chcete získat, spusťte příkaz dotnet workload update. Launch profile is specified but launch settings file is not present. - Launch profile is specified but launch settings file is not present. + Profil spuštění je zadán, ale soubor nastavení pro spuštění není k dispozici. Launch settings file does not contain '{0}' profile. - Launch settings file does not contain '{0}' profile. + Soubor nastavení pro spuštění neobsahuje profil {0}. Project does not contain project metadata. - Project does not contain project metadata. + Projekt neobsahuje metadata projektu. Project file '{0}' was not found. - Project file '{0}' was not found. + Soubor projektu {0} nebyl nalezen. diff --git a/src/Aspire.Hosting/Properties/xlf/Resources.de.xlf b/src/Aspire.Hosting/Properties/xlf/Resources.de.xlf index 4c5e4d18290..450bcfc5a30 100644 --- a/src/Aspire.Hosting/Properties/xlf/Resources.de.xlf +++ b/src/Aspire.Hosting/Properties/xlf/Resources.de.xlf @@ -4,57 +4,57 @@ Container runtime '{0}' could not be found. The error from the container runtime check was: {1}. - Container runtime '{0}' could not be found. The error from the container runtime check was: {1}. + Die Containerruntime "{0}" wurde nicht gefunden. Fehler bei der Überprüfung der Containerruntime: {1}. Container runtime '{0}' was found but appears to be unhealthy. The error from the container runtime check was {1}. - Container runtime '{0}' was found but appears to be unhealthy. The error from the container runtime check was {1}. + Die Containerruntime "{0}" wurde gefunden, scheint aber fehlerhaft zu sein. Fehler bei der Überprüfung der Containerruntime: {1}. Container runtime '{0}' was found but appears to be unresponsive. The command '{0}' did not return after {1} seconds. - Container runtime '{0}' was found but appears to be unresponsive. The command '{0}' did not return after {1} seconds. + Die Containerruntime "{0}" wurde gefunden, scheint aber nicht zu reagieren. Der Befehl "{0}" wurde nach {1} Sekunden nicht zurückgegeben. Application orchestrator dependency check had an unexpected error {0}. - Application orchestrator dependency check had an unexpected error {0}. + Bei der Abhängigkeitsprüfung des Anwendungsorchestrators ist ein unerwarteter Fehler aufgetreten: {0}. The application orchestrator version could not be determined. Continuing. - The application orchestrator version could not be determined. Continuing. + Die Version des Anwendungsorchestrators konnte nicht bestimmt werden. Vorgang wird fortgesetzt. Found incompatible version of .NET Aspire workload (need application orchestrator version {0} to run the application). - Found incompatible version of .NET Aspire workload (need application orchestrator version {0} to run the application). + Es wurde eine inkompatible Version der Workload ".NET Aspire" gefunden (für die Ausführung der Anwendung ist die Orchestratorversion {0} erforderlich). Newer version of .NET Aspire workload is required to run the application. Run 'dotnet workload update' to get it. - Newer version of .NET Aspire workload is required to run the application. Run 'dotnet workload update' to get it. + Zum Ausführen der Anwendung ist eine neuere Version der Workload ".NET Aspire" erforderlich. Führen Sie "dotnet workload update" aus, um es abzurufen. Launch profile is specified but launch settings file is not present. - Launch profile is specified but launch settings file is not present. + Das Startprofil wurde angegeben, aber die Starteinstellungsdatei ist nicht vorhanden. Launch settings file does not contain '{0}' profile. - Launch settings file does not contain '{0}' profile. + Die Starteinstellungsdatei enthält nicht das Profil "{0}". Project does not contain project metadata. - Project does not contain project metadata. + Das Projekt enthält keine Projektmetadaten. Project file '{0}' was not found. - Project file '{0}' was not found. + Die Projektdatei "{0}" wurde nicht gefunden. diff --git a/src/Aspire.Hosting/Properties/xlf/Resources.es.xlf b/src/Aspire.Hosting/Properties/xlf/Resources.es.xlf index 844458c37d9..4b8aa7ece36 100644 --- a/src/Aspire.Hosting/Properties/xlf/Resources.es.xlf +++ b/src/Aspire.Hosting/Properties/xlf/Resources.es.xlf @@ -4,57 +4,57 @@ Container runtime '{0}' could not be found. The error from the container runtime check was: {1}. - Container runtime '{0}' could not be found. The error from the container runtime check was: {1}. + No se encontró el runtime del contenedor ''{0}''. El error de la comprobación del runtime del contenedor fue: {1}. Container runtime '{0}' was found but appears to be unhealthy. The error from the container runtime check was {1}. - Container runtime '{0}' was found but appears to be unhealthy. The error from the container runtime check was {1}. + Se encontró el runtime de contenedor ''{0}'', pero parece que está en un estado incorrecto. El error de la comprobación del runtime del contenedor fue {1}. Container runtime '{0}' was found but appears to be unresponsive. The command '{0}' did not return after {1} seconds. - Container runtime '{0}' was found but appears to be unresponsive. The command '{0}' did not return after {1} seconds. + Se encontró el runtime de contenedor ''{0}'', pero parece que no responde. El comando ''{0}'' no se devolvió después de {1} segundos. Application orchestrator dependency check had an unexpected error {0}. - Application orchestrator dependency check had an unexpected error {0}. + La comprobación de dependencia del orquestador de aplicación tuvo un error inesperado {0}. The application orchestrator version could not be determined. Continuing. - The application orchestrator version could not be determined. Continuing. + No se pudo determinar la versión del orquestador de aplicación. Continuando. Found incompatible version of .NET Aspire workload (need application orchestrator version {0} to run the application). - Found incompatible version of .NET Aspire workload (need application orchestrator version {0} to run the application). + Se encontró una versión incompatible de la carga de trabajo de .NET Aspire (se necesita la versión {0} del orquestador de aplicación para ejecutar la aplicación). Newer version of .NET Aspire workload is required to run the application. Run 'dotnet workload update' to get it. - Newer version of .NET Aspire workload is required to run the application. Run 'dotnet workload update' to get it. + Se requiere una versión más reciente de la carga de trabajo de .NET Aspire para ejecutar la aplicación. Ejecute "dotnet workload update" para obtenerla. Launch profile is specified but launch settings file is not present. - Launch profile is specified but launch settings file is not present. + Se especificó el perfil de inicio, pero el archivo de configuración de inicio no está presente. Launch settings file does not contain '{0}' profile. - Launch settings file does not contain '{0}' profile. + El archivo de configuración de inicio no contiene el perfil ''{0}''. Project does not contain project metadata. - Project does not contain project metadata. + El proyecto no contiene metadatos de proyecto. Project file '{0}' was not found. - Project file '{0}' was not found. + No se encontró el archivo de proyecto "{0}". diff --git a/src/Aspire.Hosting/Properties/xlf/Resources.fr.xlf b/src/Aspire.Hosting/Properties/xlf/Resources.fr.xlf index 96e21413c9b..44b26b1526c 100644 --- a/src/Aspire.Hosting/Properties/xlf/Resources.fr.xlf +++ b/src/Aspire.Hosting/Properties/xlf/Resources.fr.xlf @@ -4,57 +4,57 @@ Container runtime '{0}' could not be found. The error from the container runtime check was: {1}. - Container runtime '{0}' could not be found. The error from the container runtime check was: {1}. + Le runtime du conteneur '{0}' est introuvable. L’erreur à partir de la vérification du runtime du conteneur était : {1}. Container runtime '{0}' was found but appears to be unhealthy. The error from the container runtime check was {1}. - Container runtime '{0}' was found but appears to be unhealthy. The error from the container runtime check was {1}. + Le runtime de conteneur « {0} » a été trouvé, mais il semble qu’il ne soit pas sain. L’erreur à partir de la vérification du runtime du conteneur était {1}. Container runtime '{0}' was found but appears to be unresponsive. The command '{0}' did not return after {1} seconds. - Container runtime '{0}' was found but appears to be unresponsive. The command '{0}' did not return after {1} seconds. + Le runtime de conteneur « {0} » a été trouvé, mais il semble qu’il ne réponde pas. La commande « {0} » n’a pas été retournée après {1} secondes. Application orchestrator dependency check had an unexpected error {0}. - Application orchestrator dependency check had an unexpected error {0}. + La vérification de dépendance de l’orchestrateur d’application a rencontré une erreur inattendue {0}. The application orchestrator version could not be determined. Continuing. - The application orchestrator version could not be determined. Continuing. + Désolé... Nous n’avons pas pu déterminer la version de l’orchestrateur d’application. Toujours en cours. Found incompatible version of .NET Aspire workload (need application orchestrator version {0} to run the application). - Found incompatible version of .NET Aspire workload (need application orchestrator version {0} to run the application). + Version incompatible trouvée de la charge de travail .NET Aspire (vous devez avoir une version d’orchestrateur d’application {0} pour exécuter l’application). Newer version of .NET Aspire workload is required to run the application. Run 'dotnet workload update' to get it. - Newer version of .NET Aspire workload is required to run the application. Run 'dotnet workload update' to get it. + Une version plus récente de la charge de travail .NET Aspire est nécessaire pour exécuter l’application. Exécutez « dotnet workload update » pour l’obtenir. Launch profile is specified but launch settings file is not present. - Launch profile is specified but launch settings file is not present. + Le profil de lancement est spécifié, mais le fichier des paramètres de lancement n’est pas présent. Launch settings file does not contain '{0}' profile. - Launch settings file does not contain '{0}' profile. + Le fichier de paramètres de lancement ne contient pas le profil « {0} ». Project does not contain project metadata. - Project does not contain project metadata. + Le projet ne contient aucune métadonnée de projet. Project file '{0}' was not found. - Project file '{0}' was not found. + Le fichier du projet « {0} » est introuvable. diff --git a/src/Aspire.Hosting/Properties/xlf/Resources.it.xlf b/src/Aspire.Hosting/Properties/xlf/Resources.it.xlf index 257463d3ef0..4ef060ca148 100644 --- a/src/Aspire.Hosting/Properties/xlf/Resources.it.xlf +++ b/src/Aspire.Hosting/Properties/xlf/Resources.it.xlf @@ -4,57 +4,57 @@ Container runtime '{0}' could not be found. The error from the container runtime check was: {1}. - Container runtime '{0}' could not be found. The error from the container runtime check was: {1}. + Non è stato possibile trovare il runtime del contenitore "{0}". Errore del controllo del runtime del contenitore: {1}. Container runtime '{0}' was found but appears to be unhealthy. The error from the container runtime check was {1}. - Container runtime '{0}' was found but appears to be unhealthy. The error from the container runtime check was {1}. + È stato trovato il runtime del contenitore "{0}", ma non è integro. Errore del controllo del runtime del contenitore: {1}. Container runtime '{0}' was found but appears to be unresponsive. The command '{0}' did not return after {1} seconds. - Container runtime '{0}' was found but appears to be unresponsive. The command '{0}' did not return after {1} seconds. + È stato trovato il runtime del contenitore "{0}", ma non risponde. Il comando "{0}" non ha risposto entro {1} secondi. Application orchestrator dependency check had an unexpected error {0}. - Application orchestrator dependency check had an unexpected error {0}. + Si è verificato un errore imprevisto durante il controllo delle dipendenze dell'agente di orchestrazione dell'applicazione: {0}. The application orchestrator version could not be determined. Continuing. - The application orchestrator version could not be determined. Continuing. + Non è stato possibile determinare la versione dell'agente di orchestrazione dell'applicazione. Continua. Found incompatible version of .NET Aspire workload (need application orchestrator version {0} to run the application). - Found incompatible version of .NET Aspire workload (need application orchestrator version {0} to run the application). + È stata trovata una versione incompatibile del carico di lavoro di .NET Aspire (per eseguire l'applicazione, è necessaria la versione {0} dell'agente di orchestrazione corrispondente). Newer version of .NET Aspire workload is required to run the application. Run 'dotnet workload update' to get it. - Newer version of .NET Aspire workload is required to run the application. Run 'dotnet workload update' to get it. + Per eseguire l'applicazione, è necessaria una versione più recente del carico di lavoro di .NET Aspire. Per ottenerla, esegui "aggiornamento del carico di lavoro dotnet". Launch profile is specified but launch settings file is not present. - Launch profile is specified but launch settings file is not present. + Il profilo di avvio è specificato, ma il file delle impostazioni di avvio non è presente. Launch settings file does not contain '{0}' profile. - Launch settings file does not contain '{0}' profile. + Il file delle impostazioni di avvio non contiene il profilo "{0}". Project does not contain project metadata. - Project does not contain project metadata. + Il progetto non contiene metadati del progetto. Project file '{0}' was not found. - Project file '{0}' was not found. + File di progetto "{0}" non trovato. diff --git a/src/Aspire.Hosting/Properties/xlf/Resources.ja.xlf b/src/Aspire.Hosting/Properties/xlf/Resources.ja.xlf index 59876240bed..90e5597efcc 100644 --- a/src/Aspire.Hosting/Properties/xlf/Resources.ja.xlf +++ b/src/Aspire.Hosting/Properties/xlf/Resources.ja.xlf @@ -4,57 +4,57 @@ Container runtime '{0}' could not be found. The error from the container runtime check was: {1}. - Container runtime '{0}' could not be found. The error from the container runtime check was: {1}. + コンテナー ランタイム '{0}' が見つかりませんでした。コンテナー ランタイム チェックのエラー: {1}。 Container runtime '{0}' was found but appears to be unhealthy. The error from the container runtime check was {1}. - Container runtime '{0}' was found but appears to be unhealthy. The error from the container runtime check was {1}. + コンテナー ランタイム '{0}' が見つかりましたが、異常な可能性があります。コンテナー ランタイム チェックのエラー: {1}。 Container runtime '{0}' was found but appears to be unresponsive. The command '{0}' did not return after {1} seconds. - Container runtime '{0}' was found but appears to be unresponsive. The command '{0}' did not return after {1} seconds. + コンテナー ランタイム '{0}' が見つかりましたが、応答していないようです。コマンド '{0}' が {1} 秒後に返されませんでした。 Application orchestrator dependency check had an unexpected error {0}. - Application orchestrator dependency check had an unexpected error {0}. + アプリケーション オーケストレーターの依存関係チェックで予期しないエラー {0} が発生しました。 The application orchestrator version could not be determined. Continuing. - The application orchestrator version could not be determined. Continuing. + アプリケーション オーケストレーターのバージョンを特定できませんでした。継続中。 Found incompatible version of .NET Aspire workload (need application orchestrator version {0} to run the application). - Found incompatible version of .NET Aspire workload (need application orchestrator version {0} to run the application). + 互換性のないバージョンの .NET Aspire ワークロードが見つかりました (アプリケーションを実行するには、アプリケーション オーケストレーターバージョン {0} が必要です)。 Newer version of .NET Aspire workload is required to run the application. Run 'dotnet workload update' to get it. - Newer version of .NET Aspire workload is required to run the application. Run 'dotnet workload update' to get it. + アプリケーションを実行するには、新しいバージョンの .NET Aspire ワークロードが必要です。'dotnet workload update' を実行して取得します。 Launch profile is specified but launch settings file is not present. - Launch profile is specified but launch settings file is not present. + 起動プロファイルが指定されていますが、起動設定ファイルが存在しません。 Launch settings file does not contain '{0}' profile. - Launch settings file does not contain '{0}' profile. + 起動設定ファイルに '{0}' プロファイルが含まれていません。 Project does not contain project metadata. - Project does not contain project metadata. + プロジェクトにプロジェクト メタデータが含まれていません。 Project file '{0}' was not found. - Project file '{0}' was not found. + プロジェクト ファイル '{0}' が見つかりませんでした。 diff --git a/src/Aspire.Hosting/Properties/xlf/Resources.ko.xlf b/src/Aspire.Hosting/Properties/xlf/Resources.ko.xlf index 41745605e54..b5b4335e2ee 100644 --- a/src/Aspire.Hosting/Properties/xlf/Resources.ko.xlf +++ b/src/Aspire.Hosting/Properties/xlf/Resources.ko.xlf @@ -4,57 +4,57 @@ Container runtime '{0}' could not be found. The error from the container runtime check was: {1}. - Container runtime '{0}' could not be found. The error from the container runtime check was: {1}. + 컨테이너 런타임 '{0}'(을)를 찾을 수 없습니다. 컨테이너 런타임 검사에서 오류가 발생했습니다. {1}. Container runtime '{0}' was found but appears to be unhealthy. The error from the container runtime check was {1}. - Container runtime '{0}' was found but appears to be unhealthy. The error from the container runtime check was {1}. + 컨테이너 런타임 '{0}'(을)를 찾았지만 비정상인 것 같습니다. 컨테이너 런타임 검사의 오류는 {1}이었습니다. Container runtime '{0}' was found but appears to be unresponsive. The command '{0}' did not return after {1} seconds. - Container runtime '{0}' was found but appears to be unresponsive. The command '{0}' did not return after {1} seconds. + 컨테이너 런타임 '{0}'(을)를 찾았지만 응답하지 않는 것 같습니다. '{0}' 명령이 {1}초 후에 반환되지 않았습니다. Application orchestrator dependency check had an unexpected error {0}. - Application orchestrator dependency check had an unexpected error {0}. + 응용 프로그램 오케스트레이터 종속성 검사에서 예기치 않은 오류 {0}(이)가 발생했습니다. The application orchestrator version could not be determined. Continuing. - The application orchestrator version could not be determined. Continuing. + 애플리케이션 오케스트레이터 버전을 확인할 수 없습니다. 계속. Found incompatible version of .NET Aspire workload (need application orchestrator version {0} to run the application). - Found incompatible version of .NET Aspire workload (need application orchestrator version {0} to run the application). + 호환되지 않는 버전의 .NET Aspire 워크로드를 찾았습니다(애플리케이션을 실행하려면 애플리케이션 오케스트레이터 버전 {0}(이)가 필요). Newer version of .NET Aspire workload is required to run the application. Run 'dotnet workload update' to get it. - Newer version of .NET Aspire workload is required to run the application. Run 'dotnet workload update' to get it. + 애플리케이션을 실행하려면 최신 버전의 .NET Aspire 워크로드가 필요합니다. 'dotnet 워크로드 업데이트'를 실행하여 가져옵니다. Launch profile is specified but launch settings file is not present. - Launch profile is specified but launch settings file is not present. + 시작 프로필이 지정되었지만 시작 설정 파일이 없습니다. Launch settings file does not contain '{0}' profile. - Launch settings file does not contain '{0}' profile. + 시작 설정 파일에 '{0}' 프로필이 없습니다. Project does not contain project metadata. - Project does not contain project metadata. + 프로젝트에 프로젝트 메타데이터가 없습니다. Project file '{0}' was not found. - Project file '{0}' was not found. + 프로젝트 파일 '{0}'을(를) 찾을 수 없습니다. diff --git a/src/Aspire.Hosting/Properties/xlf/Resources.pl.xlf b/src/Aspire.Hosting/Properties/xlf/Resources.pl.xlf index 483499c9eff..8d2058b8456 100644 --- a/src/Aspire.Hosting/Properties/xlf/Resources.pl.xlf +++ b/src/Aspire.Hosting/Properties/xlf/Resources.pl.xlf @@ -4,57 +4,57 @@ Container runtime '{0}' could not be found. The error from the container runtime check was: {1}. - Container runtime '{0}' could not be found. The error from the container runtime check was: {1}. + Nie można odnaleźć środowiska uruchomieniowego kontenera „{0}”. Błąd sprawdzania środowiska uruchomieniowego kontenera: {1}. Container runtime '{0}' was found but appears to be unhealthy. The error from the container runtime check was {1}. - Container runtime '{0}' was found but appears to be unhealthy. The error from the container runtime check was {1}. + Znaleziono środowisko uruchomieniowe kontenera „{0}”, ale wygląda na to, że jest w złej kondycji. Błąd podczas sprawdzania środowiska uruchomieniowego kontenera: {1}. Container runtime '{0}' was found but appears to be unresponsive. The command '{0}' did not return after {1} seconds. - Container runtime '{0}' was found but appears to be unresponsive. The command '{0}' did not return after {1} seconds. + Znaleziono środowisko uruchomieniowe kontenera „{0}”, ale wygląda na to, że nie odpowiada. Polecenie „{0}” nie zwróciło wyniku po {1} sekundach. Application orchestrator dependency check had an unexpected error {0}. - Application orchestrator dependency check had an unexpected error {0}. + Podczas sprawdzania zależności orkiestratora aplikacji wystąpił nieoczekiwany błąd {0}. The application orchestrator version could not be determined. Continuing. - The application orchestrator version could not be determined. Continuing. + Nie można określić wersji orkiestratora aplikacji. Trwa kontynuowanie. Found incompatible version of .NET Aspire workload (need application orchestrator version {0} to run the application). - Found incompatible version of .NET Aspire workload (need application orchestrator version {0} to run the application). + Znaleziono niezgodną wersję obciążenia programu .NET — Aspire (do uruchomienia aplikacji potrzebna jest wersja orkiestratora aplikacji {0}). Newer version of .NET Aspire workload is required to run the application. Run 'dotnet workload update' to get it. - Newer version of .NET Aspire workload is required to run the application. Run 'dotnet workload update' to get it. + Do uruchomienia aplikacji jest wymagana nowsza wersja obciążenia programu .NET — Aspire. Uruchom polecenie „dotnet workload update”, aby ją pobrać. Launch profile is specified but launch settings file is not present. - Launch profile is specified but launch settings file is not present. + Określono profil uruchamiania, ale plik ustawień uruchamiania nie istnieje. Launch settings file does not contain '{0}' profile. - Launch settings file does not contain '{0}' profile. + Plik ustawień uruchamiania nie zawiera profilu „{0}”. Project does not contain project metadata. - Project does not contain project metadata. + Projekt nie zawiera metadanych projektu. Project file '{0}' was not found. - Project file '{0}' was not found. + Nie znaleziono pliku projektu „{0}”. diff --git a/src/Aspire.Hosting/Properties/xlf/Resources.pt-BR.xlf b/src/Aspire.Hosting/Properties/xlf/Resources.pt-BR.xlf index 979eb38b2b6..1e363029a84 100644 --- a/src/Aspire.Hosting/Properties/xlf/Resources.pt-BR.xlf +++ b/src/Aspire.Hosting/Properties/xlf/Resources.pt-BR.xlf @@ -4,57 +4,57 @@ Container runtime '{0}' could not be found. The error from the container runtime check was: {1}. - Container runtime '{0}' could not be found. The error from the container runtime check was: {1}. + O runtime do contêiner '{0}' não pôde ser encontrado. O erro da verificação de runtime do contêiner foi: {1}. Container runtime '{0}' was found but appears to be unhealthy. The error from the container runtime check was {1}. - Container runtime '{0}' was found but appears to be unhealthy. The error from the container runtime check was {1}. + O runtime do contêiner '{0}' foi encontrado, mas parece não estar íntegro. O erro da verificação de runtime do contêiner foi {1}. Container runtime '{0}' was found but appears to be unresponsive. The command '{0}' did not return after {1} seconds. - Container runtime '{0}' was found but appears to be unresponsive. The command '{0}' did not return after {1} seconds. + O runtime do contêiner '{0}' foi encontrado, mas parece não responder. O comando '{0}' não retornou após {1} segundos. Application orchestrator dependency check had an unexpected error {0}. - Application orchestrator dependency check had an unexpected error {0}. + A verificação de dependência do orquestrador de aplicativos teve um erro inesperado {0}. The application orchestrator version could not be determined. Continuing. - The application orchestrator version could not be determined. Continuing. + Não foi possível determinar a versão do orquestrador de aplicativos. Continuando. Found incompatible version of .NET Aspire workload (need application orchestrator version {0} to run the application). - Found incompatible version of .NET Aspire workload (need application orchestrator version {0} to run the application). + Versão incompatível encontrada da carga de trabalho do .NET Aspire (é preciso a versão {0} do orquestrador de aplicativos para executar o aplicativo). Newer version of .NET Aspire workload is required to run the application. Run 'dotnet workload update' to get it. - Newer version of .NET Aspire workload is required to run the application. Run 'dotnet workload update' to get it. + A versão mais recente da carga de trabalho .NET Aspire é necessária para executar o aplicativo. Execute 'dotnet workload update' para obtê-la. Launch profile is specified but launch settings file is not present. - Launch profile is specified but launch settings file is not present. + O perfil de inicialização foi especificado, mas o arquivo de configurações de inicialização não está presente. Launch settings file does not contain '{0}' profile. - Launch settings file does not contain '{0}' profile. + O arquivo de configurações de inicialização não contém o perfil '{0}'. Project does not contain project metadata. - Project does not contain project metadata. + O projeto não contém metadados do projeto. Project file '{0}' was not found. - Project file '{0}' was not found. + O arquivo de projeto '{0}' não foi encontrado. diff --git a/src/Aspire.Hosting/Properties/xlf/Resources.ru.xlf b/src/Aspire.Hosting/Properties/xlf/Resources.ru.xlf index b307537a933..732812dc751 100644 --- a/src/Aspire.Hosting/Properties/xlf/Resources.ru.xlf +++ b/src/Aspire.Hosting/Properties/xlf/Resources.ru.xlf @@ -4,57 +4,57 @@ Container runtime '{0}' could not be found. The error from the container runtime check was: {1}. - Container runtime '{0}' could not be found. The error from the container runtime check was: {1}. + Не удалось найти среду выполнения контейнера "{0}". Ошибка, выданная при проверке среды выполнения контейнера: {1}. Container runtime '{0}' was found but appears to be unhealthy. The error from the container runtime check was {1}. - Container runtime '{0}' was found but appears to be unhealthy. The error from the container runtime check was {1}. + Обнаружена среда выполнения контейнера "{0}", но, судя по всему, неработоспособная. Ошибка, выданная при проверке среды выполнения контейнера: {1}. Container runtime '{0}' was found but appears to be unresponsive. The command '{0}' did not return after {1} seconds. - Container runtime '{0}' was found but appears to be unresponsive. The command '{0}' did not return after {1} seconds. + Среда выполнения контейнера "{0}" обнаружена, но не реагирует. Команда "{0}" не вернула управление по истечении {1} секунд. Application orchestrator dependency check had an unexpected error {0}. - Application orchestrator dependency check had an unexpected error {0}. + Проверка зависимости оркестратора приложений вернула непредвиденную ошибку {0}. The application orchestrator version could not be determined. Continuing. - The application orchestrator version could not be determined. Continuing. + Не удалось определить версию оркестратора приложений. Продолжаем работу. Found incompatible version of .NET Aspire workload (need application orchestrator version {0} to run the application). - Found incompatible version of .NET Aspire workload (need application orchestrator version {0} to run the application). + Обнаружена несовместимая версия рабочей нагрузки .NET Aspire (для запуска приложения необходим оркестратор приложений версии {0}). Newer version of .NET Aspire workload is required to run the application. Run 'dotnet workload update' to get it. - Newer version of .NET Aspire workload is required to run the application. Run 'dotnet workload update' to get it. + Для запуска приложения требуется более новая версия рабочей нагрузки .NET Aspire. Чтобы получить ее, используйте команду "dotnet workload update". Launch profile is specified but launch settings file is not present. - Launch profile is specified but launch settings file is not present. + Профиль запуска указан, но файл параметров запуска отсутствует. Launch settings file does not contain '{0}' profile. - Launch settings file does not contain '{0}' profile. + Файл параметров запуска не содержит профиля "{0}". Project does not contain project metadata. - Project does not contain project metadata. + Проект не содержит метаданных проекта. Project file '{0}' was not found. - Project file '{0}' was not found. + Файл проекта "{0}" не найден. diff --git a/src/Aspire.Hosting/Properties/xlf/Resources.tr.xlf b/src/Aspire.Hosting/Properties/xlf/Resources.tr.xlf index f729a4dc1e6..c458d6867ce 100644 --- a/src/Aspire.Hosting/Properties/xlf/Resources.tr.xlf +++ b/src/Aspire.Hosting/Properties/xlf/Resources.tr.xlf @@ -4,57 +4,57 @@ Container runtime '{0}' could not be found. The error from the container runtime check was: {1}. - Container runtime '{0}' could not be found. The error from the container runtime check was: {1}. + '{0}' kapsayıcı çalışma zamanı bulunamadı. Kapsayıcı çalışma zamanı denetimi hatası: {1}. Container runtime '{0}' was found but appears to be unhealthy. The error from the container runtime check was {1}. - Container runtime '{0}' was found but appears to be unhealthy. The error from the container runtime check was {1}. + '{0}' kapsayıcı çalışma zamanı bulundu, ancak iyi durumda değil. Kapsayıcı çalışma zamanı denetiminden gelen hata: {1}. Container runtime '{0}' was found but appears to be unresponsive. The command '{0}' did not return after {1} seconds. - Container runtime '{0}' was found but appears to be unresponsive. The command '{0}' did not return after {1} seconds. + '{0}' Kapsayıcı çalışma zamanı bulundu ancak yanıt vermiyor. '{0}' komutu {1} saniye sonra döndürülemedi. Application orchestrator dependency check had an unexpected error {0}. - Application orchestrator dependency check had an unexpected error {0}. + Uygulama düzenleyicisi bağımlılık denetiminde beklenmeyen bir hata oluştu {0}. The application orchestrator version could not be determined. Continuing. - The application orchestrator version could not be determined. Continuing. + Uygulama düzenleyicisi sürümü belirlenemedi. Devam ediyor. Found incompatible version of .NET Aspire workload (need application orchestrator version {0} to run the application). - Found incompatible version of .NET Aspire workload (need application orchestrator version {0} to run the application). + .NET Aspire iş yükünün uyumsuz bir sürümü bulundu (uygulamayı çalıştırmak için {0} uygulama düzenleyicisi sürümü gereklidir). Newer version of .NET Aspire workload is required to run the application. Run 'dotnet workload update' to get it. - Newer version of .NET Aspire workload is required to run the application. Run 'dotnet workload update' to get it. + Uygulamayı çalıştırmak için .NET Aspire iş yükünün daha yeni bir sürümü gereklidir. Almak için 'dotnet iş yükü güncelleştirmesini' çalıştırın. Launch profile is specified but launch settings file is not present. - Launch profile is specified but launch settings file is not present. + Başlatma profili belirtildi, ancak başlatma ayarları dosyası yok. Launch settings file does not contain '{0}' profile. - Launch settings file does not contain '{0}' profile. + Başlatma ayarları dosyası '{0}' profilini içermiyor. Project does not contain project metadata. - Project does not contain project metadata. + Proje, proje meta verilerini içermiyor. Project file '{0}' was not found. - Project file '{0}' was not found. + '{0}' proje dosyası bulunamadı. diff --git a/src/Aspire.Hosting/Properties/xlf/Resources.zh-Hans.xlf b/src/Aspire.Hosting/Properties/xlf/Resources.zh-Hans.xlf index 624f41c34e8..426f5d3fa0b 100644 --- a/src/Aspire.Hosting/Properties/xlf/Resources.zh-Hans.xlf +++ b/src/Aspire.Hosting/Properties/xlf/Resources.zh-Hans.xlf @@ -4,57 +4,57 @@ Container runtime '{0}' could not be found. The error from the container runtime check was: {1}. - Container runtime '{0}' could not be found. The error from the container runtime check was: {1}. + 找不到容器运行时“{0}”。容器运行时检查中的错误为: {1}。 Container runtime '{0}' was found but appears to be unhealthy. The error from the container runtime check was {1}. - Container runtime '{0}' was found but appears to be unhealthy. The error from the container runtime check was {1}. + 已找到容器运行时“{0}”,但似乎运行不正常。容器运行时检查中的错误为 {1}。 Container runtime '{0}' was found but appears to be unresponsive. The command '{0}' did not return after {1} seconds. - Container runtime '{0}' was found but appears to be unresponsive. The command '{0}' did not return after {1} seconds. + 已找到容器运行时“{0}”,但似乎没有响应。命令“{0}”在 {1} 秒后未返回。 Application orchestrator dependency check had an unexpected error {0}. - Application orchestrator dependency check had an unexpected error {0}. + 应用程序业务流程协调程序依赖项检查遇到意外错误 {0}。 The application orchestrator version could not be determined. Continuing. - The application orchestrator version could not be determined. Continuing. + 无法确定应用程序业务流程协调程序版本。正在继续。 Found incompatible version of .NET Aspire workload (need application orchestrator version {0} to run the application). - Found incompatible version of .NET Aspire workload (need application orchestrator version {0} to run the application). + 发现 .NET Aspire 工作负载的版本不兼容(需要应用程序业务流程协调程序版本 {0} 才能运行应用程序)。 Newer version of .NET Aspire workload is required to run the application. Run 'dotnet workload update' to get it. - Newer version of .NET Aspire workload is required to run the application. Run 'dotnet workload update' to get it. + 运行应用程序需要较新版本的 .NET Aspire 工作负载。运行“dotnet 工作负载更新”以获取它。 Launch profile is specified but launch settings file is not present. - Launch profile is specified but launch settings file is not present. + 已指定启动配置文件,但启动设置文件不存在。 Launch settings file does not contain '{0}' profile. - Launch settings file does not contain '{0}' profile. + 启动设置文件不包含“{0}”配置文件。 Project does not contain project metadata. - Project does not contain project metadata. + 项目不包含项目元数据。 Project file '{0}' was not found. - Project file '{0}' was not found. + 找不到项目文件“{0}”。 diff --git a/src/Aspire.Hosting/Properties/xlf/Resources.zh-Hant.xlf b/src/Aspire.Hosting/Properties/xlf/Resources.zh-Hant.xlf index 92a66c25f80..b247f7c712e 100644 --- a/src/Aspire.Hosting/Properties/xlf/Resources.zh-Hant.xlf +++ b/src/Aspire.Hosting/Properties/xlf/Resources.zh-Hant.xlf @@ -4,57 +4,57 @@ Container runtime '{0}' could not be found. The error from the container runtime check was: {1}. - Container runtime '{0}' could not be found. The error from the container runtime check was: {1}. + 找不到容器執行階段 '{0}'。容器執行階段檢查的錯誤為: {1}。 Container runtime '{0}' was found but appears to be unhealthy. The error from the container runtime check was {1}. - Container runtime '{0}' was found but appears to be unhealthy. The error from the container runtime check was {1}. + 找到了容器執行階段 '{0}',但似乎狀況不良。容器執行階段檢查中的錯誤為 {1}。 Container runtime '{0}' was found but appears to be unresponsive. The command '{0}' did not return after {1} seconds. - Container runtime '{0}' was found but appears to be unresponsive. The command '{0}' did not return after {1} seconds. + 已找到容器執行階段 '{0}',但似乎無回應。命令 '{0}' 在 {1} 秒後未傳回。 Application orchestrator dependency check had an unexpected error {0}. - Application orchestrator dependency check had an unexpected error {0}. + 應用程式協調器相依性檢查出現未預期的錯誤 {0}。 The application orchestrator version could not be determined. Continuing. - The application orchestrator version could not be determined. Continuing. + 無法確定應用程式協調器版本。繼續。 Found incompatible version of .NET Aspire workload (need application orchestrator version {0} to run the application). - Found incompatible version of .NET Aspire workload (need application orchestrator version {0} to run the application). + 發現不相容的 .NET Aspire 工作負載的版本 (需要應用程式協調器版本 {0} 才能執行應用程式)。 Newer version of .NET Aspire workload is required to run the application. Run 'dotnet workload update' to get it. - Newer version of .NET Aspire workload is required to run the application. Run 'dotnet workload update' to get it. + 執行應用程式需要較新版本的 .NET Aspire 工作負載。執行 'dotnet workload update' 以取得它。 Launch profile is specified but launch settings file is not present. - Launch profile is specified but launch settings file is not present. + 已指定啟動設定檔,但啟動設定檔案不存在。 Launch settings file does not contain '{0}' profile. - Launch settings file does not contain '{0}' profile. + 啟動設定檔案不包含 '{0}' 設定檔。 Project does not contain project metadata. - Project does not contain project metadata. + 專案不包含專案中繼資料。 Project file '{0}' was not found. - Project file '{0}' was not found. + 找不到專案檔 '{0}'。 From cb4e3b86f2dbabbe6ca8c030a3c4ca2b45f729c1 Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Sun, 10 Mar 2024 17:30:49 -0700 Subject: [PATCH 32/50] Update Pomelo to v8.0.1. Fixes #2690 (#2721) This reflects connection string validation changes from https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql/pull/1819. The connection string options required by Pomelo are not set by default for a MySqlDataSource, but are required when using that MySqlDataSource with Pomelo.EntityFrameworkCore.MySql. --- Directory.Packages.props | 2 +- .../TestProject.IntegrationServiceA/Program.cs | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 0e86f5bddf2..9f6e4a9c76e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -100,7 +100,7 @@ - + diff --git a/tests/testproject/TestProject.IntegrationServiceA/Program.cs b/tests/testproject/TestProject.IntegrationServiceA/Program.cs index 805f3a6a3b2..cfd24e45583 100644 --- a/tests/testproject/TestProject.IntegrationServiceA/Program.cs +++ b/tests/testproject/TestProject.IntegrationServiceA/Program.cs @@ -15,7 +15,16 @@ } if (!resourcesToSkip.Contains(TestResourceNames.mysql)) { - builder.AddMySqlDataSource("mysqldb"); + builder.AddMySqlDataSource("mysqldb", settings => + { + // add the connection string options required by Pomelo EF Core MySQL + var connectionStringBuilder = new MySqlConnector.MySqlConnectionStringBuilder(settings.ConnectionString!) + { + AllowUserVariables = true, + UseAffectedRows = false, + }; + settings.ConnectionString = connectionStringBuilder.ConnectionString; + }); if (!resourcesToSkip.Contains(TestResourceNames.pomelo)) { builder.AddMySqlDbContext("mysqldb", settings => settings.ServerVersion = "8.2.0-mysql"); From b47d6de59da8131336b7c1ede41c9d62a1537d79 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 12:38:35 +1100 Subject: [PATCH 33/50] Update dependencies from https://github.com/dotnet/arcade build 20240308.4 (#2766) Microsoft.SourceBuild.Intermediate.arcade , Microsoft.DotNet.Arcade.Sdk , Microsoft.DotNet.Build.Tasks.Installers , Microsoft.DotNet.Build.Tasks.Workloads , Microsoft.DotNet.Helix.Sdk , Microsoft.DotNet.RemoteExecutor , Microsoft.DotNet.SharedFramework.Sdk , Microsoft.DotNet.XUnitExtensions From Version 8.0.0-beta.24151.4 -> To Version 8.0.0-beta.24158.4 Co-authored-by: dotnet-maestro[bot] --- eng/Version.Details.xml | 32 +++++++++++------------ eng/Versions.props | 8 +++--- eng/common/SetupNugetSources.ps1 | 26 +++++++++--------- eng/common/templates-official/job/job.yml | 4 +++ eng/common/templates/job/job.yml | 4 +++ global.json | 6 ++--- 6 files changed, 44 insertions(+), 36 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index b84fa5d690d..5b9e7c7e0c3 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -121,42 +121,42 @@ - + https://github.com/dotnet/arcade - cbb61c3a9a42e7c3cce17ee453ff5ecdc7f69282 + 052a4b9e7a9bdb9744c86c05665f1b46e4d59b15 - + https://github.com/dotnet/arcade - cbb61c3a9a42e7c3cce17ee453ff5ecdc7f69282 + 052a4b9e7a9bdb9744c86c05665f1b46e4d59b15 - + https://github.com/dotnet/arcade - cbb61c3a9a42e7c3cce17ee453ff5ecdc7f69282 + 052a4b9e7a9bdb9744c86c05665f1b46e4d59b15 - + https://github.com/dotnet/arcade - cbb61c3a9a42e7c3cce17ee453ff5ecdc7f69282 + 052a4b9e7a9bdb9744c86c05665f1b46e4d59b15 - + https://github.com/dotnet/arcade - cbb61c3a9a42e7c3cce17ee453ff5ecdc7f69282 + 052a4b9e7a9bdb9744c86c05665f1b46e4d59b15 - + https://github.com/dotnet/arcade - cbb61c3a9a42e7c3cce17ee453ff5ecdc7f69282 + 052a4b9e7a9bdb9744c86c05665f1b46e4d59b15 - + https://github.com/dotnet/arcade - cbb61c3a9a42e7c3cce17ee453ff5ecdc7f69282 + 052a4b9e7a9bdb9744c86c05665f1b46e4d59b15 https://github.com/dotnet/xliff-tasks 73f0850939d96131c28cf6ea6ee5aacb4da0083a - + https://github.com/dotnet/arcade - cbb61c3a9a42e7c3cce17ee453ff5ecdc7f69282 + 052a4b9e7a9bdb9744c86c05665f1b46e4d59b15 diff --git a/eng/Versions.props b/eng/Versions.props index b5acadf3920..9f11eacc65c 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -20,10 +20,10 @@ 0.1.55 8.0.0-rc.1.23419.3 13.3.8825-net8-rc1 - 8.0.0-beta.24151.4 - 8.0.0-beta.24151.4 - 8.0.0-beta.24151.4 - 8.0.0-beta.24151.4 + 8.0.0-beta.24158.4 + 8.0.0-beta.24158.4 + 8.0.0-beta.24158.4 + 8.0.0-beta.24158.4 8.2.0 8.2.0 8.0.0 diff --git a/eng/common/SetupNugetSources.ps1 b/eng/common/SetupNugetSources.ps1 index 6c65e81925f..efa2fd72bfa 100644 --- a/eng/common/SetupNugetSources.ps1 +++ b/eng/common/SetupNugetSources.ps1 @@ -35,7 +35,7 @@ Set-StrictMode -Version 2.0 . $PSScriptRoot\tools.ps1 # Add source entry to PackageSources -function AddPackageSource($sources, $SourceName, $SourceEndPoint, $creds, $Username, $Password) { +function AddPackageSource($sources, $SourceName, $SourceEndPoint, $creds, $Username, $pwd) { $packageSource = $sources.SelectSingleNode("add[@key='$SourceName']") if ($packageSource -eq $null) @@ -48,12 +48,11 @@ function AddPackageSource($sources, $SourceName, $SourceEndPoint, $creds, $Usern else { Write-Host "Package source $SourceName already present." } - - AddCredential -Creds $creds -Source $SourceName -Username $Username -Password $Password + AddCredential -Creds $creds -Source $SourceName -Username $Username -pwd $pwd } # Add a credential node for the specified source -function AddCredential($creds, $source, $username, $password) { +function AddCredential($creds, $source, $username, $pwd) { # Looks for credential configuration for the given SourceName. Create it if none is found. $sourceElement = $creds.SelectSingleNode($Source) if ($sourceElement -eq $null) @@ -82,17 +81,18 @@ function AddCredential($creds, $source, $username, $password) { $passwordElement.SetAttribute("key", "ClearTextPassword") $sourceElement.AppendChild($passwordElement) | Out-Null } - $passwordElement.SetAttribute("value", $Password) + + $passwordElement.SetAttribute("value", $pwd) } -function InsertMaestroPrivateFeedCredentials($Sources, $Creds, $Username, $Password) { +function InsertMaestroPrivateFeedCredentials($Sources, $Creds, $Username, $pwd) { $maestroPrivateSources = $Sources.SelectNodes("add[contains(@key,'darc-int')]") Write-Host "Inserting credentials for $($maestroPrivateSources.Count) Maestro's private feeds." ForEach ($PackageSource in $maestroPrivateSources) { Write-Host "`tInserting credential for Maestro's feed:" $PackageSource.Key - AddCredential -Creds $creds -Source $PackageSource.Key -Username $Username -Password $Password + AddCredential -Creds $creds -Source $PackageSource.Key -Username $Username -pwd $pwd } } @@ -144,13 +144,13 @@ if ($disabledSources -ne $null) { $userName = "dn-bot" # Insert credential nodes for Maestro's private feeds -InsertMaestroPrivateFeedCredentials -Sources $sources -Creds $creds -Username $userName -Password $Password +InsertMaestroPrivateFeedCredentials -Sources $sources -Creds $creds -Username $userName -pwd $Password # 3.1 uses a different feed url format so it's handled differently here $dotnet31Source = $sources.SelectSingleNode("add[@key='dotnet3.1']") if ($dotnet31Source -ne $null) { - AddPackageSource -Sources $sources -SourceName "dotnet3.1-internal" -SourceEndPoint "https://pkgs.dev.azure.com/dnceng/_packaging/dotnet3.1-internal/nuget/v2" -Creds $creds -Username $userName -Password $Password - AddPackageSource -Sources $sources -SourceName "dotnet3.1-internal-transport" -SourceEndPoint "https://pkgs.dev.azure.com/dnceng/_packaging/dotnet3.1-internal-transport/nuget/v2" -Creds $creds -Username $userName -Password $Password + AddPackageSource -Sources $sources -SourceName "dotnet3.1-internal" -SourceEndPoint "https://pkgs.dev.azure.com/dnceng/_packaging/dotnet3.1-internal/nuget/v2" -Creds $creds -Username $userName -pwd $Password + AddPackageSource -Sources $sources -SourceName "dotnet3.1-internal-transport" -SourceEndPoint "https://pkgs.dev.azure.com/dnceng/_packaging/dotnet3.1-internal-transport/nuget/v2" -Creds $creds -Username $userName -pwd $Password } $dotnetVersions = @('5','6','7','8') @@ -159,9 +159,9 @@ foreach ($dotnetVersion in $dotnetVersions) { $feedPrefix = "dotnet" + $dotnetVersion; $dotnetSource = $sources.SelectSingleNode("add[@key='$feedPrefix']") if ($dotnetSource -ne $null) { - AddPackageSource -Sources $sources -SourceName "$feedPrefix-internal" -SourceEndPoint "https://pkgs.dev.azure.com/dnceng/internal/_packaging/$feedPrefix-internal/nuget/v2" -Creds $creds -Username $userName -Password $Password - AddPackageSource -Sources $sources -SourceName "$feedPrefix-internal-transport" -SourceEndPoint "https://pkgs.dev.azure.com/dnceng/internal/_packaging/$feedPrefix-internal-transport/nuget/v2" -Creds $creds -Username $userName -Password $Password + AddPackageSource -Sources $sources -SourceName "$feedPrefix-internal" -SourceEndPoint "https://pkgs.dev.azure.com/dnceng/internal/_packaging/$feedPrefix-internal/nuget/v2" -Creds $creds -Username $userName -pwd $Password + AddPackageSource -Sources $sources -SourceName "$feedPrefix-internal-transport" -SourceEndPoint "https://pkgs.dev.azure.com/dnceng/internal/_packaging/$feedPrefix-internal-transport/nuget/v2" -Creds $creds -Username $userName -pwd $Password } } -$doc.Save($filename) +$doc.Save($filename) \ No newline at end of file diff --git a/eng/common/templates-official/job/job.yml b/eng/common/templates-official/job/job.yml index 9e7bebe9af8..647e3f92e5f 100644 --- a/eng/common/templates-official/job/job.yml +++ b/eng/common/templates-official/job/job.yml @@ -15,6 +15,7 @@ parameters: timeoutInMinutes: '' variables: [] workspace: '' + templateContext: '' # Job base template specific parameters # See schema documentation - https://github.com/dotnet/arcade/blob/master/Documentation/AzureDevOps/TemplateSchema.md @@ -68,6 +69,9 @@ jobs: ${{ if ne(parameters.timeoutInMinutes, '') }}: timeoutInMinutes: ${{ parameters.timeoutInMinutes }} + ${{ if ne(parameters.templateContext, '') }}: + templateContext: ${{ parameters.templateContext }} + variables: - ${{ if ne(parameters.enableTelemetry, 'false') }}: - name: DOTNET_CLI_TELEMETRY_PROFILE diff --git a/eng/common/templates/job/job.yml b/eng/common/templates/job/job.yml index e24ca2f46f9..8ec5c4f2d9f 100644 --- a/eng/common/templates/job/job.yml +++ b/eng/common/templates/job/job.yml @@ -15,6 +15,7 @@ parameters: timeoutInMinutes: '' variables: [] workspace: '' + templateContext: '' # Job base template specific parameters # See schema documentation - https://github.com/dotnet/arcade/blob/master/Documentation/AzureDevOps/TemplateSchema.md @@ -68,6 +69,9 @@ jobs: ${{ if ne(parameters.timeoutInMinutes, '') }}: timeoutInMinutes: ${{ parameters.timeoutInMinutes }} + ${{ if ne(parameters.templateContext, '') }}: + templateContext: ${{ parameters.templateContext }} + variables: - ${{ if ne(parameters.enableTelemetry, 'false') }}: - name: DOTNET_CLI_TELEMETRY_PROFILE diff --git a/global.json b/global.json index abcaf569a91..503f5ec0492 100644 --- a/global.json +++ b/global.json @@ -8,9 +8,9 @@ "dotnet": "8.0.200" }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "8.0.0-beta.24151.4", - "Microsoft.DotNet.Helix.Sdk": "8.0.0-beta.24151.4", - "Microsoft.DotNet.SharedFramework.Sdk": "8.0.0-beta.24151.4", + "Microsoft.DotNet.Arcade.Sdk": "8.0.0-beta.24158.4", + "Microsoft.DotNet.Helix.Sdk": "8.0.0-beta.24158.4", + "Microsoft.DotNet.SharedFramework.Sdk": "8.0.0-beta.24158.4", "Microsoft.Build.NoTargets": "3.7.0" } } From 4b81de09e4c1c10107be0c2f9617686dbf1c02df Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Mon, 11 Mar 2024 09:42:57 +0800 Subject: [PATCH 34/50] Catch disconnect error in ConsoleLogs when quickly refreshing page (#2765) --- .../Components/Controls/ResourceSelect.razor.cs | 14 +++++++++++--- src/Aspire.Dashboard/Otlp/Storage/Subscription.cs | 4 ++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Aspire.Dashboard/Components/Controls/ResourceSelect.razor.cs b/src/Aspire.Dashboard/Components/Controls/ResourceSelect.razor.cs index 9d9fb77c1f4..083b1d39b6b 100644 --- a/src/Aspire.Dashboard/Components/Controls/ResourceSelect.razor.cs +++ b/src/Aspire.Dashboard/Components/Controls/ResourceSelect.razor.cs @@ -36,14 +36,22 @@ public partial class ResourceSelect /// Workaround for issue in fluent-select web component where the display value of the /// selected item doesn't update automatically when the item changes. /// - public ValueTask UpdateDisplayValueAsync() + public async ValueTask UpdateDisplayValueAsync() { if (JSRuntime is null || _resourceSelectComponent is null) { - return ValueTask.CompletedTask; + return; } - return JSRuntime.InvokeVoidAsync("updateFluentSelectDisplayValue", _resourceSelectComponent.Element); + try + { + await JSRuntime.InvokeVoidAsync("updateFluentSelectDisplayValue", _resourceSelectComponent.Element); + } + catch (JSDisconnectedException) + { + // Per https://learn.microsoft.com/aspnet/core/blazor/javascript-interoperability/?view=aspnetcore-7.0#javascript-interop-calls-without-a-circuit + // this is one of the calls that will fail if the circuit is disconnected, and we just need to catch the exception so it doesn't pollute the logs + } } private string? GetPopupHeight() diff --git a/src/Aspire.Dashboard/Otlp/Storage/Subscription.cs b/src/Aspire.Dashboard/Otlp/Storage/Subscription.cs index 35cd10a2ad6..9b3d9817830 100644 --- a/src/Aspire.Dashboard/Otlp/Storage/Subscription.cs +++ b/src/Aspire.Dashboard/Otlp/Storage/Subscription.cs @@ -50,7 +50,7 @@ private async Task TryQueueAsync(CancellationToken cancellationToken) var s = lastExecute.Value.Add(_telemetryRepository._subscriptionMinExecuteInterval) - DateTime.UtcNow; if (s > TimeSpan.Zero) { - Logger.LogDebug("Subscription '{Name}' minimum execute interval hit. Waiting {DelayInterval}.", Name, s); + Logger.LogTrace("Subscription '{Name}' minimum execute interval hit. Waiting {DelayInterval}.", Name, s); await Task.Delay(s, cancellationToken).ConfigureAwait(false); } } @@ -89,7 +89,7 @@ public void Execute() ExecutionContext.Restore(_executionContext); } - Logger.LogDebug("Subscription '{Name}' executing.", Name); + Logger.LogTrace("Subscription '{Name}' executing.", Name); await _callback().ConfigureAwait(false); _lastExecute = DateTime.UtcNow; } From 0759519fc643525bc1ca37b6ad09cbcfa122862b Mon Sep 17 00:00:00 2001 From: Igor Velikorossov Date: Mon, 11 Mar 2024 15:45:39 +1100 Subject: [PATCH 35/50] Public build pipeline (#2767) A copy of azure-pipelines.yml with stripped out non-public conditions. --- eng/pipelines/azure-pipelines-public.yml | 169 +++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 eng/pipelines/azure-pipelines-public.yml diff --git a/eng/pipelines/azure-pipelines-public.yml b/eng/pipelines/azure-pipelines-public.yml new file mode 100644 index 00000000000..04249a0d815 --- /dev/null +++ b/eng/pipelines/azure-pipelines-public.yml @@ -0,0 +1,169 @@ +trigger: + batch: true + branches: + include: + - main + - release/* + +pr: + branches: + include: + - main + - release/* + - feature/* + +variables: + - template: /eng/common/templates/variables/pool-providers.yml + + - name: _BuildConfig + value: Release + - name: Build.Arcade.ArtifactsPath + value: $(Build.SourcesDirectory)/artifacts/ + - name: Build.Arcade.LogsPath + value: $(Build.Arcade.ArtifactsPath)log/$(_BuildConfig)/ + - name: Build.Arcade.TestResultsPath + value: $(Build.Arcade.ArtifactsPath)TestResults/$(_BuildConfig)/ + + # needed for darc (dependency flow) publishing + - name: _PublishArgs + value: '' + - name: _OfficialBuildIdArgs + value: '' + # needed for signing + - name: _SignType + value: test + - name: _SignArgs + value: '' + - name: _Sign + value: false + +resources: + containers: + - container: LinuxContainer + image: mcr.microsoft.com/dotnet-buildtools/prereqs:cbl-mariner-2.0-fpm + +stages: + +# ---------------------------------------------------------------- +# This stage performs build, test, packaging +# ---------------------------------------------------------------- +- stage: build + displayName: Build + jobs: + - template: /eng/common/templates/jobs/jobs.yml + parameters: + artifacts: + publish: + artifacts: false + logs: true + manifests: true + enableMicrobuild: true + enablePublishUsingPipelines: true + publishAssetsImmediately: true + enablePublishTestResults: true + enableSourceBuild: true + testResultsFormat: vstest + enableSourceIndex: false + workspace: + clean: all + + jobs: + + - job: windows + # timeout accounts for wait times for helix agents up to 30mins + timeoutInMinutes: 60 + + pool: + name: $(DncEngPublicBuildPool) + demands: ImageOverride -equals windows.vs2022preview.amd64.open + + variables: + - name: _buildScript + value: $(Build.SourcesDirectory)/build.cmd -ci + + preSteps: + - checkout: self + fetchDepth: 1 + clean: true + + steps: + - template: /eng/pipelines/templates/BuildAndTest.yml + parameters: + runAsPublic: true + dotnetScript: $(Build.SourcesDirectory)/dotnet.cmd + buildScript: $(_buildScript) + buildConfig: $(_BuildConfig) + repoLogPath: $(Build.Arcade.LogsPath) + repoTestResultsPath: $(Build.Arcade.TestResultsPath) + isWindows: true + + - job: linux + # timeout accounts for wait times for helix agents up to 30mins + timeoutInMinutes: 60 + + pool: + name: $(DncEngPublicBuildPool) + demands: ImageOverride -equals build.ubuntu.2004.amd64.open + + variables: + - name: _buildScript + value: $(Build.SourcesDirectory)/build.sh --ci + + preSteps: + - checkout: self + fetchDepth: 1 + clean: true + + steps: + - template: /eng/pipelines/templates/BuildAndTest.yml + parameters: + runAsPublic: true + dotnetScript: $(Build.SourcesDirectory)/dotnet.sh + buildScript: $(_buildScript) + buildConfig: $(_BuildConfig) + repoLogPath: $(Build.Arcade.LogsPath) + repoTestResultsPath: $(Build.Arcade.TestResultsPath) + isWindows: false + + +# ---------------------------------------------------------------- +# This stage performs quality gates checks +# ---------------------------------------------------------------- +- stage: codecoverage + displayName: CodeCoverage + dependsOn: + - build + condition: succeeded('build') + variables: + - template: /eng/common/templates/variables/pool-providers.yml + jobs: + - template: /eng/common/templates/jobs/jobs.yml + parameters: + enableMicrobuild: true + enableTelemetry: true + runAsPublic: false + workspace: + clean: all + + # ---------------------------------------------------------------- + # This stage downloads the code coverage reports from the build jobs, + # merges those and validates the combined test coverage. + # ---------------------------------------------------------------- + jobs: + - job: CodeCoverageReport + timeoutInMinutes: 10 + + pool: + name: $(DncEngPublicBuildPool) + demands: ImageOverride -equals build.ubuntu.2004.amd64.open + + preSteps: + - checkout: self + fetchDepth: 1 + clean: true + + steps: + - script: $(Build.SourcesDirectory)/build.sh --ci --restore + displayName: Init toolset + + - template: /eng/pipelines/templates/VerifyCoverageReport.yml From c236e90636e6b0b4bba7a28e7dc60ab5a50f45c0 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Tue, 12 Mar 2024 00:52:35 +1100 Subject: [PATCH 36/50] Add tags to CDK-based resources (#2732) * Add tags to top level resources. * Use construct instead of builder. --- playground/cdk/CdkSample.AppHost/cache.module.bicep | 3 +++ playground/cdk/CdkSample.AppHost/cosmos.module.bicep | 3 +++ playground/cdk/CdkSample.AppHost/mykv.module.bicep | 3 +++ playground/cdk/CdkSample.AppHost/pgsql.module.bicep | 3 +++ playground/cdk/CdkSample.AppHost/pgsql2.module.bicep | 3 +++ playground/cdk/CdkSample.AppHost/sql.module.bicep | 3 +++ playground/cdk/CdkSample.AppHost/storage.module.bicep | 3 +++ src/Aspire.Hosting.Azure/AzureCosmosDBResource.cs | 2 ++ .../Extensions/AzureKeyVaultResourceExtensions.cs | 2 ++ src/Aspire.Hosting.Azure/Extensions/AzurePostgresExtensions.cs | 2 ++ src/Aspire.Hosting.Azure/Extensions/AzureRedisExtensions.cs | 2 ++ src/Aspire.Hosting.Azure/Extensions/AzureSqlExtensions.cs | 2 ++ src/Aspire.Hosting.Azure/Extensions/AzureStorageExtensions.cs | 2 ++ 13 files changed, 33 insertions(+) diff --git a/playground/cdk/CdkSample.AppHost/cache.module.bicep b/playground/cdk/CdkSample.AppHost/cache.module.bicep index bbf78fdd251..2102d0f5e5b 100644 --- a/playground/cdk/CdkSample.AppHost/cache.module.bicep +++ b/playground/cdk/CdkSample.AppHost/cache.module.bicep @@ -17,6 +17,9 @@ resource keyVault_IeF8jZvXV 'Microsoft.KeyVault/vaults@2023-02-01' existing = { resource redisCache_p9fE6TK3F 'Microsoft.Cache/Redis@2020-06-01' = { name: toLower(take(concat('cache', uniqueString(resourceGroup().id)), 24)) location: location + tags: { + 'aspire-resource-name': 'cache' + } properties: { enableNonSslPort: false minimumTlsVersion: '1.2' diff --git a/playground/cdk/CdkSample.AppHost/cosmos.module.bicep b/playground/cdk/CdkSample.AppHost/cosmos.module.bicep index c39e442e705..100fdf8d9eb 100644 --- a/playground/cdk/CdkSample.AppHost/cosmos.module.bicep +++ b/playground/cdk/CdkSample.AppHost/cosmos.module.bicep @@ -14,6 +14,9 @@ resource keyVault_IeF8jZvXV 'Microsoft.KeyVault/vaults@2023-02-01' existing = { resource cosmosDBAccount_5pKmb8KAZ 'Microsoft.DocumentDB/databaseAccounts@2023-04-15' = { name: toLower(take(concat('cosmos', uniqueString(resourceGroup().id)), 24)) location: location + tags: { + 'aspire-resource-name': 'cosmos' + } kind: 'GlobalDocumentDB' properties: { databaseAccountOfferType: 'Standard' diff --git a/playground/cdk/CdkSample.AppHost/mykv.module.bicep b/playground/cdk/CdkSample.AppHost/mykv.module.bicep index ae9a8310bfd..59c9b5f65d9 100644 --- a/playground/cdk/CdkSample.AppHost/mykv.module.bicep +++ b/playground/cdk/CdkSample.AppHost/mykv.module.bicep @@ -16,6 +16,9 @@ param signaturesecret string resource keyVault_IKWI2x0B5 'Microsoft.KeyVault/vaults@2023-02-01' = { name: toLower(take(concat('mykv', uniqueString(resourceGroup().id)), 24)) location: location + tags: { + 'aspire-resource-name': 'mykv' + } properties: { tenantId: tenant().tenantId sku: { diff --git a/playground/cdk/CdkSample.AppHost/pgsql.module.bicep b/playground/cdk/CdkSample.AppHost/pgsql.module.bicep index 12a85ae3a74..9822f8a00cc 100644 --- a/playground/cdk/CdkSample.AppHost/pgsql.module.bicep +++ b/playground/cdk/CdkSample.AppHost/pgsql.module.bicep @@ -24,6 +24,9 @@ resource keyVault_IeF8jZvXV 'Microsoft.KeyVault/vaults@2023-02-01' existing = { resource postgreSqlFlexibleServer_UTKFzAL0U 'Microsoft.DBforPostgreSQL/flexibleServers@2022-12-01' = { name: toLower(take(concat('pgsql', uniqueString(resourceGroup().id)), 24)) location: location + tags: { + 'aspire-resource-name': 'pgsql' + } sku: { name: 'Standard_B1ms' tier: 'Burstable' diff --git a/playground/cdk/CdkSample.AppHost/pgsql2.module.bicep b/playground/cdk/CdkSample.AppHost/pgsql2.module.bicep index d1cc554dffa..609b6a715c4 100644 --- a/playground/cdk/CdkSample.AppHost/pgsql2.module.bicep +++ b/playground/cdk/CdkSample.AppHost/pgsql2.module.bicep @@ -24,6 +24,9 @@ resource keyVault_IeF8jZvXV 'Microsoft.KeyVault/vaults@2023-02-01' existing = { resource postgreSqlFlexibleServer_L4yCjMLWz 'Microsoft.DBforPostgreSQL/flexibleServers@2022-12-01' = { name: toLower(take(concat('pgsql2', uniqueString(resourceGroup().id)), 24)) location: location + tags: { + 'aspire-resource-name': 'pgsql2' + } sku: { name: 'Standard_B1ms' tier: 'Burstable' diff --git a/playground/cdk/CdkSample.AppHost/sql.module.bicep b/playground/cdk/CdkSample.AppHost/sql.module.bicep index 401a32590e0..314f2ab39b5 100644 --- a/playground/cdk/CdkSample.AppHost/sql.module.bicep +++ b/playground/cdk/CdkSample.AppHost/sql.module.bicep @@ -13,6 +13,9 @@ param principalName string resource sqlServer_l5O9GRsSn 'Microsoft.Sql/servers@2022-08-01-preview' = { name: toLower(take(concat('sql', uniqueString(resourceGroup().id)), 24)) location: location + tags: { + 'aspire-resource-name': 'sql' + } properties: { version: '12.0' minimalTlsVersion: '1.2' diff --git a/playground/cdk/CdkSample.AppHost/storage.module.bicep b/playground/cdk/CdkSample.AppHost/storage.module.bicep index 44f00a68cdb..797b7062fec 100644 --- a/playground/cdk/CdkSample.AppHost/storage.module.bicep +++ b/playground/cdk/CdkSample.AppHost/storage.module.bicep @@ -19,6 +19,9 @@ param locationOverride string resource storageAccount_65zdmu5tK 'Microsoft.Storage/storageAccounts@2022-09-01' = { name: toLower(take(concat('storage', uniqueString(resourceGroup().id)), 24)) location: locationOverride + tags: { + 'aspire-resource-name': 'storage' + } sku: { name: storagesku } diff --git a/src/Aspire.Hosting.Azure/AzureCosmosDBResource.cs b/src/Aspire.Hosting.Azure/AzureCosmosDBResource.cs index 653812ec306..1291ee61609 100644 --- a/src/Aspire.Hosting.Azure/AzureCosmosDBResource.cs +++ b/src/Aspire.Hosting.Azure/AzureCosmosDBResource.cs @@ -135,6 +135,8 @@ public static IResourceBuilder AddAzureCosmosDBC cosmosAccount.AssignProperty(x => x.Locations[0].LocationName, "location"); cosmosAccount.AssignProperty(x => x.Locations[0].FailoverPriority, "0"); + cosmosAccount.Properties.Tags["aspire-resource-name"] = construct.Resource.Name; + var keyVaultNameParameter = new Parameter("keyVaultName"); construct.AddParameter(keyVaultNameParameter); diff --git a/src/Aspire.Hosting.Azure/Extensions/AzureKeyVaultResourceExtensions.cs b/src/Aspire.Hosting.Azure/Extensions/AzureKeyVaultResourceExtensions.cs index 6073cd693e6..e755bdc7a6d 100644 --- a/src/Aspire.Hosting.Azure/Extensions/AzureKeyVaultResourceExtensions.cs +++ b/src/Aspire.Hosting.Azure/Extensions/AzureKeyVaultResourceExtensions.cs @@ -43,6 +43,8 @@ public static IResourceBuilder AddAzureKeyVaultC var keyVault = construct.AddKeyVault(name: construct.Resource.Name); keyVault.AddOutput(x => x.Properties.VaultUri, "vaultUri"); + keyVault.Properties.Tags["aspire-resource-name"] = construct.Resource.Name; + var keyVaultAdministratorRoleAssignment = keyVault.AssignRole(RoleDefinition.KeyVaultAdministrator); keyVaultAdministratorRoleAssignment.AssignProperty(x => x.PrincipalId, construct.PrincipalIdParameter); keyVaultAdministratorRoleAssignment.AssignProperty(x => x.PrincipalType, construct.PrincipalTypeParameter); diff --git a/src/Aspire.Hosting.Azure/Extensions/AzurePostgresExtensions.cs b/src/Aspire.Hosting.Azure/Extensions/AzurePostgresExtensions.cs index 0026f58aaf9..6fbad40b407 100644 --- a/src/Aspire.Hosting.Azure/Extensions/AzurePostgresExtensions.cs +++ b/src/Aspire.Hosting.Azure/Extensions/AzurePostgresExtensions.cs @@ -148,6 +148,8 @@ internal static IResourceBuilder PublishAsAzurePostgresF postgres.AssignProperty(x => x.Backup.GeoRedundantBackup, "'Disabled'"); postgres.AssignProperty(x => x.AvailabilityZone, "'1'"); + postgres.Properties.Tags["aspire-resource-name"] = construct.Resource.Name; + // Opens access to all Azure services. var azureServicesFirewallRule = new PostgreSqlFirewallRule(construct, "0.0.0.0", "0.0.0.0", postgres, "AllowAllAzureIps"); diff --git a/src/Aspire.Hosting.Azure/Extensions/AzureRedisExtensions.cs b/src/Aspire.Hosting.Azure/Extensions/AzureRedisExtensions.cs index 4322dabf6c7..780288ace85 100644 --- a/src/Aspire.Hosting.Azure/Extensions/AzureRedisExtensions.cs +++ b/src/Aspire.Hosting.Azure/Extensions/AzureRedisExtensions.cs @@ -94,6 +94,8 @@ internal static IResourceBuilder PublishAsAzureRedisConstruct(thi { var redisCache = new RedisCache(construct, name: builder.Resource.Name); + redisCache.Properties.Tags["aspire-resource-name"] = construct.Resource.Name; + var vaultNameParameter = new Parameter("keyVaultName"); construct.AddParameter(vaultNameParameter); diff --git a/src/Aspire.Hosting.Azure/Extensions/AzureSqlExtensions.cs b/src/Aspire.Hosting.Azure/Extensions/AzureSqlExtensions.cs index 09e3055e9b5..d65d573f18d 100644 --- a/src/Aspire.Hosting.Azure/Extensions/AzureSqlExtensions.cs +++ b/src/Aspire.Hosting.Azure/Extensions/AzureSqlExtensions.cs @@ -85,6 +85,8 @@ internal static IResourceBuilder PublishAsAzureSqlDatab sqlServer.AssignProperty(x => x.Administrators.Login, construct.PrincipalNameParameter); sqlServer.AssignProperty(x => x.Administrators.TenantId, "subscription().tenantId"); + sqlServer.Properties.Tags["aspire-resource-name"] = construct.Resource.Name; + var azureServicesFirewallRule = new SqlFirewallRule(construct, sqlServer, "AllowAllAzureIps"); azureServicesFirewallRule.AssignProperty(x => x.StartIPAddress, "'0.0.0.0'"); azureServicesFirewallRule.AssignProperty(x => x.EndIPAddress, "'0.0.0.0'"); diff --git a/src/Aspire.Hosting.Azure/Extensions/AzureStorageExtensions.cs b/src/Aspire.Hosting.Azure/Extensions/AzureStorageExtensions.cs index 704997d4e45..e32800d76fc 100644 --- a/src/Aspire.Hosting.Azure/Extensions/AzureStorageExtensions.cs +++ b/src/Aspire.Hosting.Azure/Extensions/AzureStorageExtensions.cs @@ -49,6 +49,8 @@ public static IResourceBuilder AddAzureConstructS sku: StorageSkuName.StandardGrs ); + storageAccount.Properties.Tags["aspire-resource-name"] = construct.Resource.Name; + var blobService = new BlobService(construct); var blobRole = storageAccount.AssignRole(RoleDefinition.StorageBlobDataContributor); From 98176786b3737940860e8269a36b405b01eeb6e9 Mon Sep 17 00:00:00 2001 From: Stephan Bauer <67107950+stbau04@users.noreply.github.com> Date: Mon, 11 Mar 2024 16:09:56 +0100 Subject: [PATCH 37/50] Add WithVolumeMountForData (#1317) * Make it easier to use external volumes for database containers * Remove optional volume name * Update existing code * Add methods directly to types * Fix naming of volumes * Adapt docs * Add oracle database resource extensions * Add kafka extensions --- .../Kafka/KafkaBuilderExtensions.cs | 20 ++++++++++ .../MongoDB/MongoDBBuilderExtensions.cs | 30 ++++++++++++++ .../MySql/MySqlBuilderExtensions.cs | 30 ++++++++++++++ .../Oracle/OracleDatabaseBuilderExtensions.cs | 38 ++++++++++++++++++ .../Postgres/PostgresBuilderExtensions.cs | 30 ++++++++++++++ .../RabbitMQ/RabbitMQBuilderExtensions.cs | 20 ++++++++++ .../Redis/RedisBuilderExtensions.cs | 20 ++++++++++ .../SqlServer/SqlServerBuilderExtensions.cs | 40 +++++++++++++++++++ 8 files changed, 228 insertions(+) diff --git a/src/Aspire.Hosting/Kafka/KafkaBuilderExtensions.cs b/src/Aspire.Hosting/Kafka/KafkaBuilderExtensions.cs index 38642f1025b..071cd4f763a 100644 --- a/src/Aspire.Hosting/Kafka/KafkaBuilderExtensions.cs +++ b/src/Aspire.Hosting/Kafka/KafkaBuilderExtensions.cs @@ -29,6 +29,26 @@ public static IResourceBuilder AddKafka(this IDistributedAp .PublishAsContainer(); } + /// + /// Adds a named volume for the data folder to a KafkaServer container resource. + /// + /// The resource builder. + /// The name of the volume. Defaults to an auto-generated name based on the resource name. + /// 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 ?? $"{builder.Resource.Name}-data", "/var/lib/kafka/data", isReadOnly); + + /// + /// Adds a bind mount for the data folder to a KafkaServer container resource. + /// + /// The resource builder. + /// The source directory on the host to mount into the container. + /// A flag that indicates if this is a read-only mount. + /// The . + public static IResourceBuilder WithDataBindMount(this IResourceBuilder builder, string source, bool isReadOnly = false) + => builder.WithBindMount(source, "/var/lib/kafka/data", isReadOnly); + private static void ConfigureKafkaContainer(EnvironmentCallbackContext context, KafkaServerResource resource) { // confluentinc/confluent-local is a docker image that contains a Kafka broker started with KRaft to avoid pulling a separate image for ZooKeeper. diff --git a/src/Aspire.Hosting/MongoDB/MongoDBBuilderExtensions.cs b/src/Aspire.Hosting/MongoDB/MongoDBBuilderExtensions.cs index 3fc2c0c2950..4f57a236e7f 100644 --- a/src/Aspire.Hosting/MongoDB/MongoDBBuilderExtensions.cs +++ b/src/Aspire.Hosting/MongoDB/MongoDBBuilderExtensions.cs @@ -73,6 +73,36 @@ public static IResourceBuilder WithMongoExpress(this IResourceBuilder b return builder; } + /// + /// Adds a named volume for the data folder to a MongoDb container resource. + /// + /// The resource builder. + /// The name of the volume. Defaults to an auto-generated name based on the resource name. + /// 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 ?? $"{builder.Resource.Name}-data", "/data/db", isReadOnly); + + /// + /// Adds a bind mount for the data folder to a MongoDb container resource. + /// + /// The resource builder. + /// The source directory on the host to mount into the container. + /// A flag that indicates if this is a read-only mount. + /// The . + public static IResourceBuilder WithDataBindMount(this IResourceBuilder builder, string source, bool isReadOnly = false) + => builder.WithBindMount(source, "/data/db", isReadOnly); + + /// + /// Adds a bind mount for the init folder to a MongoDb container resource. + /// + /// The resource builder. + /// The source directory on the host to mount into the container. + /// A flag that indicates if this is a read-only mount. + /// The . + public static IResourceBuilder WithInitBindMount(this IResourceBuilder builder, string source, bool isReadOnly = true) + => builder.WithBindMount(source, "/docker-entrypoint-initdb.d", isReadOnly); + private static void ConfigureMongoExpressContainer(EnvironmentCallbackContext context, MongoDBServerResource resource) { context.EnvironmentVariables.Add("ME_CONFIG_MONGODB_URL", $"mongodb://{resource.PrimaryEndpoint.ContainerHost}:{resource.PrimaryEndpoint.Port}/?directConnection=true"); diff --git a/src/Aspire.Hosting/MySql/MySqlBuilderExtensions.cs b/src/Aspire.Hosting/MySql/MySqlBuilderExtensions.cs index 48b3d7c5dfb..7968fb2895e 100644 --- a/src/Aspire.Hosting/MySql/MySqlBuilderExtensions.cs +++ b/src/Aspire.Hosting/MySql/MySqlBuilderExtensions.cs @@ -90,4 +90,34 @@ public static IResourceBuilder PublishAsContainer(this IRes { return builder.WithManifestPublishingCallback(context => context.WriteContainerAsync(builder.Resource)); } + + /// + /// Adds a named volume for the data folder to a MySql container resource. + /// + /// The resource builder. + /// The name of the volume. Defaults to an auto-generated name based on the resource name. + /// 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 ?? $"{builder.Resource.Name}-data", "/var/lib/mysql", isReadOnly); + + /// + /// Adds a bind mount for the data folder to a MySql container resource. + /// + /// The resource builder. + /// The source directory on the host to mount into the container. + /// A flag that indicates if this is a read-only mount. + /// The . + public static IResourceBuilder WithDataBindMount(this IResourceBuilder builder, string source, bool isReadOnly = false) + => builder.WithBindMount(source, "/var/lib/mysql", isReadOnly); + + /// + /// Adds a bind mount for the init folder to a MySql container resource. + /// + /// The resource builder. + /// The source directory on the host to mount into the container. + /// A flag that indicates if this is a read-only mount. + /// The . + public static IResourceBuilder WithInitBindMount(this IResourceBuilder builder, string source, bool isReadOnly = true) + => builder.WithBindMount(source, "/docker-entrypoint-initdb.d", isReadOnly); } diff --git a/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs b/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs index 9d0a1b05492..aed646e0e1e 100644 --- a/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs +++ b/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs @@ -74,4 +74,42 @@ public static IResourceBuilder PublishAsContainer( { return builder.WithManifestPublishingCallback(context => context.WriteContainerAsync(builder.Resource)); } + + /// + /// Adds a named volume for the data folder to a OracleDatabaseServer container resource. + /// + /// The resource builder. + /// The name of the volume. Defaults to an auto-generated name based on the resource name. + /// The . + public static IResourceBuilder WithDataVolume(this IResourceBuilder builder, string? name = null) + => builder.WithVolume(name ?? $"{builder.Resource.Name}-data", "/opt/oracle/oradata", true); + + /// + /// Adds a bind mount for the data folder to a OracleDatabaseServer 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) + => builder.WithBindMount(source, "/opt/oracle/oradata", false); + + /// + /// Adds a bind mount for the init folder to a OracleDatabaseServer container resource. + /// + /// The resource builder. + /// The source directory on the host to mount into the container. + /// A flag that indicates if this is a read-only mount. + /// The . + public static IResourceBuilder WithInitBindMount(this IResourceBuilder builder, string source, bool isReadOnly = true) + => builder.WithBindMount(source, "/opt/oracle/scripts/startup", isReadOnly); + + /// + /// Adds a bind mount for the database setup folder to a OracleDatabaseServer container resource. + /// + /// The resource builder. + /// The source directory on the host to mount into the container. + /// A flag that indicates if this is a read-only mount. + /// The . + public static IResourceBuilder WithDbSetupBindMount(this IResourceBuilder builder, string source, bool isReadOnly = true) + => builder.WithBindMount(source, "/opt/oracle/scripts/setup", isReadOnly); } diff --git a/src/Aspire.Hosting/Postgres/PostgresBuilderExtensions.cs b/src/Aspire.Hosting/Postgres/PostgresBuilderExtensions.cs index f9e3c49de47..3a40f29d964 100644 --- a/src/Aspire.Hosting/Postgres/PostgresBuilderExtensions.cs +++ b/src/Aspire.Hosting/Postgres/PostgresBuilderExtensions.cs @@ -104,4 +104,34 @@ public static IResourceBuilder PublishAsContainer(this I { return builder.WithManifestPublishingCallback(context => context.WriteContainerAsync(builder.Resource)); } + + /// + /// Adds a named volume for the data folder to a Postgres container resource. + /// + /// The resource builder. + /// The name of the volume. Defaults to an auto-generated name based on the resource name. + /// 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 ?? $"{builder.Resource.Name}-data", "/var/lib/postgresql/data", isReadOnly); + + /// + /// Adds a bind mount for the data folder to a Postgres container resource. + /// + /// The resource builder. + /// The source directory on the host to mount into the container. + /// A flag that indicates if this is a read-only mount. + /// The . + public static IResourceBuilder WithDataBindMount(this IResourceBuilder builder, string source, bool isReadOnly = false) + => builder.WithBindMount(source, "/var/lib/postgresql/data", isReadOnly); + + /// + /// Adds a bind mount for the init folder to a Postgres container resource. + /// + /// The resource builder. + /// The source directory on the host to mount into the container. + /// A flag that indicates if this is a read-only mount. + /// The . + public static IResourceBuilder WithInitBindMount(this IResourceBuilder builder, string source, bool isReadOnly = true) + => builder.WithBindMount(source, "/docker-entrypoint-initdb.d", isReadOnly); } diff --git a/src/Aspire.Hosting/RabbitMQ/RabbitMQBuilderExtensions.cs b/src/Aspire.Hosting/RabbitMQ/RabbitMQBuilderExtensions.cs index ab63d33f280..d23c8401ed0 100644 --- a/src/Aspire.Hosting/RabbitMQ/RabbitMQBuilderExtensions.cs +++ b/src/Aspire.Hosting/RabbitMQ/RabbitMQBuilderExtensions.cs @@ -40,4 +40,24 @@ public static IResourceBuilder PublishAsContainer(this I { return builder.WithManifestPublishingCallback(context => context.WriteContainerAsync(builder.Resource)); } + + /// + /// Adds a named volume for the data folder to a RabbitMQ container resource. + /// + /// The resource builder. + /// The name of the volume. Defaults to an auto-generated name based on the resource name. + /// 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 ?? $"{builder.Resource.Name}-data", "/var/lib/rabbitmq", isReadOnly); + + /// + /// Adds a bind mount for the data folder to a RabbitMQ container resource. + /// + /// The resource builder. + /// The source directory on the host to mount into the container. + /// A flag that indicates if this is a read-only mount. + /// The . + public static IResourceBuilder WithDataBindMount(this IResourceBuilder builder, string source, bool isReadOnly = false) + => builder.WithBindMount(source, "/var/lib/rabbitmq", isReadOnly); } diff --git a/src/Aspire.Hosting/Redis/RedisBuilderExtensions.cs b/src/Aspire.Hosting/Redis/RedisBuilderExtensions.cs index 9b0dc413692..ec7cd3cbdbc 100644 --- a/src/Aspire.Hosting/Redis/RedisBuilderExtensions.cs +++ b/src/Aspire.Hosting/Redis/RedisBuilderExtensions.cs @@ -54,4 +54,24 @@ public static IResourceBuilder WithRedisCommander(this IResourceB return builder; } + + /// + /// Adds a named volume for the data folder to a Redis container resource. + /// + /// The resource builder. + /// The name of the volume. Defaults to an auto-generated name based on the resource name. + /// 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 ?? $"{builder.Resource.Name}-data", "/data", isReadOnly); + + /// + /// Adds a bind mount for the data folder to a Redis container resource. + /// + /// The resource builder. + /// The source directory on the host to mount into the container. + /// A flag that indicates if this is a read-only mount. + /// The . + public static IResourceBuilder WithDataBindMount(this IResourceBuilder builder, string source, bool isReadOnly = false) + => builder.WithBindMount(source, "/data", isReadOnly); } diff --git a/src/Aspire.Hosting/SqlServer/SqlServerBuilderExtensions.cs b/src/Aspire.Hosting/SqlServer/SqlServerBuilderExtensions.cs index f4c3effddf5..18c4191dede 100644 --- a/src/Aspire.Hosting/SqlServer/SqlServerBuilderExtensions.cs +++ b/src/Aspire.Hosting/SqlServer/SqlServerBuilderExtensions.cs @@ -60,4 +60,44 @@ public static IResourceBuilder AddDatabase(this IReso return builder.ApplicationBuilder.AddResource(sqlServerDatabase) .WithManifestPublishingCallback(sqlServerDatabase.WriteToManifest); } + + /// + /// Adds a named volume for the log folder to a SqlServer resource. + /// + /// The resource builder. + /// The name of the volume. Defaults to an auto-generated name based on the resource name. + /// 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 ?? $"{builder.Resource.Name}-data", "/var/opt/mssql/data", isReadOnly); + + /// + /// Adds a bind mount for the log folder to a SqlServer resource. + /// + /// The resource builder. + /// The source directory on the host to mount into the container. + /// A flag that indicates if this is a read-only mount. + /// The . + public static IResourceBuilder WithDataBindMount(this IResourceBuilder builder, string source, bool isReadOnly = false) + => builder.WithBindMount(source, "/var/opt/mssql/data", isReadOnly); + + /// + /// Adds a named volume for the log folder to a SqlServer container resource. + /// + /// The resource builder. + /// The name of the volume. Defaults to an auto-generated name based on the resource name. + /// A flag that indicates if this is a read-only volume. + /// The . + public static IResourceBuilder WithLogsVolume(this IResourceBuilder builder, string? name = null, bool isReadOnly = false) + => builder.WithVolume(name ?? $"{builder.Resource.Name}-logs", "/var/opt/mssql/log", isReadOnly); + + /// + /// Adds a bind mount for the log folder to a SqlServer container resource. + /// + /// The resource builder. + /// The source directory on the host to mount into the container. + /// A flag that indicates if this is a read-only mount. + /// The . + public static IResourceBuilder WithLogsBindMount(this IResourceBuilder builder, string source, bool isReadOnly = false) + => builder.WithBindMount(source, "/var/opt/mssql/log", isReadOnly); } From 01dcb7072d5e43e6ed21aef03d6d9fd02bf6d78f Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Mon, 11 Mar 2024 13:49:19 -0500 Subject: [PATCH 38/50] Follow up from Seq PR (#2778) * Follow up from Seq PR Address PR feedback * Fix up schema --- src/Components/Aspire.Seq/Aspire.Seq.csproj | 18 ++++--- .../Aspire.Seq/AspireSeqExtensions.cs | 47 +++++++++---------- .../Aspire.Seq/ConfigurationSchema.json | 6 +-- src/Components/Aspire.Seq/README.md | 18 +++---- src/Components/Aspire.Seq/SeqHealthCheck.cs | 2 +- src/Components/Aspire.Seq/SeqSettings.cs | 12 ++--- src/Components/Aspire_Components_Progress.md | 2 + src/Components/Telemetry.md | 8 ++++ 8 files changed, 55 insertions(+), 58 deletions(-) diff --git a/src/Components/Aspire.Seq/Aspire.Seq.csproj b/src/Components/Aspire.Seq/Aspire.Seq.csproj index f6b7a44e7a8..3e3ff77c2d0 100644 --- a/src/Components/Aspire.Seq/Aspire.Seq.csproj +++ b/src/Components/Aspire.Seq/Aspire.Seq.csproj @@ -1,14 +1,12 @@ - + - - $(NetCurrent) - true - $(ComponentDatabasePackageTags) Seq - Connects Aspire projects' telemetry to Seq - enable - $(NoWarn);CS8002 - $(SharedDir)Seq_logo.275x147.png - + + $(NetCurrent) + true + $(ComponentCommonPackageTags) Seq + A Seq client that connects an Aspire project's telemetry to Seq. + $(SharedDir)Seq_logo.275x147.png + diff --git a/src/Components/Aspire.Seq/AspireSeqExtensions.cs b/src/Components/Aspire.Seq/AspireSeqExtensions.cs index ebefff95865..ae3cff714ad 100644 --- a/src/Components/Aspire.Seq/AspireSeqExtensions.cs +++ b/src/Components/Aspire.Seq/AspireSeqExtensions.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Aspire; @@ -17,28 +17,34 @@ namespace Microsoft.Extensions.Hosting; /// public static class AspireSeqExtensions { - const string ConnectionStringConfigurationKeyPrefix = "ConnectionStrings:"; - const string DefaultConnectionStringConfigurationKey = $"{ConnectionStringConfigurationKeyPrefix}seq"; - /// /// Registers OTLP log and trace exporters to send to Seq. /// /// The to read config from and add services to. - /// A name used to retrieve the connection string from the ConnectionStrings configuration section. + /// A name used to retrieve the connection string from the ConnectionStrings configuration section. /// An optional delegate that can be used for customizing options. It's invoked after the settings are read from the configuration. - public static void AddSeqEndpoint(this IHostApplicationBuilder builder, string name, Action? configureSettings = null) + public static void AddSeqEndpoint(this IHostApplicationBuilder builder, string connectionName, Action? configureSettings = null) { - var settings = GetSettings(builder, configureSettings); + ArgumentNullException.ThrowIfNull(builder); - var seqUri = !string.IsNullOrEmpty(settings.ServerUrl) - ? settings.ServerUrl - : (builder.Configuration[string.IsNullOrEmpty(name) - ? DefaultConnectionStringConfigurationKey - : $"{ConnectionStringConfigurationKeyPrefix}{name}"]) ?? "http://localhost:5341"; + var settings = new SeqSettings(); + builder.Configuration.GetSection("Aspire:Seq").Bind(settings); + + if (builder.Configuration.GetConnectionString(connectionName) is string connectionString) + { + settings.ServerUrl = connectionString; + } + + configureSettings?.Invoke(settings); + + if (string.IsNullOrEmpty(settings.ServerUrl)) + { + settings.ServerUrl = "http://localhost:5341"; + } builder.Services.Configure(logging => logging.AddOtlpExporter(opt => { - opt.Endpoint = new Uri($"{seqUri}/ingest/otlp/v1/logs"); + opt.Endpoint = new Uri($"{settings.ServerUrl}/ingest/otlp/v1/logs"); opt.Protocol = OtlpExportProtocol.HttpProtobuf; if (!string.IsNullOrEmpty(settings.ApiKey)) { @@ -48,7 +54,7 @@ public static void AddSeqEndpoint(this IHostApplicationBuilder builder, string n builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing .AddOtlpExporter(opt => { - opt.Endpoint = new Uri($"{seqUri}/ingest/otlp/v1/traces"); + opt.Endpoint = new Uri($"{settings.ServerUrl}/ingest/otlp/v1/traces"); opt.Protocol = OtlpExportProtocol.HttpProtobuf; if (!string.IsNullOrEmpty(settings.ApiKey)) { @@ -61,20 +67,9 @@ public static void AddSeqEndpoint(this IHostApplicationBuilder builder, string n { builder.TryAddHealthCheck(new HealthCheckRegistration( "Seq", - _ => new SeqHealthCheck(seqUri), + _ => new SeqHealthCheck(settings.ServerUrl), failureStatus: default, tags: default)); } } - - static SeqSettings GetSettings(this IHostApplicationBuilder builder, Action? configureSettings = null) - { - ArgumentNullException.ThrowIfNull(builder); - - var settings = new SeqSettings(); - builder.Configuration.GetSection("Aspire:Seq").Bind(settings); - - configureSettings?.Invoke(settings); - return settings; - } } diff --git a/src/Components/Aspire.Seq/ConfigurationSchema.json b/src/Components/Aspire.Seq/ConfigurationSchema.json index 5aefcaef300..53615d779fd 100644 --- a/src/Components/Aspire.Seq/ConfigurationSchema.json +++ b/src/Components/Aspire.Seq/ConfigurationSchema.json @@ -17,15 +17,15 @@ "properties": { "ApiKey": { "type": "string", - "description": "A Seq API key that authenticates the client to the Seq server." + "description": "Gets or sets a Seq API key that authenticates the client to the Seq server." }, "HealthChecks": { "type": "boolean", - "description": "Is the Seq server health check enabled." + "description": "Gets or sets a boolean value that indicates whetherthe Seq server health check is enabled or not." }, "ServerUrl": { "type": "string", - "description": "The base URL of the Seq server (including protocol and port). E.g. \"https://example.seq.com:6789\"" + "description": "Gets or sets the base URL of the Seq server (including protocol and port). E.g. \"https://example.seq.com:6789\"" } }, "description": "Provides the client configuration settings for connecting telemetry to a Seq server." diff --git a/src/Components/Aspire.Seq/README.md b/src/Components/Aspire.Seq/README.md index 507ce541c27..715352f7f77 100644 --- a/src/Components/Aspire.Seq/README.md +++ b/src/Components/Aspire.Seq/README.md @@ -20,7 +20,7 @@ dotnet add package Aspire.Seq ## Usage example -In the _Program.cs_ file of your projects, call the `AddSeqEndpoint` extension method to register OpenTelemetry Protocol exporters to send logs and traces to Seq. The method takes an optional name parameter. +In the _Program.cs_ file of your projects, call the `AddSeqEndpoint` extension method to register OpenTelemetry Protocol exporters to send logs and traces to Seq. The method takes a connection name parameter. ```csharp builder.AddSeqEndpoint("seq"); @@ -63,9 +63,9 @@ builder.AddSeqEndpoint("seq", settings => { In your AppHost project, register a Seq server and propagate its configuration using the following methods (note that you must accept the [Seq End User Licence Agreement](https://datalust.co/doc/eula-current.pdf) for Seq to start): ```csharp -var seq = builder.AddSeq("seq); +var seq = builder.AddSeq("seq"); -var myService = builder.AddProject() +var myService = builder.AddProject() .WithReference(seq); ``` @@ -77,7 +77,7 @@ builder.AddSeqEndpoint("seq"); ### Persistent logs and traces -To retain Seq's data and configuration across application restarts register Seq with a data directory. +To retain Seq's data and configuration across application restarts register Seq with a data directory in your AppHost project. ```csharp var seq = builder.AddSeq("seq", seqDataDirectory: "./seqdata"); @@ -85,15 +85,9 @@ var seq = builder.AddSeq("seq", seqDataDirectory: "./seqdata"); Note that the directory specified must already exist. -### Including Seq in the .NET Aspire manifest +### Seq in the .NET Aspire manifest -To deploy Seq as part of .NET Aspire deployment it must be included in the manifest. - -> Note that this should not be done without having [properly secured the Seq instance](https://docs.datalust.co/docs/production-deployment). It is currently easier to set up a secure production Seq server outside of .NET Aspire. - -```csharp -var seq = builder.AddSeq("seq", seqDataDirectory: "./seqdata"); -``` +Seq is not part of the .NET Aspire deployment manifest. It is recommended to set up a secure production Seq server outside of .NET Aspire. ## Additional documentation diff --git a/src/Components/Aspire.Seq/SeqHealthCheck.cs b/src/Components/Aspire.Seq/SeqHealthCheck.cs index 129a120e6aa..f035972c610 100644 --- a/src/Components/Aspire.Seq/SeqHealthCheck.cs +++ b/src/Components/Aspire.Seq/SeqHealthCheck.cs @@ -9,7 +9,7 @@ namespace Aspire.Seq; /// A diagnostic health check implementation for Seq servers. /// /// The URI of the Seq server to check. -public class SeqHealthCheck(string seqUri) : IHealthCheck +internal sealed class SeqHealthCheck(string seqUri) : IHealthCheck { readonly HttpClient _client = new(new SocketsHttpHandler { ActivityHeadersPropagator = null }) { BaseAddress = new Uri(seqUri) }; diff --git a/src/Components/Aspire.Seq/SeqSettings.cs b/src/Components/Aspire.Seq/SeqSettings.cs index d1330914ec0..322bf2545b6 100644 --- a/src/Components/Aspire.Seq/SeqSettings.cs +++ b/src/Components/Aspire.Seq/SeqSettings.cs @@ -6,20 +6,20 @@ namespace Aspire.Seq; /// /// Provides the client configuration settings for connecting telemetry to a Seq server. /// -public class SeqSettings +public sealed class SeqSettings { /// - /// Is the Seq server health check enabled. + /// Gets or sets a boolean value that indicates whetherthe Seq server health check is enabled or not. /// public bool HealthChecks { get; set; } = true; /// - /// A Seq API key that authenticates the client to the Seq server. + /// Gets or sets a Seq API key that authenticates the client to the Seq server. /// - public string ApiKey { get; set; } = string.Empty; + public string? ApiKey { get; set; } /// - /// The base URL of the Seq server (including protocol and port). E.g. "https://example.seq.com:6789" + /// Gets or sets the base URL of the Seq server (including protocol and port). E.g. "https://example.seq.com:6789" /// - public string ServerUrl { get; set; } = string.Empty; + public string? ServerUrl { get; set; } } diff --git a/src/Components/Aspire_Components_Progress.md b/src/Components/Aspire_Components_Progress.md index 9568c4244da..e07ed0d9dd7 100644 --- a/src/Components/Aspire_Components_Progress.md +++ b/src/Components/Aspire_Components_Progress.md @@ -27,12 +27,14 @@ As part of the .NET Aspire November preview, we want to include a set of .NET As | Confluent.Kafka | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | | Pomelo.EntityFrameworkCore.MySql | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | NATS.Net | ✅ | ✅ | ✅ | ✅ | ✅ | | | ✅ | +| Seq | ✅ | ✅ | ✅ | ✅ | ✅ | N/A | N/A | ✅ | Nomenclature used in the table above: - ✅ - Requirement is met - (blank) - Requirement hasn't been met yet - ❌ - Requirement can't be met +- N/A - Requirement not applicable ## .NET Aspire Component Requirements diff --git a/src/Components/Telemetry.md b/src/Components/Telemetry.md index f3183112356..1bfe7b5a6b9 100644 --- a/src/Components/Telemetry.md +++ b/src/Components/Telemetry.md @@ -340,6 +340,14 @@ Aspire.RabbitMQ.Client: - Metric names: - none (currently not supported by RabbitMQ.Client library) +Aspire.Seq: +- Log categories: + - "Seq" +- Activity source names: + - N/A (Seq is a telemetry sink, not a telemetry source) +- Metric names: + - N/A (Seq is a telemetry sink, not a telemetry source) + Aspire.StackExchange.Redis: - Log categories: - "StackExchange.Redis" From 95a729b5f64f1c037370d8a345b0af90056f05dd Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Mon, 11 Mar 2024 14:15:46 -0500 Subject: [PATCH 39/50] Follow up from NATS PR (#2779) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Follow up from NATS PR Address PR feedback * Update README.md --------- Co-authored-by: Sébastien Ros --- src/Components/Aspire.NATS.Net/Aspire.NATS.Net.csproj | 4 ++-- .../Aspire.NATS.Net/AspireNatsClientExtensions.cs | 4 ++-- src/Components/Aspire.NATS.Net/README.md | 8 ++++---- tests/Aspire.NATS.Net.Tests/ConformanceTests.cs | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Components/Aspire.NATS.Net/Aspire.NATS.Net.csproj b/src/Components/Aspire.NATS.Net/Aspire.NATS.Net.csproj index 38ad39930a3..653a18e0939 100644 --- a/src/Components/Aspire.NATS.Net/Aspire.NATS.Net.csproj +++ b/src/Components/Aspire.NATS.Net/Aspire.NATS.Net.csproj @@ -3,8 +3,8 @@ $(NetCurrent) true - $(ComponentDatabasePackageTags) nats messaging - A NATS client that integrates with Aspire, including health checks, metrics, logging, and telemetry. + $(ComponentCommonPackageTags) nats messaging + A NATS client that integrates with Aspire, including health checks and logging. $(SharedDir)nats-icon.png diff --git a/src/Components/Aspire.NATS.Net/AspireNatsClientExtensions.cs b/src/Components/Aspire.NATS.Net/AspireNatsClientExtensions.cs index e403d1923db..3b17dcd2018 100644 --- a/src/Components/Aspire.NATS.Net/AspireNatsClientExtensions.cs +++ b/src/Components/Aspire.NATS.Net/AspireNatsClientExtensions.cs @@ -18,7 +18,7 @@ namespace Microsoft.Extensions.Hosting; /// public static class AspireNatsClientExtensions { - private const string DefaultConfigSectionName = "Aspire:Nats:Client"; + private const string DefaultConfigSectionName = "Aspire:NATS:Net"; /// /// Registers service for connecting NATS server with NATS client. @@ -122,7 +122,7 @@ public static void AddNatsJetStream(this IHostApplicationBuilder builder) builder.Services.AddSingleton(static provider => { - return new NatsJSContextFactory().CreateContext(provider.GetService()!); + return new NatsJSContextFactory().CreateContext(provider.GetRequiredService()); }); } } diff --git a/src/Components/Aspire.NATS.Net/README.md b/src/Components/Aspire.NATS.Net/README.md index c980f3cdc68..a724c519090 100644 --- a/src/Components/Aspire.NATS.Net/README.md +++ b/src/Components/Aspire.NATS.Net/README.md @@ -1,6 +1,6 @@ # Aspire.NATS.Net library -Registers [INatsConnection](https://nats-io.github.io/nats.net.v2/api/NATS.Client.Core.INatsConnection.html) in the DI container for connecting NATS server. Enables corresponding health check, metrics, logging and telemetry. +Registers [INatsConnection](https://nats-io.github.io/nats.net.v2/api/NATS.Client.Core.INatsConnection.html) in the DI container for connecting NATS server. Enables corresponding health check and logging. ## Getting started @@ -41,10 +41,10 @@ The .NET Aspire NATS component provides multiple options to configure the NATS c ### Use a connection string -When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddNats()`: +When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddNatsClient()`: ```csharp -builder.AddNats("myConnection"); +builder.AddNatsClient("myConnection"); ``` And then the connection string will be retrieved from the `ConnectionStrings` configuration section: @@ -80,7 +80,7 @@ The .NET Aspire NATS component supports [Microsoft.Extensions.Configuration](htt Also you can pass the `Action configureSettings` delegate to set up some or all the options inline, for example to disable health checks from code: ```csharp - builder.AddNats("nats", settings => settings.HealthChecks = false); +builder.AddNatsClient("nats", settings => settings.HealthChecks = false); ``` ## AppHost extensions diff --git a/tests/Aspire.NATS.Net.Tests/ConformanceTests.cs b/tests/Aspire.NATS.Net.Tests/ConformanceTests.cs index 122e359713e..53f4674ee76 100644 --- a/tests/Aspire.NATS.Net.Tests/ConformanceTests.cs +++ b/tests/Aspire.NATS.Net.Tests/ConformanceTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.ConformanceTests; @@ -31,7 +31,7 @@ protected override void RegisterComponent(HostApplicationBuilder builder, Action protected override void PopulateConfiguration(ConfigurationManager configuration, string? key = null) => configuration.AddInMemoryCollection(new KeyValuePair[1] { - new KeyValuePair(CreateConfigKey("Aspire:Nats:Client", key, "ConnectionString"), ConnectionSting) + new KeyValuePair(CreateConfigKey("Aspire:Nats:Net", key, "ConnectionString"), ConnectionSting) }); protected override bool CanCreateClientWithoutConnectingToServer => false; From efcafb5887144089765958d1b72d4c8fa0e2494d Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 19:51:30 +0000 Subject: [PATCH 40/50] Update dependencies from https://github.com/microsoft/usvc-apiserver build 0.1.56 (#2780) [main] Update dependencies from microsoft/usvc-apiserver --- 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 5b9e7c7e0c3..af745f47042 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,33 +1,33 @@ - + https://github.com/microsoft/usvc-apiserver - 690063f8bd9b0520481ff057ed8dd7287ef74129 + efac1f661ea53922dee69b991e120b0fd74929a0 - + https://github.com/microsoft/usvc-apiserver - 690063f8bd9b0520481ff057ed8dd7287ef74129 + efac1f661ea53922dee69b991e120b0fd74929a0 - + https://github.com/microsoft/usvc-apiserver - 690063f8bd9b0520481ff057ed8dd7287ef74129 + efac1f661ea53922dee69b991e120b0fd74929a0 - + https://github.com/microsoft/usvc-apiserver - 690063f8bd9b0520481ff057ed8dd7287ef74129 + efac1f661ea53922dee69b991e120b0fd74929a0 - + https://github.com/microsoft/usvc-apiserver - 690063f8bd9b0520481ff057ed8dd7287ef74129 + efac1f661ea53922dee69b991e120b0fd74929a0 - + https://github.com/microsoft/usvc-apiserver - 690063f8bd9b0520481ff057ed8dd7287ef74129 + efac1f661ea53922dee69b991e120b0fd74929a0 - + https://github.com/microsoft/usvc-apiserver - 690063f8bd9b0520481ff057ed8dd7287ef74129 + efac1f661ea53922dee69b991e120b0fd74929a0 https://dev.azure.com/dnceng/internal/_git/dotnet-extensions diff --git a/eng/Versions.props b/eng/Versions.props index 9f11eacc65c..a864587868a 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -11,13 +11,13 @@ 8.0.100-rtm.23512.16 - 0.1.55 - 0.1.55 - 0.1.55 - 0.1.55 - 0.1.55 - 0.1.55 - 0.1.55 + 0.1.56 + 0.1.56 + 0.1.56 + 0.1.56 + 0.1.56 + 0.1.56 + 0.1.56 8.0.0-rc.1.23419.3 13.3.8825-net8-rc1 8.0.0-beta.24158.4 From ded1eaef1592000b7c99136d1cc9a9085170f07b Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Mon, 11 Mar 2024 16:22:19 -0400 Subject: [PATCH 41/50] [workload-testing] Update testing nuget, and remove some workarounds (#2748) * Update Microsoft.NET.Runtime.WorkloadTesting.Internal to 9.0.0-preview.3.24158.8 * [workload-testing] Remove the hacks, and use the update task 1. remove package source mapping from `nuget.config` as it gets added at build time now 2. Manifest packages are installed with a `PackageDownload` instead of a `PackageReference`, thus it can work with `PackageType='dotnetplatform'` packages. 3. Generating a patched nuget.config for building test projects can be done using a new `PatchNuGetConfig` task, and thus we can remove the inline task. * Run _PatchNuGetConfigForBuildingTestProject target only when using workloads for testing * Update tests/Shared/Aspire.Workload.Testing.targets * fix windows build --- NuGet.config | 4 - eng/Version.Details.xml | 4 +- eng/Versions.props | 2 +- .../Microsoft.NET.Sdk.Aspire.csproj | 10 --- .../Aspire.EndToEnd.Tests.csproj | 2 +- tests/Shared/Aspire.Workload.Testing.targets | 75 ++++--------------- 6 files changed, 18 insertions(+), 79 deletions(-) diff --git a/NuGet.config b/NuGet.config index 35a7f2a6bc0..d611b31697b 100644 --- a/NuGet.config +++ b/NuGet.config @@ -22,10 +22,6 @@ - - - - diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index af745f47042..eff0cd64417 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -73,9 +73,9 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-runtime 5535e31a712343a63f5d7d796cd874e563e5ac14 - + https://github.com/dotnet/runtime - 50d6e5d5ffd05dd4034cffd222ea610baedcc326 + 54d318d6da76e409d4e865220d9923283d55a06d https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore diff --git a/eng/Versions.props b/eng/Versions.props index a864587868a..ede2f374cf0 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -45,6 +45,6 @@ 8.0.2 8.0.2 8.0.2 - 9.0.0-preview.3.24151.4 + 9.0.0-preview.3.24158.8 diff --git a/src/Microsoft.NET.Sdk.Aspire/Microsoft.NET.Sdk.Aspire.csproj b/src/Microsoft.NET.Sdk.Aspire/Microsoft.NET.Sdk.Aspire.csproj index 02522b16037..d73e9507f26 100644 --- a/src/Microsoft.NET.Sdk.Aspire/Microsoft.NET.Sdk.Aspire.csproj +++ b/src/Microsoft.NET.Sdk.Aspire/Microsoft.NET.Sdk.Aspire.csproj @@ -5,16 +5,6 @@ $(PackageId).Manifest-$(DotNetAspireManifestVersionBand) .NET Aspire workload manifest - - - diff --git a/tests/Aspire.EndToEnd.Tests/Aspire.EndToEnd.Tests.csproj b/tests/Aspire.EndToEnd.Tests/Aspire.EndToEnd.Tests.csproj index 106880d5113..dd18c53e4cb 100644 --- a/tests/Aspire.EndToEnd.Tests/Aspire.EndToEnd.Tests.csproj +++ b/tests/Aspire.EndToEnd.Tests/Aspire.EndToEnd.Tests.csproj @@ -23,7 +23,7 @@ - + diff --git a/tests/Shared/Aspire.Workload.Testing.targets b/tests/Shared/Aspire.Workload.Testing.targets index 54045ac6548..c1440126e21 100644 --- a/tests/Shared/Aspire.Workload.Testing.targets +++ b/tests/Shared/Aspire.Workload.Testing.targets @@ -3,6 +3,10 @@ _GetWorkloadsToInstall;$(GetWorkloadInputsDependsOn) _GetNuGetsToBuild;$(GetNuGetsToBuildForWorkloadTestingDependsOn) + + High + *Aspire* + <_ShippingPackagesDir>$([MSBuild]::NormalizeDirectory($(ArtifactsDir), 'packages', $(Configuration), 'Shipping')) <_GlobalJsonContent>$([System.IO.File]::ReadAllText('$(RepoRoot)global.json')) @@ -72,69 +76,18 @@ - - - - - - - - - - - - - - - - - - - + + + From b93520de6bccbdd59af5f57ed5f0d62aee921159 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Mon, 11 Mar 2024 15:16:51 -0700 Subject: [PATCH 42/50] Use helper methods to model resources (#2777) * Use helper methods to model resources - Remove the low level annotation usage of ContainerImageAnnotation and use the existing helper methods (to make sure they work as designed). - Added tests for error conditions with helper methods * Remove manifest writer check from tests * Use a single helper method to throw the same error * Fixed more tests --- src/Aspire.Hosting/Dcp/DcpDependencyCheck.cs | 2 +- .../ContainerResourceBuilderExtensions.cs | 29 ++++++--- .../Kafka/KafkaBuilderExtensions.cs | 5 +- .../MongoDB/MongoDBBuilderExtensions.cs | 5 +- .../MongoDB/MongoDBConnectionStringBuilder.cs | 62 ------------------- .../MySql/MySqlBuilderExtensions.cs | 17 +---- .../Nats/NatsBuilderExtensions.cs | 4 +- .../Oracle/OracleDatabaseBuilderExtensions.cs | 13 +--- .../Postgres/PostgresBuilderExtensions.cs | 19 ++---- .../RabbitMQ/RabbitMQBuilderExtensions.cs | 25 +++----- .../Redis/RedisBuilderExtensions.cs | 5 +- .../Seq/SeqBuilderExtensions.cs | 3 +- .../SqlServer/SqlServerBuilderExtensions.cs | 16 +---- .../ContainerResourceBuilderTests.cs | 36 ++++++++++- .../Kafka/AddKafkaTests.cs | 3 - .../MongoDB/AddMongoDBTests.cs | 6 -- .../MongoDBConnectionStringBuilderTests.cs | 36 ----------- .../MySql/AddMySqlTests.cs | 6 -- .../Aspire.Hosting.Tests/Nats/AddNatsTests.cs | 6 -- .../Oracle/AddOracleDatabaseTests.cs | 3 - .../Postgres/AddPostgresTests.cs | 9 --- .../RabbitMQ/AddRabbitMQTests.cs | 3 - .../Redis/AddRedisTests.cs | 6 -- .../SqlServer/AddSqlServerTests.cs | 3 - 24 files changed, 84 insertions(+), 238 deletions(-) delete mode 100644 src/Aspire.Hosting/MongoDB/MongoDBConnectionStringBuilder.cs delete mode 100644 tests/Aspire.Hosting.Tests/MongoDB/MongoDBConnectionStringBuilderTests.cs diff --git a/src/Aspire.Hosting/Dcp/DcpDependencyCheck.cs b/src/Aspire.Hosting/Dcp/DcpDependencyCheck.cs index e46ba3b2a93..bdfccdf04fd 100644 --- a/src/Aspire.Hosting/Dcp/DcpDependencyCheck.cs +++ b/src/Aspire.Hosting/Dcp/DcpDependencyCheck.cs @@ -147,7 +147,7 @@ private void EnsureDcpContainerRuntime(DcpInfo dcpInfo) { // If we don't have any resources that need a container then we // don't need to check for a healthy container runtime. - if (!_applicationModel.Resources.Any(c => c.Annotations.OfType().Any())) + if (!_applicationModel.Resources.Any(c => c.IsContainer())) { return; } diff --git a/src/Aspire.Hosting/Extensions/ContainerResourceBuilderExtensions.cs b/src/Aspire.Hosting/Extensions/ContainerResourceBuilderExtensions.cs index 832a0939568..df82e3bb89b 100644 --- a/src/Aspire.Hosting/Extensions/ContainerResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/Extensions/ContainerResourceBuilderExtensions.cs @@ -34,7 +34,7 @@ public static IResourceBuilder AddContainer(this IDistributed { var container = new ContainerResource(name); return builder.AddResource(container) - .WithAnnotation(new ContainerImageAnnotation { Image = image, Tag = tag }); + .WithImage(image, tag); } /// @@ -111,7 +111,7 @@ public static IResourceBuilder WithImageTag(this IResourceBuilder build return builder; } - throw new InvalidOperationException($"The resource '{builder.Resource.Name}' does not have a container image specified. Use WithImage to specify the container image and tag."); + return ThrowResourceIsNotContainer(builder); } /// @@ -123,9 +123,13 @@ public static IResourceBuilder WithImageTag(this IResourceBuilder build /// public static IResourceBuilder WithImageRegistry(this IResourceBuilder builder, string registry) where T : ContainerResource { - var containerImageAnnotation = builder.Resource.Annotations.OfType().Single(); - containerImageAnnotation.Registry = registry; - return builder; + if (builder.Resource.Annotations.OfType().LastOrDefault() is { } existingImageAnnotation) + { + existingImageAnnotation.Registry = registry; + return builder; + } + + return ThrowResourceIsNotContainer(builder); } /// @@ -160,9 +164,18 @@ public static IResourceBuilder WithImage(this IResourceBuilder builder, /// public static IResourceBuilder WithImageSHA256(this IResourceBuilder builder, string sha256) where T : ContainerResource { - var containerImageAnnotation = builder.Resource.Annotations.OfType().Single(); - containerImageAnnotation.SHA256 = sha256; - return builder; + if (builder.Resource.Annotations.OfType().LastOrDefault() is { } existingImageAnnotation) + { + existingImageAnnotation.SHA256 = sha256; + return builder; + } + + return ThrowResourceIsNotContainer(builder); + } + + private static IResourceBuilder ThrowResourceIsNotContainer(IResourceBuilder builder) where T : ContainerResource + { + throw new InvalidOperationException($"The resource '{builder.Resource.Name}' does not have a container image specified. Use WithImage to specify the container image and tag."); } /// diff --git a/src/Aspire.Hosting/Kafka/KafkaBuilderExtensions.cs b/src/Aspire.Hosting/Kafka/KafkaBuilderExtensions.cs index 071cd4f763a..e43629eb074 100644 --- a/src/Aspire.Hosting/Kafka/KafkaBuilderExtensions.cs +++ b/src/Aspire.Hosting/Kafka/KafkaBuilderExtensions.cs @@ -24,9 +24,8 @@ public static IResourceBuilder AddKafka(this IDistributedAp var kafka = new KafkaServerResource(name); return builder.AddResource(kafka) .WithEndpoint(containerPort: KafkaBrokerPort, hostPort: port, name: KafkaServerResource.PrimaryEndpointName) - .WithAnnotation(new ContainerImageAnnotation { Image = "confluentinc/confluent-local", Tag = "7.6.0" }) - .WithEnvironment(context => ConfigureKafkaContainer(context, kafka)) - .PublishAsContainer(); + .WithImage("confluentinc/confluent-local", "7.6.0") + .WithEnvironment(context => ConfigureKafkaContainer(context, kafka)); } /// diff --git a/src/Aspire.Hosting/MongoDB/MongoDBBuilderExtensions.cs b/src/Aspire.Hosting/MongoDB/MongoDBBuilderExtensions.cs index 4f57a236e7f..e34b3952856 100644 --- a/src/Aspire.Hosting/MongoDB/MongoDBBuilderExtensions.cs +++ b/src/Aspire.Hosting/MongoDB/MongoDBBuilderExtensions.cs @@ -28,8 +28,7 @@ public static IResourceBuilder AddMongoDB(this IDistribut return builder .AddResource(mongoDBContainer) .WithEndpoint(hostPort: port, containerPort: DefaultContainerPort, name: MongoDBServerResource.PrimaryEndpointName) - .WithAnnotation(new ContainerImageAnnotation { Image = "mongo", Tag = "7.0.5" }) - .PublishAsContainer(); + .WithImage("mongo", "7.0.5"); } /// @@ -65,7 +64,7 @@ public static IResourceBuilder WithMongoExpress(this IResourceBuilder b var mongoExpressContainer = new MongoExpressContainerResource(containerName); builder.ApplicationBuilder.AddResource(mongoExpressContainer) - .WithAnnotation(new ContainerImageAnnotation { Image = "mongo-express", Tag = "1.0.2-20" }) + .WithImage("mongo-express", "1.0.2-20") .WithEnvironment(context => ConfigureMongoExpressContainer(context, builder.Resource)) .WithHttpEndpoint(containerPort: 8081, hostPort: hostPort, name: containerName) .ExcludeFromManifest(); diff --git a/src/Aspire.Hosting/MongoDB/MongoDBConnectionStringBuilder.cs b/src/Aspire.Hosting/MongoDB/MongoDBConnectionStringBuilder.cs deleted file mode 100644 index 874876e1e73..00000000000 --- a/src/Aspire.Hosting/MongoDB/MongoDBConnectionStringBuilder.cs +++ /dev/null @@ -1,62 +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 Aspire.Hosting.MongoDB; - -internal class MongoDBConnectionStringBuilder -{ - private const string Scheme = "mongodb"; - - private string? _server; - private int _port; - private string? _userName; - private string? _password; - - public MongoDBConnectionStringBuilder WithServer(string server) - { - ArgumentNullException.ThrowIfNullOrWhiteSpace(server, nameof(server)); - - _server = server; - - return this; - } - - public MongoDBConnectionStringBuilder WithPort(int port) - { - _port = port; - - return this; - } - - public MongoDBConnectionStringBuilder WithUserName(string userName) - { - ArgumentNullException.ThrowIfNullOrWhiteSpace(userName, nameof(userName)); - - _userName = userName; - - return this; - } - - public MongoDBConnectionStringBuilder WithPassword(string password) - { - ArgumentNullException.ThrowIfNullOrWhiteSpace(password, nameof(password)); - - _password = password; - - return this; - } - - public string Build() - { - var builder = new UriBuilder - { - Scheme = Scheme, - Host = _server, - Port = _port, - UserName = _userName, - Password = _password - }; - - return builder.ToString(); - } -} diff --git a/src/Aspire.Hosting/MySql/MySqlBuilderExtensions.cs b/src/Aspire.Hosting/MySql/MySqlBuilderExtensions.cs index 7968fb2895e..13df5767b9f 100644 --- a/src/Aspire.Hosting/MySql/MySqlBuilderExtensions.cs +++ b/src/Aspire.Hosting/MySql/MySqlBuilderExtensions.cs @@ -27,12 +27,11 @@ public static IResourceBuilder AddMySql(this IDistributedAp var resource = new MySqlServerResource(name, password); return builder.AddResource(resource) .WithEndpoint(hostPort: port, containerPort: 3306, name: MySqlServerResource.PrimaryEndpointName) // Internal port is always 3306. - .WithAnnotation(new ContainerImageAnnotation { Image = "mysql", Tag = "8.3.0" }) + .WithImage("mysql", "8.3.0") .WithEnvironment(context => { context.EnvironmentVariables[PasswordEnvVarName] = resource.PasswordInput; - }) - .PublishAsContainer(); + }); } /// @@ -73,7 +72,7 @@ public static IResourceBuilder WithPhpMyAdmin(this IResourceBuilder bui var phpMyAdminContainer = new PhpMyAdminContainerResource(containerName); builder.ApplicationBuilder.AddResource(phpMyAdminContainer) - .WithAnnotation(new ContainerImageAnnotation { Image = "phpmyadmin", Tag = "5.2" }) + .WithImage("phpmyadmin", "5.2") .WithHttpEndpoint(containerPort: 80, hostPort: hostPort, name: containerName) .WithBindMount(Path.GetTempFileName(), "/etc/phpmyadmin/config.user.inc.php") .ExcludeFromManifest(); @@ -81,16 +80,6 @@ public static IResourceBuilder WithPhpMyAdmin(this IResourceBuilder bui return builder; } - /// - /// Changes resource to be published as a container. - /// - /// The builder. - /// A reference to the . - public static IResourceBuilder PublishAsContainer(this IResourceBuilder builder) - { - return builder.WithManifestPublishingCallback(context => context.WriteContainerAsync(builder.Resource)); - } - /// /// Adds a named volume for the data folder to a MySql container resource. /// diff --git a/src/Aspire.Hosting/Nats/NatsBuilderExtensions.cs b/src/Aspire.Hosting/Nats/NatsBuilderExtensions.cs index db2a486f350..6372d77ad76 100644 --- a/src/Aspire.Hosting/Nats/NatsBuilderExtensions.cs +++ b/src/Aspire.Hosting/Nats/NatsBuilderExtensions.cs @@ -22,9 +22,7 @@ public static IResourceBuilder AddNats(this IDistributedAppl var nats = new NatsServerResource(name); return builder.AddResource(nats) .WithEndpoint(containerPort: 4222, hostPort: port, name: NatsServerResource.PrimaryEndpointName) - .WithAnnotation(new ContainerImageAnnotation { Image = "nats" }) - .WithImageTag("2") - .PublishAsContainer(); + .WithImage("nats", "2"); } /// diff --git a/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs b/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs index aed646e0e1e..5c6309ff76b 100644 --- a/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs +++ b/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs @@ -39,7 +39,8 @@ public static IResourceBuilder AddOracle(this IDis var oracleDatabaseServer = new OracleDatabaseServerResource(name, password); return builder.AddResource(oracleDatabaseServer) .WithEndpoint(hostPort: port, containerPort: 1521, name: OracleDatabaseServerResource.PrimaryEndpointName) - .WithAnnotation(new ContainerImageAnnotation { Image = "database/free", Tag = "23.3.0.0", Registry = "container-registry.oracle.com" }) + .WithImage("database/free", "23.3.0.0") + .WithImageRegistry("container-registry.oracle.com") .WithEnvironment(context => { context.EnvironmentVariables[PasswordEnvVarName] = oracleDatabaseServer.PasswordInput; @@ -65,16 +66,6 @@ public static IResourceBuilder AddDatabase(this IResourc .WithManifestPublishingCallback(oracleDatabase.WriteToManifest); } - /// - /// Changes the Oracle Database Server resource to be published as a container. - /// - /// Builder for the underlying . - /// - public static IResourceBuilder PublishAsContainer(this IResourceBuilder builder) - { - return builder.WithManifestPublishingCallback(context => context.WriteContainerAsync(builder.Resource)); - } - /// /// Adds a named volume for the data folder to a OracleDatabaseServer container resource. /// diff --git a/src/Aspire.Hosting/Postgres/PostgresBuilderExtensions.cs b/src/Aspire.Hosting/Postgres/PostgresBuilderExtensions.cs index 3a40f29d964..9b44666b157 100644 --- a/src/Aspire.Hosting/Postgres/PostgresBuilderExtensions.cs +++ b/src/Aspire.Hosting/Postgres/PostgresBuilderExtensions.cs @@ -27,14 +27,13 @@ public static IResourceBuilder AddPostgres(this IDistrib var postgresServer = new PostgresServerResource(name, password); return builder.AddResource(postgresServer) .WithEndpoint(hostPort: port, containerPort: 5432, name: PostgresServerResource.PrimaryEndpointName) // Internal port is always 5432. - .WithAnnotation(new ContainerImageAnnotation { Image = "postgres", Tag = "16.2" }) + .WithImage("postgres", "16.2") .WithEnvironment("POSTGRES_HOST_AUTH_METHOD", "scram-sha-256") .WithEnvironment("POSTGRES_INITDB_ARGS", "--auth-host=scram-sha-256 --auth-local=scram-sha-256") .WithEnvironment(context => { context.EnvironmentVariables[PasswordEnvVarName] = postgresServer.PasswordInput; - }) - .PublishAsContainer(); + }); } /// @@ -62,7 +61,7 @@ public static IResourceBuilder AddDatabase(this IResou /// The host port for the application ui. /// The name of the container (Optional). /// A reference to the . - public static IResourceBuilder WithPgAdmin(this IResourceBuilder builder, int? hostPort = null, string? containerName = null) where T: PostgresServerResource + public static IResourceBuilder WithPgAdmin(this IResourceBuilder builder, int? hostPort = null, string? containerName = null) where T : PostgresServerResource { if (builder.ApplicationBuilder.Resources.OfType().Any()) { @@ -75,7 +74,7 @@ public static IResourceBuilder WithPgAdmin(this IResourceBuilder builde var pgAdminContainer = new PgAdminContainerResource(containerName); builder.ApplicationBuilder.AddResource(pgAdminContainer) - .WithAnnotation(new ContainerImageAnnotation { Image = "dpage/pgadmin4", Tag = "8.3" }) + .WithImage("dpage/pgadmin4", "8.3") .WithHttpEndpoint(containerPort: 80, hostPort: hostPort, name: containerName) .WithEnvironment(SetPgAdminEnvironmentVariables) .WithBindMount(Path.GetTempFileName(), "/pgadmin4/servers.json") @@ -95,16 +94,6 @@ private static void SetPgAdminEnvironmentVariables(EnvironmentCallbackContext co context.EnvironmentVariables.Add("PGADMIN_DEFAULT_PASSWORD", "admin"); } - /// - /// Changes the PostgreSQL resource to be published as a container in the manifest. - /// - /// The Postgres server resource builder. - /// - public static IResourceBuilder PublishAsContainer(this IResourceBuilder builder) - { - return builder.WithManifestPublishingCallback(context => context.WriteContainerAsync(builder.Resource)); - } - /// /// Adds a named volume for the data folder to a Postgres container resource. /// diff --git a/src/Aspire.Hosting/RabbitMQ/RabbitMQBuilderExtensions.cs b/src/Aspire.Hosting/RabbitMQ/RabbitMQBuilderExtensions.cs index d23c8401ed0..fdb2b798ec1 100644 --- a/src/Aspire.Hosting/RabbitMQ/RabbitMQBuilderExtensions.cs +++ b/src/Aspire.Hosting/RabbitMQ/RabbitMQBuilderExtensions.cs @@ -21,24 +21,13 @@ public static IResourceBuilder AddRabbitMQ(this IDistrib { var rabbitMq = new RabbitMQServerResource(name); return builder.AddResource(rabbitMq) - .WithEndpoint(hostPort: port, containerPort: 5672, name: RabbitMQServerResource.PrimaryEndpointName) - .WithAnnotation(new ContainerImageAnnotation { Image = "rabbitmq", Tag = "3" }) - .WithEnvironment("RABBITMQ_DEFAULT_USER", "guest") - .WithEnvironment(context => - { - context.EnvironmentVariables["RABBITMQ_DEFAULT_PASS"] = rabbitMq.PasswordInput; - }) - .PublishAsContainer(); - } - - /// - /// Changes the RabbitMQ resource to be published as a container in the manifest. - /// - /// Resource builder for . - /// A reference to the . - public static IResourceBuilder PublishAsContainer(this IResourceBuilder builder) - { - return builder.WithManifestPublishingCallback(context => context.WriteContainerAsync(builder.Resource)); + .WithEndpoint(hostPort: port, containerPort: 5672, name: RabbitMQServerResource.PrimaryEndpointName) + .WithImage("rabbitmq", "3") + .WithEnvironment("RABBITMQ_DEFAULT_USER", "guest") + .WithEnvironment(context => + { + context.EnvironmentVariables["RABBITMQ_DEFAULT_PASS"] = rabbitMq.PasswordInput; + }); } /// diff --git a/src/Aspire.Hosting/Redis/RedisBuilderExtensions.cs b/src/Aspire.Hosting/Redis/RedisBuilderExtensions.cs index ec7cd3cbdbc..4fc1f857c4b 100644 --- a/src/Aspire.Hosting/Redis/RedisBuilderExtensions.cs +++ b/src/Aspire.Hosting/Redis/RedisBuilderExtensions.cs @@ -24,8 +24,7 @@ public static IResourceBuilder AddRedis(this IDistributedApplicat var redis = new RedisResource(name); return builder.AddResource(redis) .WithEndpoint(hostPort: port, containerPort: 6379, name: RedisResource.PrimaryEndpointName) - .WithAnnotation(new ContainerImageAnnotation { Image = "redis", Tag = "7.2.4" }) - .PublishAsContainer(); + .WithImage("redis", "7.2.4"); } /// @@ -48,7 +47,7 @@ public static IResourceBuilder WithRedisCommander(this IResourceB var resource = new RedisCommanderResource(containerName); builder.ApplicationBuilder.AddResource(resource) - .WithAnnotation(new ContainerImageAnnotation { Image = "rediscommander/redis-commander", Tag = "latest" }) + .WithImage("rediscommander/redis-commander", "latest") .WithHttpEndpoint(containerPort: 8081, hostPort: hostPort, name: containerName) .ExcludeFromManifest(); diff --git a/src/Aspire.Hosting/Seq/SeqBuilderExtensions.cs b/src/Aspire.Hosting/Seq/SeqBuilderExtensions.cs index 417d3fdc269..fdeba93b44a 100644 --- a/src/Aspire.Hosting/Seq/SeqBuilderExtensions.cs +++ b/src/Aspire.Hosting/Seq/SeqBuilderExtensions.cs @@ -29,8 +29,7 @@ public static IResourceBuilder AddSeq( var seqResource = new SeqResource(name); var resourceBuilder = builder.AddResource(seqResource) .WithHttpEndpoint(hostPort: port, containerPort: 80, name: SeqResource.PrimaryEndpointName) - .WithAnnotation(new ContainerImageAnnotation { Image = "datalust/seq" }) - .WithImageTag("2024.1") + .WithImage("datalust/seq", "2024.1") .WithEnvironment("ACCEPT_EULA", "Y"); if (!string.IsNullOrEmpty(seqDataDirectory)) diff --git a/src/Aspire.Hosting/SqlServer/SqlServerBuilderExtensions.cs b/src/Aspire.Hosting/SqlServer/SqlServerBuilderExtensions.cs index 18c4191dede..ae405626ab7 100644 --- a/src/Aspire.Hosting/SqlServer/SqlServerBuilderExtensions.cs +++ b/src/Aspire.Hosting/SqlServer/SqlServerBuilderExtensions.cs @@ -24,23 +24,13 @@ public static IResourceBuilder AddSqlServer(this IDistr return builder.AddResource(sqlServer) .WithEndpoint(hostPort: port, containerPort: 1433, name: SqlServerServerResource.PrimaryEndpointName) - .WithAnnotation(new ContainerImageAnnotation { Registry = "mcr.microsoft.com", Image = "mssql/server", Tag = "2022-latest" }) + .WithImage("mssql/server", "2022-latest") + .WithImageRegistry("mcr.microsoft.com") .WithEnvironment("ACCEPT_EULA", "Y") .WithEnvironment(context => { context.EnvironmentVariables["MSSQL_SA_PASSWORD"] = sqlServer.PasswordInput; - }) - .PublishAsContainer(); - } - - /// - /// Changes the SQL Server resource to be published as a container. - /// - /// Builder for the underlying . - /// - public static IResourceBuilder PublishAsContainer(this IResourceBuilder builder) - { - return builder.WithManifestPublishingCallback(context => context.WriteContainerAsync(builder.Resource)); + }); } /// diff --git a/tests/Aspire.Hosting.Tests/ContainerResourceBuilderTests.cs b/tests/Aspire.Hosting.Tests/ContainerResourceBuilderTests.cs index d26700e340a..ca4926b302d 100644 --- a/tests/Aspire.Hosting.Tests/ContainerResourceBuilderTests.cs +++ b/tests/Aspire.Hosting.Tests/ContainerResourceBuilderTests.cs @@ -41,7 +41,7 @@ public void WithImageMutatesImageNameOfLastAnnotation() { var builder = DistributedApplication.CreateBuilder(); var container = builder.AddContainer("app", "some-image"); - container.Resource.Annotations.Add(new ContainerImageAnnotation { Image = "another-image" } ); + container.Resource.Annotations.Add(new ContainerImageAnnotation { Image = "another-image" }); container.WithImage("new-image"); Assert.Equal("new-image", container.Resource.Annotations.OfType().Last().Image); @@ -71,4 +71,38 @@ public void WithImageSHA256MutatesImageSHA256() var redis = builder.AddRedis("redis").WithImageSHA256("42b5c726e719639fcc1e9dbc13dd843f567dcd37911d0e1abb9f47f2cc1c95cd"); Assert.Equal("42b5c726e719639fcc1e9dbc13dd843f567dcd37911d0e1abb9f47f2cc1c95cd", redis.Resource.Annotations.OfType().Single().SHA256); } + + [Fact] + public void WithImageTagThrowsIfNoImageAnnotation() + { + var builder = DistributedApplication.CreateBuilder(); + var container = builder.AddResource(new TestContainerResource("testcontainer")); + + var exception = Assert.Throws(() => container.WithImageTag("7.2.4")); + Assert.Equal("The resource 'testcontainer' does not have a container image specified. Use WithImage to specify the container image and tag.", exception.Message); + } + + [Fact] + public void WithImageRegistryThrowsIfNoImageAnnotation() + { + var builder = DistributedApplication.CreateBuilder(); + var container = builder.AddResource(new TestContainerResource("testcontainer")); + + var exception = Assert.Throws(() => container.WithImageRegistry("myregistry.azurecr.io")); + Assert.Equal("The resource 'testcontainer' does not have a container image specified. Use WithImage to specify the container image and tag.", exception.Message); + } + + [Fact] + public void WithImageSHA256ThrowsIfNoImageAnnotation() + { + var builder = DistributedApplication.CreateBuilder(); + var container = builder.AddResource(new TestContainerResource("testcontainer")); + + var exception = Assert.Throws(() => container.WithImageSHA256("42b5c726e719639fcc1e9dbc13dd843f567dcd37911d0e1abb9f47f2cc1c95cd")); + Assert.Equal("The resource 'testcontainer' does not have a container image specified. Use WithImage to specify the container image and tag.", exception.Message); + } + + private sealed class TestContainerResource(string name) : ContainerResource(name) + { + } } diff --git a/tests/Aspire.Hosting.Tests/Kafka/AddKafkaTests.cs b/tests/Aspire.Hosting.Tests/Kafka/AddKafkaTests.cs index 3e618641e9c..ec2f252663a 100644 --- a/tests/Aspire.Hosting.Tests/Kafka/AddKafkaTests.cs +++ b/tests/Aspire.Hosting.Tests/Kafka/AddKafkaTests.cs @@ -23,9 +23,6 @@ public void AddKafkaContainerWithDefaultsAddsAnnotationMetadata() var containerResource = Assert.Single(appModel.Resources.OfType()); Assert.Equal("kafka", containerResource.Name); - var manifestAnnotation = Assert.Single(containerResource.Annotations.OfType()); - Assert.NotNull(manifestAnnotation.Callback); - var endpoint = Assert.Single(containerResource.Annotations.OfType()); Assert.Equal(9092, endpoint.ContainerPort); Assert.False(endpoint.IsExternal); diff --git a/tests/Aspire.Hosting.Tests/MongoDB/AddMongoDBTests.cs b/tests/Aspire.Hosting.Tests/MongoDB/AddMongoDBTests.cs index 302dfab1987..872228dfae0 100644 --- a/tests/Aspire.Hosting.Tests/MongoDB/AddMongoDBTests.cs +++ b/tests/Aspire.Hosting.Tests/MongoDB/AddMongoDBTests.cs @@ -25,9 +25,6 @@ public void AddMongoDBContainerWithDefaultsAddsAnnotationMetadata() var containerResource = Assert.Single(appModel.Resources.OfType()); Assert.Equal("mongodb", containerResource.Name); - var manifestAnnotation = Assert.Single(containerResource.Annotations.OfType()); - Assert.NotNull(manifestAnnotation.Callback); - var endpoint = Assert.Single(containerResource.Annotations.OfType()); Assert.Equal(27017, endpoint.ContainerPort); Assert.False(endpoint.IsExternal); @@ -56,9 +53,6 @@ public void AddMongoDBContainerAddsAnnotationMetadata() var containerResource = Assert.Single(appModel.Resources.OfType()); Assert.Equal("mongodb", containerResource.Name); - var manifestAnnotation = Assert.Single(containerResource.Annotations.OfType()); - Assert.NotNull(manifestAnnotation.Callback); - var endpoint = Assert.Single(containerResource.Annotations.OfType()); Assert.Equal(27017, endpoint.ContainerPort); Assert.False(endpoint.IsExternal); diff --git a/tests/Aspire.Hosting.Tests/MongoDB/MongoDBConnectionStringBuilderTests.cs b/tests/Aspire.Hosting.Tests/MongoDB/MongoDBConnectionStringBuilderTests.cs deleted file mode 100644 index 698aac5f53b..00000000000 --- a/tests/Aspire.Hosting.Tests/MongoDB/MongoDBConnectionStringBuilderTests.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 Aspire.Hosting.MongoDB; -using MongoDB.Driver; -using Xunit; - -namespace Aspire.Hosting.Tests.MongoDB; - -public class MongoDBConnectionStringBuilderTests -{ - [Theory] - [InlineData("password", "mongodb://root:password@myserver:1000/")] - [InlineData("@abc!$", "mongodb://root:%40abc!$@myserver:1000/")] - [InlineData("mypasswordwitha\"inthemiddle", "mongodb://root:mypasswordwitha\"inthemiddle@myserver:1000/")] - [InlineData("mypasswordwitha\"attheend\"", "mongodb://root:mypasswordwitha\"attheend\"@myserver:1000/")] - [InlineData("\"mypasswordwitha\"atthestart", "mongodb://root:\"mypasswordwitha\"atthestart@myserver:1000/")] - [InlineData("mypasswordwitha'inthemiddle", "mongodb://root:mypasswordwitha'inthemiddle@myserver:1000/")] - [InlineData("mypasswordwitha'attheend'", "mongodb://root:mypasswordwitha'attheend'@myserver:1000/")] - [InlineData("'mypasswordwitha'atthestart", "mongodb://root:'mypasswordwitha'atthestart@myserver:1000/")] - public void TestSpecialCharactersAndEscapeForPassword(string password, string expectedConnectionString) - { - var connectionString = new MongoDBConnectionStringBuilder() - .WithServer("myserver") - .WithPort(1000) - .WithUserName("root") - .WithPassword(password) - .Build(); - - Assert.NotNull(connectionString); - - var builder = MongoUrl.Create(connectionString); - Assert.Equal(password, builder.Password); - Assert.Equal(expectedConnectionString, connectionString); - } -} diff --git a/tests/Aspire.Hosting.Tests/MySql/AddMySqlTests.cs b/tests/Aspire.Hosting.Tests/MySql/AddMySqlTests.cs index 00cf58a90e5..bc364493681 100644 --- a/tests/Aspire.Hosting.Tests/MySql/AddMySqlTests.cs +++ b/tests/Aspire.Hosting.Tests/MySql/AddMySqlTests.cs @@ -26,9 +26,6 @@ public async Task AddMySqlContainerWithDefaultsAddsAnnotationMetadata() var containerResource = Assert.Single(appModel.Resources.OfType()); Assert.Equal("mysql", containerResource.Name); - var manifestAnnotation = Assert.Single(containerResource.Annotations.OfType()); - Assert.NotNull(manifestAnnotation.Callback); - var containerAnnotation = Assert.Single(containerResource.Annotations.OfType()); Assert.Equal("8.3.0", containerAnnotation.Tag); Assert.Equal("mysql", containerAnnotation.Image); @@ -66,9 +63,6 @@ public async Task AddMySqlAddsAnnotationMetadata() var containerResource = Assert.Single(appModel.GetContainerResources()); Assert.Equal("mysql", containerResource.Name); - var manifestPublishing = Assert.Single(containerResource.Annotations.OfType()); - Assert.NotNull(manifestPublishing.Callback); - var containerAnnotation = Assert.Single(containerResource.Annotations.OfType()); Assert.Equal("8.3.0", containerAnnotation.Tag); Assert.Equal("mysql", containerAnnotation.Image); diff --git a/tests/Aspire.Hosting.Tests/Nats/AddNatsTests.cs b/tests/Aspire.Hosting.Tests/Nats/AddNatsTests.cs index 136537f3ae0..2f6a0c748a2 100644 --- a/tests/Aspire.Hosting.Tests/Nats/AddNatsTests.cs +++ b/tests/Aspire.Hosting.Tests/Nats/AddNatsTests.cs @@ -24,9 +24,6 @@ public void AddNatsContainerWithDefaultsAddsAnnotationMetadata() var containerResource = Assert.Single(appModel.Resources.OfType()); Assert.Equal("nats", containerResource.Name); - var manifestAnnotation = Assert.Single(containerResource.Annotations.OfType()); - Assert.NotNull(manifestAnnotation.Callback); - var endpoint = Assert.Single(containerResource.Annotations.OfType()); Assert.Equal(4222, endpoint.ContainerPort); Assert.False(endpoint.IsExternal); @@ -55,9 +52,6 @@ public void AddNatsContainerAddsAnnotationMetadata() var containerResource = Assert.Single(appModel.Resources.OfType()); Assert.Equal("nats", containerResource.Name); - var manifestAnnotation = Assert.Single(containerResource.Annotations.OfType()); - Assert.NotNull(manifestAnnotation.Callback); - var mountAnnotation = Assert.Single(containerResource.Annotations.OfType()); Assert.Equal("/tmp/dev-data", mountAnnotation.Source); Assert.Equal("/data", mountAnnotation.Target); diff --git a/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs b/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs index c2bb2de96f3..bb3ee95cd3f 100644 --- a/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs +++ b/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs @@ -24,9 +24,6 @@ public async Task AddOracleWithDefaultsAddsAnnotationMetadata() var containerResource = Assert.Single(appModel.GetContainerResources()); Assert.Equal("orcl", containerResource.Name); - var manifestPublishing = Assert.Single(containerResource.Annotations.OfType()); - Assert.NotNull(manifestPublishing.Callback); - var containerAnnotation = Assert.Single(containerResource.Annotations.OfType()); Assert.Equal("23.3.0.0", containerAnnotation.Tag); Assert.Equal("database/free", containerAnnotation.Image); diff --git a/tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs b/tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs index ce04d105f6f..f8ae67080cb 100644 --- a/tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs +++ b/tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs @@ -26,9 +26,6 @@ public async Task AddPostgresWithDefaultsAddsAnnotationMetadata() var containerResource = Assert.Single(appModel.GetContainerResources()); Assert.Equal("myPostgres", containerResource.Name); - var manifestPublishing = Assert.Single(containerResource.Annotations.OfType()); - Assert.NotNull(manifestPublishing.Callback); - var containerAnnotation = Assert.Single(containerResource.Annotations.OfType()); Assert.Equal("16.2", containerAnnotation.Tag); Assert.Equal("postgres", containerAnnotation.Image); @@ -76,9 +73,6 @@ public async Task AddPostgresAddsAnnotationMetadata() var containerResource = Assert.Single(appModel.GetContainerResources()); Assert.Equal("myPostgres", containerResource.Name); - var manifestPublishing = Assert.Single(containerResource.Annotations.OfType()); - Assert.NotNull(manifestPublishing.Callback); - var containerAnnotation = Assert.Single(containerResource.Annotations.OfType()); Assert.Equal("16.2", containerAnnotation.Tag); Assert.Equal("postgres", containerAnnotation.Image); @@ -160,9 +154,6 @@ public async Task AddDatabaseToPostgresAddsAnnotationMetadata() var containerResource = Assert.Single(containerResources); Assert.Equal("postgres", containerResource.Name); - var manifestPublishing = Assert.Single(containerResource.Annotations.OfType()); - Assert.NotNull(manifestPublishing.Callback); - var containerAnnotation = Assert.Single(containerResource.Annotations.OfType()); Assert.Equal("16.2", containerAnnotation.Tag); Assert.Equal("postgres", containerAnnotation.Image); diff --git a/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs b/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs index c083e1ef5c4..cf3bafe3815 100644 --- a/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs +++ b/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs @@ -24,9 +24,6 @@ public void AddRabbitMQContainerWithDefaultsAddsAnnotationMetadata() var containerResource = Assert.Single(appModel.Resources.OfType()); Assert.Equal("rabbit", containerResource.Name); - var manifestAnnotation = Assert.Single(containerResource.Annotations.OfType()); - Assert.NotNull(manifestAnnotation.Callback); - var endpoint = Assert.Single(containerResource.Annotations.OfType()); Assert.Equal(5672, endpoint.ContainerPort); Assert.False(endpoint.IsExternal); diff --git a/tests/Aspire.Hosting.Tests/Redis/AddRedisTests.cs b/tests/Aspire.Hosting.Tests/Redis/AddRedisTests.cs index e423320f275..86ec7949d5d 100644 --- a/tests/Aspire.Hosting.Tests/Redis/AddRedisTests.cs +++ b/tests/Aspire.Hosting.Tests/Redis/AddRedisTests.cs @@ -25,9 +25,6 @@ public void AddRedisContainerWithDefaultsAddsAnnotationMetadata() var containerResource = Assert.Single(appModel.Resources.OfType()); Assert.Equal("myRedis", containerResource.Name); - var manifestAnnotation = Assert.Single(containerResource.Annotations.OfType()); - Assert.NotNull(manifestAnnotation.Callback); - var endpoint = Assert.Single(containerResource.Annotations.OfType()); Assert.Equal(6379, endpoint.ContainerPort); Assert.False(endpoint.IsExternal); @@ -56,9 +53,6 @@ public void AddRedisContainerAddsAnnotationMetadata() var containerResource = Assert.Single(appModel.Resources.OfType()); Assert.Equal("myRedis", containerResource.Name); - var manifestAnnotation = Assert.Single(containerResource.Annotations.OfType()); - Assert.NotNull(manifestAnnotation.Callback); - var endpoint = Assert.Single(containerResource.Annotations.OfType()); Assert.Equal(6379, endpoint.ContainerPort); Assert.False(endpoint.IsExternal); diff --git a/tests/Aspire.Hosting.Tests/SqlServer/AddSqlServerTests.cs b/tests/Aspire.Hosting.Tests/SqlServer/AddSqlServerTests.cs index eac3cf765d4..96a0001ed28 100644 --- a/tests/Aspire.Hosting.Tests/SqlServer/AddSqlServerTests.cs +++ b/tests/Aspire.Hosting.Tests/SqlServer/AddSqlServerTests.cs @@ -25,9 +25,6 @@ public async Task AddSqlServerContainerWithDefaultsAddsAnnotationMetadata() var containerResource = Assert.Single(appModel.Resources.OfType()); Assert.Equal("sqlserver", containerResource.Name); - var manifestAnnotation = Assert.Single(containerResource.Annotations.OfType()); - Assert.NotNull(manifestAnnotation.Callback); - var endpoint = Assert.Single(containerResource.Annotations.OfType()); Assert.Equal(1433, endpoint.ContainerPort); Assert.False(endpoint.IsExternal); From e20552be5193a23a7ecb9fac217f49de5d9c1c8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Ros?= Date: Mon, 11 Mar 2024 16:14:08 -0700 Subject: [PATCH 43/50] Fix docs after client renaming (#2781) --- .../CosmosEndToEnd.ApiService/Program.cs | 2 +- playground/bicep/BicepSample.ApiService/Program.cs | 2 +- src/Components/Aspire.Azure.AI.OpenAI/README.md | 10 +++++----- src/Components/Aspire.Azure.Data.Tables/README.md | 10 +++++----- .../Aspire.Azure.Messaging.ServiceBus/README.md | 10 +++++----- .../Aspire.Azure.Search.Documents/README.md | 10 +++++----- .../Aspire.Azure.Security.KeyVault/README.md | 12 ++++++------ src/Components/Aspire.Azure.Storage.Blobs/README.md | 10 +++++----- src/Components/Aspire.Azure.Storage.Queues/README.md | 10 +++++----- .../AspireAzureCosmosDBExtensions.cs | 6 +++--- .../Aspire.Microsoft.Azure.Cosmos/README.md | 10 +++++----- src/Components/Aspire.RabbitMQ.Client/README.md | 10 +++++----- src/Components/Aspire.StackExchange.Redis/README.md | 10 +++++----- .../ConformanceTests.cs | 2 +- .../TestProject.IntegrationServiceA/Program.cs | 2 +- 15 files changed, 58 insertions(+), 58 deletions(-) diff --git a/playground/CosmosEndToEnd/CosmosEndToEnd.ApiService/Program.cs b/playground/CosmosEndToEnd/CosmosEndToEnd.ApiService/Program.cs index 252d5abc1e3..404e85f0004 100644 --- a/playground/CosmosEndToEnd/CosmosEndToEnd.ApiService/Program.cs +++ b/playground/CosmosEndToEnd/CosmosEndToEnd.ApiService/Program.cs @@ -8,7 +8,7 @@ var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); -builder.AddAzureCosmosDbClient("cosmos"); +builder.AddAzureCosmosDBClient("cosmos"); builder.AddCosmosDbContext("cosmos", "ef"); var app = builder.Build(); diff --git a/playground/bicep/BicepSample.ApiService/Program.cs b/playground/bicep/BicepSample.ApiService/Program.cs index 870ba503663..af1c832bca6 100644 --- a/playground/bicep/BicepSample.ApiService/Program.cs +++ b/playground/bicep/BicepSample.ApiService/Program.cs @@ -16,7 +16,7 @@ builder.AddSqlServerDbContext("db"); builder.AddNpgsqlDbContext("db2"); -builder.AddAzureCosmosDbClient("cosmos"); +builder.AddAzureCosmosDBClient("cosmos"); builder.AddRedisClient("redis"); builder.AddAzureBlobClient("blob"); builder.AddAzureTableClient("table"); diff --git a/src/Components/Aspire.Azure.AI.OpenAI/README.md b/src/Components/Aspire.Azure.AI.OpenAI/README.md index 93c31ca86aa..75861531034 100644 --- a/src/Components/Aspire.Azure.AI.OpenAI/README.md +++ b/src/Components/Aspire.Azure.AI.OpenAI/README.md @@ -44,10 +44,10 @@ The .NET Aspire Azure Azure OpenAI library provides multiple options to configur ### Use a connection string -A connection can be constructed from the __Keys and Endpoint__ tab with the format `Endpoint={endpoint};Key={key};`. You can provide the name of the connection string when calling `builder.AddAzureAIOpenAI()`: +A connection can be constructed from the __Keys and Endpoint__ tab with the format `Endpoint={endpoint};Key={key};`. You can provide the name of the connection string when calling `builder.AddAzureAIOpenAIClient()`: ```csharp -builder.AddAzureAIOpenAI("openaiConnectionName"); +builder.AddAzureAIOpenAIClient("openaiConnectionName"); ``` And then the connection string will be retrieved from the `ConnectionStrings` configuration section. Two connection formats are supported: @@ -101,13 +101,13 @@ The .NET Aspire Azure AI OpenAI library supports [Microsoft.Extensions.Configura You can also pass the `Action configureSettings` delegate to set up some or all the options inline, for example to disable tracing from code: ```csharp -builder.AddAzureAIOpenAI("openaiConnectionName", settings => settings.Tracing = false); +builder.AddAzureAIOpenAIClient("openaiConnectionName", settings => settings.Tracing = false); ``` -You can also setup the [OpenAIClientOptions](https://learn.microsoft.com/dotnet/api/azure.ai.openai.openaiclientoptions) using the optional `Action> configureClientBuilder` parameter of the `AddAzureAIOpenAI` method. For example, to set the client ID for this client: +You can also setup the [OpenAIClientOptions](https://learn.microsoft.com/dotnet/api/azure.ai.openai.openaiclientoptions) using the optional `Action> configureClientBuilder` parameter of the `AddAzureAIOpenAIClient` method. For example, to set the client ID for this client: ```csharp -builder.AddAzureAIOpenAI("openaiConnectionName", configureClientBuilder: builder => builder.ConfigureOptions(options => options.Diagnostics.ApplicationId = "CLIENT_ID")); +builder.AddAzureAIOpenAIClient("openaiConnectionName", configureClientBuilder: builder => builder.ConfigureOptions(options => options.Diagnostics.ApplicationId = "CLIENT_ID")); ``` ## AppHost extensions diff --git a/src/Components/Aspire.Azure.Data.Tables/README.md b/src/Components/Aspire.Azure.Data.Tables/README.md index 67d22e03ab8..c550bf44056 100644 --- a/src/Components/Aspire.Azure.Data.Tables/README.md +++ b/src/Components/Aspire.Azure.Data.Tables/README.md @@ -44,10 +44,10 @@ The .NET Aspire Azure Table storage library provides multiple options to configu ### Use a connection string -When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddAzureTableService()`: +When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddAzureTableClient()`: ```csharp -builder.AddAzureTableService("tableConnectionName"); +builder.AddAzureTableClient("tableConnectionName"); ``` And then the connection information will be retrieved from the `ConnectionStrings` configuration section. Two connection formats are supported: @@ -105,13 +105,13 @@ The Azure Table storage library supports [Microsoft.Extensions.Configuration](ht You can also pass the `Action configureSettings` delegate to set up some or all the options inline, for example to disable health checks from code: ```csharp -builder.AddAzureTableService("tables", settings => settings.HealthChecks = false); +builder.AddAzureTableClient("tables", settings => settings.HealthChecks = false); ``` -You can also setup the [TableClientOptions](https://learn.microsoft.com/dotnet/api/azure.data.tables.tableclientoptions) using the optional `Action> configureClientBuilder` parameter of the `AddAzureTableService` method. For example, to set the first part of "User-Agent" headers for all requests issues by this client: +You can also setup the [TableClientOptions](https://learn.microsoft.com/dotnet/api/azure.data.tables.tableclientoptions) using the optional `Action> configureClientBuilder` parameter of the `AddAzureTableClient` method. For example, to set the first part of "User-Agent" headers for all requests issues by this client: ```csharp -builder.AddAzureTableService("tables", configureClientBuilder: clientBuilder => clientBuilder.ConfigureOptions(options => options.Diagnostics.ApplicationId = "myapp")); +builder.AddAzureTableClient("tables", configureClientBuilder: clientBuilder => clientBuilder.ConfigureOptions(options => options.Diagnostics.ApplicationId = "myapp")); ``` ## AppHost extensions diff --git a/src/Components/Aspire.Azure.Messaging.ServiceBus/README.md b/src/Components/Aspire.Azure.Messaging.ServiceBus/README.md index 319f3e00bfd..69e54664dbf 100644 --- a/src/Components/Aspire.Azure.Messaging.ServiceBus/README.md +++ b/src/Components/Aspire.Azure.Messaging.ServiceBus/README.md @@ -44,10 +44,10 @@ The .NET Aspire Azure Service Bus library provides multiple options to configure ### Use a connection string -When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddAzureServiceBus()`: +When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddAzureServiceBusClient()`: ```csharp -builder.AddAzureServiceBus("serviceBusConnectionName"); +builder.AddAzureServiceBusClient("serviceBusConnectionName"); ``` And then the connection information will be retrieved from the `ConnectionStrings` configuration section. Two connection formats are supported: @@ -103,13 +103,13 @@ The .NET Aspire Azure Service Bus library supports [Microsoft.Extensions.Configu You can also pass the `Action configureSettings` delegate to set up some or all the options inline, for example to configure the health check queue name from code: ```csharp -builder.AddAzureServiceBus("sb", settings => settings.HealthCheckQueueName = "myQueue"); +builder.AddAzureServiceBusClient("sb", settings => settings.HealthCheckQueueName = "myQueue"); ``` -You can also setup the [ServiceBusClientOptions](https://learn.microsoft.com/dotnet/api/azure.messaging.servicebus.servicebusclientoptions) using the optional `Action> configureClientBuilder` parameter of the `AddAzureServiceBus` method. For example, to set the client ID for this client: +You can also setup the [ServiceBusClientOptions](https://learn.microsoft.com/dotnet/api/azure.messaging.servicebus.servicebusclientoptions) using the optional `Action> configureClientBuilder` parameter of the `AddAzureServiceBusClient` method. For example, to set the client ID for this client: ```csharp -builder.AddAzureServiceBus("sb", configureClientBuilder: clientBuilder => clientBuilder.ConfigureOptions(options => options.Identifier = "CLIENT_ID")); +builder.AddAzureServiceBusClient("sb", configureClientBuilder: clientBuilder => clientBuilder.ConfigureOptions(options => options.Identifier = "CLIENT_ID")); ``` ## AppHost extensions diff --git a/src/Components/Aspire.Azure.Search.Documents/README.md b/src/Components/Aspire.Azure.Search.Documents/README.md index 1b958ba58ab..05e0ec848f7 100644 --- a/src/Components/Aspire.Azure.Search.Documents/README.md +++ b/src/Components/Aspire.Azure.Search.Documents/README.md @@ -62,10 +62,10 @@ The .NET Aspire Azure Azure Search library provides multiple options to configur ### Use a connection string -A connection can be constructed from the __Keys and Endpoint__ tab with the format `Endpoint={endpoint};Key={key};`. You can provide the name of the connection string when calling `builder.AddAzureSearch()`: +A connection can be constructed from the __Keys and Endpoint__ tab with the format `Endpoint={endpoint};Key={key};`. You can provide the name of the connection string when calling `builder.AddAzureSearchClient()`: ```csharp -builder.AddAzureSearch("searchConnectionName"); +builder.AddAzureSearchClient("searchConnectionName"); ``` And then the connection string will be retrieved from the `ConnectionStrings` configuration section. Two connection formats are supported: @@ -117,13 +117,13 @@ The .NET Aspire Azure Search library supports [Microsoft.Extensions.Configuratio You can also pass the `Action configureSettings` delegate to set up some or all the options inline, for example to disable tracing from code: ```csharp -builder.AddAzureSearch("searchConnectionName", settings => settings.Tracing = false); +builder.AddAzureSearchClient("searchConnectionName", settings => settings.Tracing = false); ``` -You can also setup the [SearchClientOptions](https://learn.microsoft.com/dotnet/api/azure.search.documents.searchclientoptions) using the optional `Action> configureClientBuilder` parameter of the `AddAzureSearch` method. For example, to set the client ID for this client: +You can also setup the [SearchClientOptions](https://learn.microsoft.com/dotnet/api/azure.search.documents.searchclientoptions) using the optional `Action> configureClientBuilder` parameter of the `AddAzureSearchClient` method. For example, to set the client ID for this client: ```csharp -builder.AddAzureSearch("searchConnectionName", configureClientBuilder: builder => builder.ConfigureOptions(options => options.Diagnostics.ApplicationId = "CLIENT_ID")); +builder.AddAzureSearchClient("searchConnectionName", configureClientBuilder: builder => builder.ConfigureOptions(options => options.Diagnostics.ApplicationId = "CLIENT_ID")); ``` ## AppHost extensions diff --git a/src/Components/Aspire.Azure.Security.KeyVault/README.md b/src/Components/Aspire.Azure.Security.KeyVault/README.md index ab8b9ae6180..50b2c383d78 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/README.md +++ b/src/Components/Aspire.Azure.Security.KeyVault/README.md @@ -63,10 +63,10 @@ The .NET Aspire Azure Key Vault library provides multiple options to configure t ### Use a connection string -When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddAzureKeyVaultSecrets()`: +When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddAzureKeyVaultClient()`: ```csharp -builder.AddAzureKeyVaultSecrets("secretConnectionName"); +builder.AddAzureKeyVaultClient("secretConnectionName"); ``` And then the vault URI will be retrieved from the `ConnectionStrings` configuration section. The vault URI which works with the `AzureSecurityKeyVaultSettings.Credential` property to establish a connection. If no credential is configured, the [DefaultAzureCredential](https://learn.microsoft.com/dotnet/api/azure.identity.defaultazurecredential) is used. @@ -108,13 +108,13 @@ The .NET Aspire Azure Key Vault library supports [Microsoft.Extensions.Configura You can also pass the `Action configureSettings` delegate to set up some or all the options inline, for example to disable health checks from code: ```csharp -builder.AddAzureKeyVaultSecrets("secrets", settings => settings.HealthChecks = false); +builder.AddAzureKeyVaultClient("secrets", settings => settings.HealthChecks = false); ``` -You can also setup the [SecretClientOptions](https://learn.microsoft.com/dotnet/api/azure.security.keyvault.secrets.secretclientoptions) using the optional `Action> configureClientBuilder` parameter of the `AddAzureKeyVaultSecrets` method. For example, to set the first part of "User-Agent" headers for all requests issues by this client: +You can also setup the [SecretClientOptions](https://learn.microsoft.com/dotnet/api/azure.security.keyvault.secrets.secretclientoptions) using the optional `Action> configureClientBuilder` parameter of the `AddAzureKeyVaultClient` method. For example, to set the first part of "User-Agent" headers for all requests issues by this client: ```csharp -builder.AddAzureKeyVaultSecrets("secrets", configureClientBuilder: clientBuilder => clientBuilder.ConfigureOptions(options => options.Diagnostics.ApplicationId = "myapp")); +builder.AddAzureKeyVaultClient("secrets", configureClientBuilder: clientBuilder => clientBuilder.ConfigureOptions(options => options.Diagnostics.ApplicationId = "myapp")); ``` ## AppHost extensions @@ -137,7 +137,7 @@ var myService = builder.AddProject() The `AddAzureKeyVault` method will read connection information from the AppHost's configuration (for example, from "user secrets") under the `ConnectionStrings:secrets` config key. The `WithReference` method passes that connection information into a connection string named `secrets` in the `MyService` project. In the _Program.cs_ file of `MyService`, the connection can be consumed using: ```csharp -builder.AddKeyVaultSecretsClient("secrets"); +builder.AddAzureKeyVaultClient("secrets"); ``` ## Additional documentation diff --git a/src/Components/Aspire.Azure.Storage.Blobs/README.md b/src/Components/Aspire.Azure.Storage.Blobs/README.md index a36c928c039..f597bb146a6 100644 --- a/src/Components/Aspire.Azure.Storage.Blobs/README.md +++ b/src/Components/Aspire.Azure.Storage.Blobs/README.md @@ -44,10 +44,10 @@ The .NET Aspire Azure Storage Blobs library provides multiple options to configu ### Use a connection string -When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddAzureBlobService()`: +When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddAzureBlobClient()`: ```csharp -builder.AddAzureBlobService("blobsConnectionName"); +builder.AddAzureBlobClient("blobsConnectionName"); ``` And then the connection information will be retrieved from the `ConnectionStrings` configuration section. Two connection formats are supported: @@ -105,13 +105,13 @@ The .NET Aspire Azure Storage Blobs library supports [Microsoft.Extensions.Confi You can also pass the `Action configureSettings` delegate to set up some or all the options inline, for example to disable health checks from code: ```csharp -builder.AddAzureBlobService("blobs", settings => settings.HealthChecks = false); +builder.AddAzureBlobClient("blobs", settings => settings.HealthChecks = false); ``` -You can also setup the [BlobClientOptions](https://learn.microsoft.com/dotnet/api/azure.storage.blobs.blobclientoptions) using the optional `Action> configureClientBuilder` parameter of the `AddAzureBlobService` method. For example, to set the first part of "User-Agent" headers for all requests issues by this client: +You can also setup the [BlobClientOptions](https://learn.microsoft.com/dotnet/api/azure.storage.blobs.blobclientoptions) using the optional `Action> configureClientBuilder` parameter of the `AddAzureBlobClient` method. For example, to set the first part of "User-Agent" headers for all requests issues by this client: ```csharp -builder.AddAzureBlobService("blobs", configureClientBuilder: clientBuilder => clientBuilder.ConfigureOptions(options => options.Diagnostics.ApplicationId = "myapp")); +builder.AddAzureBlobClient("blobs", configureClientBuilder: clientBuilder => clientBuilder.ConfigureOptions(options => options.Diagnostics.ApplicationId = "myapp")); ``` ## AppHost extensions diff --git a/src/Components/Aspire.Azure.Storage.Queues/README.md b/src/Components/Aspire.Azure.Storage.Queues/README.md index 1fe085f7cec..1b4b7ab733a 100644 --- a/src/Components/Aspire.Azure.Storage.Queues/README.md +++ b/src/Components/Aspire.Azure.Storage.Queues/README.md @@ -44,10 +44,10 @@ The .NET Aspire Azure Storage Queues library provides multiple options to config ### Use a connection string -When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddAzureQueueService()`: +When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddAzureQueueClient()`: ```csharp -builder.AddAzureQueueService("queueConnectionName"); +builder.AddAzureQueueClient("queueConnectionName"); ``` And then the connection string will be retrieved from the `ConnectionStrings` configuration section. Two connection formats are supported: @@ -105,13 +105,13 @@ The .NET Aspire Azure Storage Queues library supports [Microsoft.Extensions.Conf You can also pass the `Action configureSettings` delegate to set up some or all the options inline, for example to disable health checks from code: ```csharp -builder.AddAzureQueueService("queue", settings => settings.HealthChecks = false); +builder.AddAzureQueueClient("queue", settings => settings.HealthChecks = false); ``` -You can also setup the [QueueClientOptions](https://learn.microsoft.com/dotnet/api/azure.storage.queues.queueclientoptions) using the optional `Action> configureClientBuilder` parameter of the `AddAzureQueueService` method. For example, to set the first part of "User-Agent" headers for all requests issues by this client: +You can also setup the [QueueClientOptions](https://learn.microsoft.com/dotnet/api/azure.storage.queues.queueclientoptions) using the optional `Action> configureClientBuilder` parameter of the `AddAzureQueueClient` method. For example, to set the first part of "User-Agent" headers for all requests issues by this client: ```csharp -builder.AddAzureQueueService("queue", configureClientBuilder: clientBuilder => clientBuilder.ConfigureOptions(options => options.Diagnostics.ApplicationId = "myapp")); +builder.AddAzureQueueClient("queue", configureClientBuilder: clientBuilder => clientBuilder.ConfigureOptions(options => options.Diagnostics.ApplicationId = "myapp")); ``` ## AppHost extensions diff --git a/src/Components/Aspire.Microsoft.Azure.Cosmos/AspireAzureCosmosDBExtensions.cs b/src/Components/Aspire.Microsoft.Azure.Cosmos/AspireAzureCosmosDBExtensions.cs index ec0ddc08d96..4f6faf3894a 100644 --- a/src/Components/Aspire.Microsoft.Azure.Cosmos/AspireAzureCosmosDBExtensions.cs +++ b/src/Components/Aspire.Microsoft.Azure.Cosmos/AspireAzureCosmosDBExtensions.cs @@ -27,13 +27,13 @@ public static class AspireAzureCosmosDBExtensions /// An optional method that can be used for customizing the . /// Reads the configuration from "Aspire:Microsoft:Azure:Cosmos" section. /// If required ConnectionString is not provided in configuration section - [Obsolete($"This method is obsolete and will be removed in a future version. Use {nameof(AddAzureCosmosDbClient)} instead.")] + [Obsolete($"This method is obsolete and will be removed in a future version. Use {nameof(AddAzureCosmosDBClient)} instead.")] public static void AddAzureCosmosDB( this IHostApplicationBuilder builder, string connectionName, Action? configureSettings = null, Action? configureClientOptions = null) - => AddAzureCosmosDbClient(builder, connectionName, configureSettings, configureClientOptions); + => AddAzureCosmosDBClient(builder, connectionName, configureSettings, configureClientOptions); /// /// Registers as a singleton in the services provided by the . @@ -45,7 +45,7 @@ public static void AddAzureCosmosDB( /// An optional method that can be used for customizing the . /// Reads the configuration from "Aspire:Microsoft:Azure:Cosmos" section. /// If required ConnectionString is not provided in configuration section - public static void AddAzureCosmosDbClient( + public static void AddAzureCosmosDBClient( this IHostApplicationBuilder builder, string connectionName, Action? configureSettings = null, diff --git a/src/Components/Aspire.Microsoft.Azure.Cosmos/README.md b/src/Components/Aspire.Microsoft.Azure.Cosmos/README.md index e2abecef58b..11a88c4cc42 100644 --- a/src/Components/Aspire.Microsoft.Azure.Cosmos/README.md +++ b/src/Components/Aspire.Microsoft.Azure.Cosmos/README.md @@ -44,10 +44,10 @@ The .NET Aspire Azure Cosmos DB library provides multiple options to configure t ### Use a connection string -When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddAzureCosmosDB()`: +When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddAzureCosmosDbClient()`: ```csharp -builder.AddAzureCosmosDB("cosmosConnectionName"); +builder.AddAzureCosmosDbClient("cosmosConnectionName"); ``` And then the connection string will be retrieved from the `ConnectionStrings` configuration section. Two connection formats are supported: @@ -99,13 +99,13 @@ The .NET Aspire Microsoft Azure Cosmos DB library supports [Microsoft.Extensions You can also pass the `Action configureSettings` delegate to set up some or all the options inline, for example to disable tracing from code: ```csharp -builder.AddAzureCosmosDB("cosmosConnectionName", settings => settings.Tracing = false); +builder.AddAzureCosmosDbClient("cosmosConnectionName", settings => settings.Tracing = false); ``` -You can also setup the [CosmosClientOptions](https://learn.microsoft.com/dotnet/api/microsoft.azure.cosmos.cosmosclientoptions) using the optional `Action configureClientOptions` parameter of the `AddAzureCosmosDB` method. For example, to set the `ApplicationName` "User-Agent" header suffix for all requests issues by this client: +You can also setup the [CosmosClientOptions](https://learn.microsoft.com/dotnet/api/microsoft.azure.cosmos.cosmosclientoptions) using the optional `Action configureClientOptions` parameter of the `AddAzureCosmosDbClient` method. For example, to set the `ApplicationName` "User-Agent" header suffix for all requests issues by this client: ```csharp -builder.AddAzureCosmosDB("cosmosConnectionName", configureClientOptions: clientOptions => clientOptions.ApplicationName = "myapp"); +builder.AddAzureCosmosDbClient("cosmosConnectionName", configureClientOptions: clientOptions => clientOptions.ApplicationName = "myapp"); ``` ## AppHost extensions diff --git a/src/Components/Aspire.RabbitMQ.Client/README.md b/src/Components/Aspire.RabbitMQ.Client/README.md index 008e7cd28a3..23841e81794 100644 --- a/src/Components/Aspire.RabbitMQ.Client/README.md +++ b/src/Components/Aspire.RabbitMQ.Client/README.md @@ -18,10 +18,10 @@ dotnet add package Aspire.RabbitMQ.Client ## Usage example -In the _Program.cs_ file of your project, call the `AddRabbitMQ` extension method to register an `IConnection` for use via the dependency injection container. The method takes a connection name parameter. +In the _Program.cs_ file of your project, call the `AddRabbitMQClient` extension method to register an `IConnection` for use via the dependency injection container. The method takes a connection name parameter. ```csharp -builder.AddRabbitMQ("messaging"); +builder.AddRabbitMQClient("messaging"); ``` You can then retrieve the `IConnection` instance using dependency injection. For example, to retrieve the connection from a Web API controller: @@ -80,13 +80,13 @@ The .NET Aspire RabbitMQ component supports [Microsoft.Extensions.Configuration] Also you can pass the `Action configureSettings` delegate to set up some or all the options inline, for example to disable health checks from code: ```csharp -builder.AddRabbitMQ("messaging", settings => settings.HealthChecks = false); +builder.AddRabbitMQClient("messaging", settings => settings.HealthChecks = false); ``` -You can also setup the [ConnectionFactory](https://rabbitmq.github.io/rabbitmq-dotnet-client/api/RabbitMQ.Client.ConnectionFactory.html) using the `Action configureConnectionFactory` delegate parameter of the `AddRabbitMQ` method. For example to set the client provided name for connections: +You can also setup the [ConnectionFactory](https://rabbitmq.github.io/rabbitmq-dotnet-client/api/RabbitMQ.Client.ConnectionFactory.html) using the `Action configureConnectionFactory` delegate parameter of the `AddRabbitMQClient` method. For example to set the client provided name for connections: ```csharp -builder.AddRabbitMQ("messaging", configureConnectionFactory: factory => factory.ClientProvidedName = "MyApp"); +builder.AddRabbitMQClient("messaging", configureConnectionFactory: factory => factory.ClientProvidedName = "MyApp"); ``` ## AppHost extensions diff --git a/src/Components/Aspire.StackExchange.Redis/README.md b/src/Components/Aspire.StackExchange.Redis/README.md index 2f4cd7dbb0d..9525b1354db 100644 --- a/src/Components/Aspire.StackExchange.Redis/README.md +++ b/src/Components/Aspire.StackExchange.Redis/README.md @@ -43,10 +43,10 @@ The .NET Aspire StackExchange Redis component provides multiple options to confi ### Use a connection string -When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddRedis()`: +When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddRedisClient()`: ```csharp -builder.AddRedis("myRedisConnectionName"); +builder.AddRedisClient("myRedisConnectionName"); ``` And then the connection string will be retrieved from the `ConnectionStrings` configuration section: @@ -87,13 +87,13 @@ The Redis component supports [Microsoft.Extensions.Configuration](https://learn. You can also pass the `Action configureSettings` delegate to set up some or all the options inline, for example to disable health checks from code: ```csharp -builder.AddRedis("cache", settings => settings.HealthChecks = false); +builder.AddRedisClient("cache", settings => settings.HealthChecks = false); ``` -You can also setup the [ConfigurationOptions](https://stackexchange.github.io/StackExchange.Redis/Configuration.html#configuration-options) using the `Action configureOptions` delegate parameter of the `AddRedis` method. For example to set the connection timeout: +You can also setup the [ConfigurationOptions](https://stackexchange.github.io/StackExchange.Redis/Configuration.html#configuration-options) using the `Action configureOptions` delegate parameter of the `AddRedisClient` method. For example to set the connection timeout: ```csharp -builder.AddRedis("cache", configureOptions: options => options.ConnectTimeout = 3000); +builder.AddRedisClient("cache", configureOptions: options => options.ConnectTimeout = 3000); ``` ## AppHost extensions diff --git a/tests/Aspire.Microsoft.Azure.Cosmos.Tests/ConformanceTests.cs b/tests/Aspire.Microsoft.Azure.Cosmos.Tests/ConformanceTests.cs index 2282d333688..1b61af90099 100644 --- a/tests/Aspire.Microsoft.Azure.Cosmos.Tests/ConformanceTests.cs +++ b/tests/Aspire.Microsoft.Azure.Cosmos.Tests/ConformanceTests.cs @@ -28,7 +28,7 @@ protected override void RegisterComponent(HostApplicationBuilder builder, Action { if (key is null) { - builder.AddAzureCosmosDbClient("cosmosdb", configure); + builder.AddAzureCosmosDBClient("cosmosdb", configure); } else { diff --git a/tests/testproject/TestProject.IntegrationServiceA/Program.cs b/tests/testproject/TestProject.IntegrationServiceA/Program.cs index cfd24e45583..0f3f2da2442 100644 --- a/tests/testproject/TestProject.IntegrationServiceA/Program.cs +++ b/tests/testproject/TestProject.IntegrationServiceA/Program.cs @@ -66,7 +66,7 @@ if (!resourcesToSkip.Contains(TestResourceNames.cosmos)) { - builder.AddAzureCosmosDbClient("cosmos"); + builder.AddAzureCosmosDBClient("cosmos"); } var app = builder.Build(); From b3b9979a87155555e362c5e0a20d8e37e5deee3b Mon Sep 17 00:00:00 2001 From: Karol Zadora-Przylecki Date: Mon, 11 Mar 2024 16:18:34 -0700 Subject: [PATCH 44/50] Switch to log streaming (#2744) * Use streaming logs for Containers and Executables * Formatting --- src/Aspire.Hosting/Dcp/ApplicationExecutor.cs | 5 +- .../Dcp/DockerContainerLogSource.cs | 104 ----------------- src/Aspire.Hosting/Dcp/FileLogSource.cs | 105 ------------------ src/Aspire.Hosting/Dcp/KubernetesService.cs | 3 + src/Aspire.Hosting/Dcp/Model/Container.cs | 6 + src/Aspire.Hosting/Dcp/Model/Executable.cs | 9 +- src/Aspire.Hosting/Dcp/ResourceLogSource.cs | 5 +- .../Dcp/MockKubernetesService.cs | 2 +- 8 files changed, 22 insertions(+), 217 deletions(-) delete mode 100644 src/Aspire.Hosting/Dcp/DockerContainerLogSource.cs delete mode 100644 src/Aspire.Hosting/Dcp/FileLogSource.cs diff --git a/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs b/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs index f8a11524158..1c9bb6a889a 100644 --- a/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs +++ b/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs @@ -266,9 +266,8 @@ private void StartLogStream(T resource) where T : CustomResource { IAsyncEnumerable>? enumerable = resource switch { - Container c when c.Status?.ContainerId is not null => new DockerContainerLogSource(c.Status.ContainerId), - Executable e when e.Status?.StdOutFile is not null && e.Status?.StdErrFile is not null => new FileLogSource(e.Status.StdOutFile, e.Status.StdErrFile), - // Container or Executable => new ResourceLogSource(_logger, kubernetesService, resource), + Container c when c.LogsAvailable => new ResourceLogSource(_logger, kubernetesService, resource), + Executable e when e.LogsAvailable => new ResourceLogSource(_logger, kubernetesService, resource), _ => null }; diff --git a/src/Aspire.Hosting/Dcp/DockerContainerLogSource.cs b/src/Aspire.Hosting/Dcp/DockerContainerLogSource.cs deleted file mode 100644 index 409c9f34fbc..00000000000 --- a/src/Aspire.Hosting/Dcp/DockerContainerLogSource.cs +++ /dev/null @@ -1,104 +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.Threading.Channels; -using Aspire.Hosting.Dcp.Process; -using Aspire.Hosting.Utils; - -namespace Aspire.Hosting.Dcp; - -internal sealed class DockerContainerLogSource(string containerId) : IAsyncEnumerable> -{ - public async IAsyncEnumerator> GetAsyncEnumerator(CancellationToken cancellationToken) - { - if (!cancellationToken.CanBeCanceled) - { - throw new ArgumentException("Cancellation token must be cancellable in order to prevent leaking resources.", nameof(cancellationToken)); - } - - Task? processResultTask = null; - IAsyncDisposable? processDisposable = null; - - var channel = Channel.CreateUnbounded<(string Content, bool IsErrorMessage)>( - new UnboundedChannelOptions { AllowSynchronousContinuations = false, SingleReader = true, SingleWriter = false }); - - try - { - var spec = new ProcessSpec(FileUtil.FindFullPathFromPath("docker")) - { - Arguments = $"logs --follow -t {containerId}", - OnOutputData = OnOutputData, - OnErrorData = OnErrorData, - KillEntireProcessTree = false, - // We don't want this to throw an exception because it is common for - // us to cancel the task and kill the process, which returns -1. - ThrowOnNonZeroReturnCode = false - }; - - (processResultTask, processDisposable) = ProcessUtil.Run(spec); - - var tcs = new TaskCompletionSource(); - - // Make sure the process exits if the cancellation token is cancelled - var ctr = cancellationToken.Register(() => tcs.TrySetResult()); - - // Don't forward cancellationToken here, because it's handled internally in WaitForExit - _ = Task.Run(() => WaitForExit(tcs, ctr), CancellationToken.None); - - await foreach (var batch in channel.GetBatchesAsync(cancellationToken: cancellationToken)) - { - yield return batch; - } - } - finally - { - await DisposeProcess().ConfigureAwait(false); - } - - yield break; - - void OnOutputData(string line) - { - channel.Writer.TryWrite((Content: line, IsErrorMessage: false)); - } - - void OnErrorData(string line) - { - channel.Writer.TryWrite((Content: line, IsErrorMessage: true)); - } - - async Task WaitForExit(TaskCompletionSource tcs, CancellationTokenRegistration ctr) - { - if (processResultTask is not null) - { - // Wait for cancellation (tcs.Task) or for the process itself to exit. - await Task.WhenAny(tcs.Task, processResultTask).ConfigureAwait(false); - - if (processResultTask.IsCompleted) - { - // If it was the process that exited, write that out to the logs. - // If it was cancelled, there's no need to because the user has left the page - var processResult = processResultTask.Result; - await channel.Writer.WriteAsync(($"Process exited with code {processResult.ExitCode}", false), cancellationToken).ConfigureAwait(false); - } - - channel.Writer.Complete(); - - // If the process has already exited, this will be a no-op. But if it was cancelled - // we need to end the process - await DisposeProcess().ConfigureAwait(false); - } - - ctr.Unregister(); - } - - async ValueTask DisposeProcess() - { - if (processDisposable is not null) - { - await processDisposable.DisposeAsync().ConfigureAwait(false); - processDisposable = null; - } - } - } -} diff --git a/src/Aspire.Hosting/Dcp/FileLogSource.cs b/src/Aspire.Hosting/Dcp/FileLogSource.cs deleted file mode 100644 index 691636d7840..00000000000 --- a/src/Aspire.Hosting/Dcp/FileLogSource.cs +++ /dev/null @@ -1,105 +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.Buffers; -using System.IO.Pipelines; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Channels; - -namespace Aspire.Hosting.Dcp; - -internal sealed partial class FileLogSource(string stdOutPath, string stdErrPath) : IAsyncEnumerable> -{ - private static readonly StreamPipeReaderOptions s_streamPipeReaderOptions = new(leaveOpen: true); - private static readonly Regex s_lineSplitRegex = GenerateLineSplitRegex(); - - public async IAsyncEnumerator> GetAsyncEnumerator(CancellationToken cancellationToken) - { - if (!cancellationToken.CanBeCanceled) - { - throw new ArgumentException("Cancellation token must be cancellable in order to prevent leaking resources.", nameof(cancellationToken)); - } - - var channel = Channel.CreateUnbounded<(string Content, bool IsErrorMessage)>( - new UnboundedChannelOptions { AllowSynchronousContinuations = false, SingleReader = true, SingleWriter = false }); - - var stdOut = Task.Run(() => WatchFileAsync(stdOutPath, isError: false), cancellationToken); - var stdErr = Task.Run(() => WatchFileAsync(stdErrPath, isError: true), cancellationToken); - - await foreach (var batch in channel.GetBatchesAsync(cancellationToken: cancellationToken)) - { - yield return batch; - } - - async Task WatchFileAsync(string filePath, bool isError) - { - var fileStream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - - // Close the file stream when the cancellation token fires. - // It's important that callers cancel when no longer needed. - using var _ = fileStream; - - var partialLine = ""; - - // The FileStream will stay open and continue growing as data is written to it - // but the PipeReader will close as soon as it reaches the end of the FileStream. - // So we need to keep re-creating it. It will read from the last position - while (!cancellationToken.IsCancellationRequested) - { - var reader = PipeReader.Create(fileStream, s_streamPipeReaderOptions); - - while (!cancellationToken.IsCancellationRequested) - { - var result = await reader.ReadAsync(cancellationToken).ConfigureAwait(false); - - if (result.IsCompleted) - { - // There's no more data in the file. Because we are polling, we will loop - // around again and land back here almost immediately. We introduce a small - // sleep here in order to not burn CPU while polling. This sleep won't limit - // the rate at which we can consume file changes when many exist, as the sleep - // only occurs when we have caught up. - // - // Longer term we hope to have a log streaming API from DCP for this. - // https://github.com/dotnet/aspire/issues/760 - await Task.Delay(100, cancellationToken).ConfigureAwait(false); - - // We're done here. Loop around and wait for a signal that the file has changed. - break; - } - - var str = Encoding.UTF8.GetString(result.Buffer); - - // It's possible that we don't read an entire log line at the end of the data we're reading. - // If that's the case, we'll wait for the next iteration, grab the rest and concatenate them. - var lines = s_lineSplitRegex.Split(str); - var isLastLineComplete = str[^1] is '\r' or '\n' && lines[^1] is not { Length: 0 }; - lines[0] = partialLine + lines[0]; - partialLine = isLastLineComplete ? "" : lines[^1]; - - reader.AdvanceTo(GetEndPosition(result.Buffer)); - - var count = isLastLineComplete ? lines.Length : lines.Length - 1; - - for (var i = 0; i < count; i++) - { - channel.Writer.TryWrite((lines[i], isError)); - } - } - - reader.Complete(); - } - - static SequencePosition GetEndPosition(in ReadOnlySequence buffer) - { - var sequenceReader = new SequenceReader(buffer); - sequenceReader.AdvanceToEnd(); - return sequenceReader.Position; - } - } - } - - [GeneratedRegex("""\r\n|\r|\n""", RegexOptions.CultureInvariant)] - private static partial Regex GenerateLineSplitRegex(); -} diff --git a/src/Aspire.Hosting/Dcp/KubernetesService.cs b/src/Aspire.Hosting/Dcp/KubernetesService.cs index 6132a4eca47..c96b761fcc9 100644 --- a/src/Aspire.Hosting/Dcp/KubernetesService.cs +++ b/src/Aspire.Hosting/Dcp/KubernetesService.cs @@ -35,6 +35,7 @@ Task GetLogStreamAsync( T obj, string logStreamType, bool? follow = true, + bool? timestamps = false, CancellationToken cancellationToken = default) where T : CustomResource; } @@ -176,12 +177,14 @@ public Task GetLogStreamAsync( T obj, string logStreamType, bool? follow = true, + bool? timestamps = false, CancellationToken cancellationToken = default) where T : CustomResource { var resourceType = GetResourceFor(); ImmutableArray<(string name, string value)>? queryParams = [ (name: "follow", value: follow == true ? "true": "false"), + (name: "timestamps", value: timestamps == true ? "true" : "false"), (name: "source", value: logStreamType) ]; diff --git a/src/Aspire.Hosting/Dcp/Model/Container.cs b/src/Aspire.Hosting/Dcp/Model/Container.cs index 01b2802f851..e75a84b43ef 100644 --- a/src/Aspire.Hosting/Dcp/Model/Container.cs +++ b/src/Aspire.Hosting/Dcp/Model/Container.cs @@ -237,5 +237,11 @@ public static Container Create(string name, string image) return c; } + + public bool LogsAvailable => + this.Status?.State == ContainerState.Running + || this.Status?.State == ContainerState.Paused + || this.Status?.State == ContainerState.Exited + || (this.Status?.State == ContainerState.FailedToStart && this.Status?.ContainerId is not null); } diff --git a/src/Aspire.Hosting/Dcp/Model/Executable.cs b/src/Aspire.Hosting/Dcp/Model/Executable.cs index 126b6ef8d7c..28e29cf9b94 100644 --- a/src/Aspire.Hosting/Dcp/Model/Executable.cs +++ b/src/Aspire.Hosting/Dcp/Model/Executable.cs @@ -56,7 +56,7 @@ internal sealed class ExecutableStatus : V1Status // The current state of the process/IDE session started for this executable [JsonPropertyName("state")] - public string? State { get; set; } = ExecutableStates.Unknown; + public string? State { get; set; } = ExecutableState.Unknown; // Start (attempt) timestamp. [JsonPropertyName("startupTimestamp")] @@ -88,7 +88,7 @@ internal sealed class ExecutableStatus : V1Status public List? EffectiveArgs { get; set; } } -internal static class ExecutableStates +internal static class ExecutableState { // Executable was successfully started and was running last time we checked. public const string Running = "Running"; @@ -134,4 +134,9 @@ public static Executable Create(string name, string executablePath) return exe; } + + public bool LogsAvailable => + this.Status?.State == ExecutableState.Running + || this.Status?.State == ExecutableState.Finished + || this.Status?.State == ExecutableState.Terminated; } diff --git a/src/Aspire.Hosting/Dcp/ResourceLogSource.cs b/src/Aspire.Hosting/Dcp/ResourceLogSource.cs index 6a1e5f4f1c2..be8af996856 100644 --- a/src/Aspire.Hosting/Dcp/ResourceLogSource.cs +++ b/src/Aspire.Hosting/Dcp/ResourceLogSource.cs @@ -24,8 +24,9 @@ public async IAsyncEnumerator GetAsyncEnumerator(CancellationToken throw new ArgumentException("Cancellation token must be cancellable in order to prevent leaking resources.", nameof(cancellationToken)); } - var stdoutStream = await kubernetesService.GetLogStreamAsync(resource, Logs.StreamTypeStdOut, follow: true, cancellationToken).ConfigureAwait(false); - var stderrStream = await kubernetesService.GetLogStreamAsync(resource, Logs.StreamTypeStdErr, follow: true, cancellationToken).ConfigureAwait(false); + var timestamps = resource is Container; // Timestamps are available only for Containers as of Aspire P5. + var stdoutStream = await kubernetesService.GetLogStreamAsync(resource, Logs.StreamTypeStdOut, follow: true, timestamps: timestamps, cancellationToken).ConfigureAwait(false); + var stderrStream = await kubernetesService.GetLogStreamAsync(resource, Logs.StreamTypeStdErr, follow: true, timestamps: timestamps, cancellationToken).ConfigureAwait(false); var channel = Channel.CreateUnbounded(new UnboundedChannelOptions { diff --git a/tests/Aspire.Hosting.Tests/Dcp/MockKubernetesService.cs b/tests/Aspire.Hosting.Tests/Dcp/MockKubernetesService.cs index 3035393a805..376868424f9 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/MockKubernetesService.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/MockKubernetesService.cs @@ -34,7 +34,7 @@ public Task> ListAsync(string? namespaceParameter = null, Cancellatio throw new NotImplementedException(); } - public Task GetLogStreamAsync(T obj, string logStreamType, bool? follow = true, CancellationToken cancellationToken = default) where T : CustomResource + public Task GetLogStreamAsync(T obj, string logStreamType, bool? follow = true, bool? timestamps = false, CancellationToken cancellationToken = default) where T : CustomResource { throw new NotImplementedException(); } From a04135eceb77d1a21f947328c208a169635d950b Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Mon, 11 Mar 2024 19:02:19 -0500 Subject: [PATCH 45/50] Rename AddKeyVaultSecrets to match other extension methods naming (#2786) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Rename AddKeyVaultSecrets to match other extension methods naming The other extension methods were renamed to have "Azure" in them. This method should be named the same. * Update README --------- Co-authored-by: Sébastien Ros --- .../AspireKeyVaultExtensions.cs | 17 +++++++++++++++++ .../Aspire.Azure.Security.KeyVault/README.md | 4 ++-- .../AspireKeyVaultExtensionsTests.cs | 2 +- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs b/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs index 879080d494d..826b6d86edf 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs @@ -109,12 +109,29 @@ public static void AddKeyedAzureKeyVaultClient( /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration. /// An optional method that can be used for customizing the . /// An optional instance to configure the behavior of the configuration provider. + [Obsolete($"This method is obsolete and will be removed in a future version. Use {nameof(AddAzureKeyVaultSecrets)} instead.")] public static void AddKeyVaultSecrets( this IConfigurationManager configurationManager, string connectionName, Action? configureSettings = null, Action? configureClientOptions = null, AzureKeyVaultConfigurationOptions? options = null) + => AddAzureKeyVaultSecrets(configurationManager, connectionName, configureSettings, configureClientOptions, options); + + /// + /// Adds the Azure KeyVault secrets to be configuration values in the . + /// + /// The to add the secrets to. + /// A name used to retrieve the connection string from the ConnectionStrings configuration section. + /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration. + /// An optional method that can be used for customizing the . + /// An optional instance to configure the behavior of the configuration provider. + public static void AddAzureKeyVaultSecrets( + this IConfigurationManager configurationManager, + string connectionName, + Action? configureSettings = null, + Action? configureClientOptions = null, + AzureKeyVaultConfigurationOptions? options = null) { ArgumentNullException.ThrowIfNull(configurationManager); ArgumentException.ThrowIfNullOrEmpty(connectionName); diff --git a/src/Components/Aspire.Azure.Security.KeyVault/README.md b/src/Components/Aspire.Azure.Security.KeyVault/README.md index 50b2c383d78..2410c24019c 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/README.md +++ b/src/Components/Aspire.Azure.Security.KeyVault/README.md @@ -21,10 +21,10 @@ dotnet add package Aspire.Azure.Security.KeyVault ### Add secrets to configuration -In the _Program.cs_ file of your project, call the `builder.Configuration.AddKeyVaultSecrets` extension method to add the secrets in the Azure Key Vault to the application's Configuration. The method takes a connection name parameter. +In the _Program.cs_ file of your project, call the `builder.Configuration.AddAzureKeyVaultSecrets` extension method to add the secrets in the Azure Key Vault to the application's Configuration. The method takes a connection name parameter. ```csharp -builder.Configuration.AddKeyVaultSecrets("secrets"); +builder.Configuration.AddAzureKeyVaultSecrets("secrets"); ``` You can then retrieve a secret through normal `IConfiguration` APIs. For example, to retrieve a secret from a Web API controller: diff --git a/tests/Aspire.Azure.Security.KeyVault.Tests/AspireKeyVaultExtensionsTests.cs b/tests/Aspire.Azure.Security.KeyVault.Tests/AspireKeyVaultExtensionsTests.cs index 4521254d255..024c3fb3095 100644 --- a/tests/Aspire.Azure.Security.KeyVault.Tests/AspireKeyVaultExtensionsTests.cs +++ b/tests/Aspire.Azure.Security.KeyVault.Tests/AspireKeyVaultExtensionsTests.cs @@ -81,7 +81,7 @@ public void AddsKeyVaultSecretsToConfig() new KeyValuePair("ConnectionStrings:secrets", ConformanceTests.VaultUri) ]); - builder.Configuration.AddKeyVaultSecrets("secrets", configureClientOptions: o => + builder.Configuration.AddAzureKeyVaultSecrets("secrets", configureClientOptions: o => { o.Transport = new MockTransport( CreateResponse(""" From c5acb6c20e725aeed98d5f74fc577a8e12e8dfe2 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Mon, 11 Mar 2024 18:41:20 -0700 Subject: [PATCH 46/50] Emit bind mounts in manifest (#2782) * Emit bind mounts in manifest Fixes #2761 --- .../Publishing/ManifestPublishingContext.cs | 100 ++++++++++++------ .../ManifestGenerationTests.cs | 54 +++++++++- 2 files changed, 116 insertions(+), 38 deletions(-) diff --git a/src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs b/src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs index dbf24d981e4..c4d0ee3d63e 100644 --- a/src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs +++ b/src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs @@ -52,7 +52,9 @@ public sealed class ManifestPublishingContext(DistributedApplicationExecutionCon var fullyQualifiedManifestPath = Path.GetFullPath(ManifestPath); var manifestDirectory = Path.GetDirectoryName(fullyQualifiedManifestPath) ?? throw new DistributedApplicationException("Could not get directory name of output path"); - var relativePath = Path.GetRelativePath(manifestDirectory, path); + + var normalizedPath = path.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar); + var relativePath = Path.GetRelativePath(manifestDirectory, normalizedPath); return relativePath.Replace('\\', '/'); } @@ -105,39 +107,8 @@ public async Task WriteContainerAsync(ContainerResource container) } } - // Write volume details - if (container.TryGetAnnotationsOfType(out var mounts)) - { - var volumes = mounts.Where(mounts => mounts.Type == ContainerMountType.Named).ToList(); - - // Only write out details for volumes (no bind mounts) - if (volumes.Count > 0) - { - // Volumes are written as an array of objects as anonymous volumes do not have a name - Writer.WriteStartArray("volumes"); - - foreach (var volume in volumes) - { - Writer.WriteStartObject(); - - // This can be null for anonymous volumes - if (volume.Source is not null) - { - Writer.WritePropertyName("name"); - Writer.WriteStringValue(volume.Source); - } - - Writer.WritePropertyName("target"); - Writer.WriteStringValue(volume.Target); - - Writer.WriteBoolean("readOnly", volume.IsReadOnly); - - Writer.WriteEndObject(); - } - - Writer.WriteEndArray(); - } - } + // Write volume & bind mount details + WriteContainerMounts(container); await WriteEnvironmentVariablesAsync(container).ConfigureAwait(false); WriteBindings(container, emitContainerPort: true); @@ -297,4 +268,65 @@ internal void WriteManifestMetadata(IResource resource) Writer.WriteEndObject(); } + + private void WriteContainerMounts(ContainerResource container) + { + if (container.TryGetAnnotationsOfType(out var mounts)) + { + // Write out details for bind mounts + var bindMounts = mounts.Where(mounts => mounts.Type == ContainerMountType.Bind).ToList(); + if (bindMounts.Count > 0) + { + // Bind mounts are written as an array of objects to be consistent with volumes + Writer.WriteStartArray("bindMounts"); + + foreach (var bindMount in bindMounts) + { + Writer.WriteStartObject(); + + Writer.WritePropertyName("source"); + var manifestRelativeSource = GetManifestRelativePath(bindMount.Source); + Writer.WriteStringValue(manifestRelativeSource); + + Writer.WritePropertyName("target"); + Writer.WriteStringValue(bindMount.Target.Replace('\\', '/')); + + Writer.WriteBoolean("readOnly", bindMount.IsReadOnly); + + Writer.WriteEndObject(); + } + + Writer.WriteEndArray(); + } + + // Write out details for volumes + var volumes = mounts.Where(mounts => mounts.Type == ContainerMountType.Named).ToList(); + if (volumes.Count > 0) + { + // Volumes are written as an array of objects as anonymous volumes do not have a name + Writer.WriteStartArray("volumes"); + + foreach (var volume in volumes) + { + Writer.WriteStartObject(); + + // This can be null for anonymous volumes + if (volume.Source is not null) + { + Writer.WritePropertyName("name"); + Writer.WriteStringValue(volume.Source); + } + + Writer.WritePropertyName("target"); + Writer.WriteStringValue(volume.Target); + + Writer.WriteBoolean("readOnly", volume.IsReadOnly); + + Writer.WriteEndObject(); + } + + Writer.WriteEndArray(); + } + } + } } diff --git a/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs b/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs index 801806d9a41..4683c01e774 100644 --- a/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs +++ b/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs @@ -189,10 +189,9 @@ public async Task EnsureContainerWithVolumesEmitsVolumes() using var program = CreateTestProgramJsonDocumentManifestPublisher(); var container = program.AppBuilder.AddContainer("containerwithvolumes", "image/name") - .WithVolume("myvolume", "/mount/here") - .WithBindMount("./some/source", "/bound") // This should be ignored and not written to the manifest - .WithVolume("myreadonlyvolume", "/mount/there", isReadOnly: true) - .WithVolume(null! /* anonymous volume */, "/mount/everywhere"); + .WithVolume("myvolume", "/mount/here") + .WithVolume("myreadonlyvolume", "/mount/there", isReadOnly: true) + .WithVolume(null! /* anonymous volume */, "/mount/everywhere"); program.Build(); @@ -224,6 +223,53 @@ public async Task EnsureContainerWithVolumesEmitsVolumes() Assert.Equal(expectedManifest, manifest.ToString()); } + [Fact] + public async Task EnsureContainerWithBindMountsEmitsBindMounts() + { + using var program = CreateTestProgramJsonDocumentManifestPublisher(); + + var container = program.AppBuilder.AddContainer("containerwithbindmounts", "image/name") + .WithBindMount("./some/source", "/bound") + .WithBindMount("not/relative/qualified", "/another/place") + .WithBindMount(".\\some\\other\\source", "\\mount\\here") + .WithBindMount("./some/file/path.txt", "/mount/there.txt", isReadOnly: true); + + program.Build(); + + var manifest = await ManifestUtils.GetManifest(container.Resource); + + var expectedManifest = """ + { + "type": "container.v0", + "image": "image/name:latest", + "bindMounts": [ + { + "source": "net8.0/some/source", + "target": "/bound", + "readOnly": false + }, + { + "source": "net8.0/not/relative/qualified", + "target": "/another/place", + "readOnly": false + }, + { + "source": "net8.0/some/other/source", + "target": "/mount/here", + "readOnly": false + }, + { + "source": "net8.0/some/file/path.txt", + "target": "/mount/there.txt", + "readOnly": true + } + ] + } + """; + + Assert.Equal(expectedManifest, manifest.ToString()); + } + [Theory] [InlineData(new string[] { "args1", "args2" }, new string[] { "withArgs1", "withArgs2" })] [InlineData(new string[] { }, new string[] { "withArgs1", "withArgs2" })] From fae6d229e0fabac0fb8bcc286007188ee800b1bc Mon Sep 17 00:00:00 2001 From: David Fowler Date: Mon, 11 Mar 2024 20:22:08 -0700 Subject: [PATCH 47/50] Bring environment and arguments to parity (#2797) * Bring environment and arguments to parity - Added IResourceWithArgs, and moved all of the WithArgs extension methods to it. - Allow objects in command line arguments. - Added tests * Remove multiple ways of setting args for executables - Removed args array from ExecutableResource and use WithArgs * Fix test --- ...DaprDistributedApplicationLifecycleHook.cs | 4 +- .../CommandLineArgsCallbackAnnotation.cs | 6 +- .../ApplicationModel/ContainerResource.cs | 2 +- .../ApplicationModel/ExecutableResource.cs | 8 +-- .../ApplicationModel/IResourceWithArgs.cs | 11 +++ src/Aspire.Hosting/Dcp/ApplicationExecutor.cs | 60 ++++++++++++++-- .../ContainerResourceBuilderExtensions.cs | 16 ----- .../ExecutableResourceBuilderExtensions.cs | 27 +++---- .../Extensions/ResourceBuilderExtensions.cs | 40 +++++++++++ src/Aspire.Hosting/Node/NodeAppResource.cs | 5 +- src/Aspire.Hosting/Node/NodeExtensions.cs | 10 +-- .../Publishing/ManifestPublisher.cs | 23 +----- .../Publishing/ManifestPublishingContext.cs | 62 ++++++++++------ .../ContainerResourceTests.cs | 59 +++++++++++++++ .../ExecutableResourceTests.cs | 71 +++++++++++++++++++ .../ManifestGenerationTests.cs | 3 +- .../Aspire.Hosting.Tests/Nats/AddNatsTests.cs | 2 +- .../Utils/ArgumentEvaluator.cs | 41 +++++++++++ 18 files changed, 344 insertions(+), 106 deletions(-) create mode 100644 src/Aspire.Hosting/ApplicationModel/IResourceWithArgs.cs create mode 100644 tests/Aspire.Hosting.Tests/ExecutableResourceTests.cs create mode 100644 tests/Aspire.Hosting.Tests/Utils/ArgumentEvaluator.cs diff --git a/src/Aspire.Hosting.Dapr/DaprDistributedApplicationLifecycleHook.cs b/src/Aspire.Hosting.Dapr/DaprDistributedApplicationLifecycleHook.cs index 425b2a41673..90d76bfef60 100644 --- a/src/Aspire.Hosting.Dapr/DaprDistributedApplicationLifecycleHook.cs +++ b/src/Aspire.Hosting.Dapr/DaprDistributedApplicationLifecycleHook.cs @@ -133,7 +133,7 @@ public async Task BeforeStartAsync(DistributedApplicationModel appModel, Cancell PostOptionsArgs(Args(sidecarOptions?.Command))); var daprCliResourceName = $"{daprSidecar.Name}-cli"; - var daprCli = new ExecutableResource(daprCliResourceName, fileName, appHostDirectory, daprCommandLine.Arguments.ToArray()); + var daprCli = new ExecutableResource(daprCliResourceName, fileName, appHostDirectory); resource.Annotations.Add( new EnvironmentCallbackAnnotation( @@ -183,6 +183,8 @@ public async Task BeforeStartAsync(DistributedApplicationModel appModel, Cancell new CommandLineArgsCallbackAnnotation( updatedArgs => { + updatedArgs.AddRange(daprCommandLine.Arguments); + EndpointReference? httpEndPoint = null; if (resource is IResourceWithEndpoints resourceWithEndpoints) { diff --git a/src/Aspire.Hosting/ApplicationModel/CommandLineArgsCallbackAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/CommandLineArgsCallbackAnnotation.cs index db7bbc2ae07..7d77a0827ff 100644 --- a/src/Aspire.Hosting/ApplicationModel/CommandLineArgsCallbackAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/CommandLineArgsCallbackAnnotation.cs @@ -21,7 +21,7 @@ public CommandLineArgsCallbackAnnotation(Func class with the specified callback action. /// /// The callback action to be executed. - public CommandLineArgsCallbackAnnotation(Action> callback) + public CommandLineArgsCallbackAnnotation(Action> callback) { Callback = (c) => { @@ -41,12 +41,12 @@ public CommandLineArgsCallbackAnnotation(Action> callback) /// /// The list of command-line arguments. /// The cancellation token associated with this execution. -public sealed class CommandLineArgsCallbackContext(IList args, CancellationToken cancellationToken = default) +public sealed class CommandLineArgsCallbackContext(IList args, CancellationToken cancellationToken = default) { /// /// Gets the list of command-line arguments. /// - public IList Args { get; } = args; + public IList Args { get; } = args; /// /// Gets the cancellation token associated with the callback context. diff --git a/src/Aspire.Hosting/ApplicationModel/ContainerResource.cs b/src/Aspire.Hosting/ApplicationModel/ContainerResource.cs index 79ff2569530..a58399637c3 100644 --- a/src/Aspire.Hosting/ApplicationModel/ContainerResource.cs +++ b/src/Aspire.Hosting/ApplicationModel/ContainerResource.cs @@ -8,7 +8,7 @@ namespace Aspire.Hosting.ApplicationModel; /// /// The name of the resource. /// An optional container entrypoint. -public class ContainerResource(string name, string? entrypoint = null) : Resource(name), IResourceWithEnvironment, IResourceWithEndpoints +public class ContainerResource(string name, string? entrypoint = null) : Resource(name), IResourceWithEnvironment, IResourceWithArgs, IResourceWithEndpoints { /// /// The container Entrypoint. diff --git a/src/Aspire.Hosting/ApplicationModel/ExecutableResource.cs b/src/Aspire.Hosting/ApplicationModel/ExecutableResource.cs index 3e3170c28c1..1db593f64bf 100644 --- a/src/Aspire.Hosting/ApplicationModel/ExecutableResource.cs +++ b/src/Aspire.Hosting/ApplicationModel/ExecutableResource.cs @@ -9,8 +9,7 @@ namespace Aspire.Hosting.ApplicationModel; /// The name of the resource. /// The command to execute. /// The working directory of the executable. -/// The arguments to pass to the executable. -public class ExecutableResource(string name, string command, string workingDirectory, string[]? args) : Resource(name), IResourceWithEnvironment, IResourceWithEndpoints +public class ExecutableResource(string name, string command, string workingDirectory) : Resource(name), IResourceWithEnvironment, IResourceWithArgs, IResourceWithEndpoints { /// /// Gets the command associated with this executable resource. @@ -21,9 +20,4 @@ public class ExecutableResource(string name, string command, string workingDirec /// Gets the working directory for the executable resource. /// public string WorkingDirectory { get; } = workingDirectory; - - /// - /// Gets the command line arguments passed to the executable resource. - /// - public string[]? Args { get; } = args; } diff --git a/src/Aspire.Hosting/ApplicationModel/IResourceWithArgs.cs b/src/Aspire.Hosting/ApplicationModel/IResourceWithArgs.cs new file mode 100644 index 00000000000..6df6fb63796 --- /dev/null +++ b/src/Aspire.Hosting/ApplicationModel/IResourceWithArgs.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// Represents a resource that is associated with commandline arguments. +/// +public interface IResourceWithArgs : IResource +{ +} diff --git a/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs b/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs index 1c9bb6a889a..85a4b3fe9a3 100644 --- a/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs +++ b/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs @@ -883,7 +883,6 @@ private void PreparePlainExecutables() // The working directory is always relative to the app host project directory (if it exists). exe.Spec.WorkingDirectory = executable.WorkingDirectory; - exe.Spec.Args = executable.Args?.ToList(); exe.Spec.ExecutionType = ExecutionType.Process; exe.Annotate(Executable.OtelServiceNameAnnotation, exe.Metadata.Name); exe.Annotate(Executable.ResourceNameAnnotation, executable.Name); @@ -1062,12 +1061,29 @@ private async Task CreateExecutableAsync(AppResource er, ILogger resourceLogger, if (er.ModelResource.TryGetAnnotationsOfType(out var exeArgsCallbacks)) { - var commandLineContext = new CommandLineArgsCallbackContext(spec.Args, cancellationToken); + var args = new List(); + var commandLineContext = new CommandLineArgsCallbackContext(args, cancellationToken); foreach (var exeArgsCallback in exeArgsCallbacks) { await exeArgsCallback.Callback(commandLineContext).ConfigureAwait(false); } + + foreach (var arg in args) + { + var value = arg switch + { + string s => s, + IValueProvider valueProvider => await GetValue(key: null, valueProvider, resourceLogger, isContainer: false, cancellationToken).ConfigureAwait(false), + null => null, + _ => throw new InvalidOperationException($"Unexpected value for {arg}") + }; + + if (value is not null) + { + spec.Args.Add(value); + } + } } var config = new Dictionary(); @@ -1145,7 +1161,7 @@ private async Task CreateExecutableAsync(AppResource er, ILogger resourceLogger, } } - private async Task GetValue(string key, IValueProvider valueProvider, ILogger logger, bool isContainer, CancellationToken cancellationToken) + private async Task GetValue(string? key, IValueProvider valueProvider, ILogger logger, bool isContainer, CancellationToken cancellationToken) { var task = valueProvider.GetValueAsync(cancellationToken); @@ -1153,7 +1169,14 @@ private async Task CreateExecutableAsync(AppResource er, ILogger resourceLogger, { if (valueProvider is IResource resource) { - logger.LogInformation("Waiting for value for environment variable value '{Name}' from resource '{ResourceName}'", key, resource.Name); + if (key is null) + { + logger.LogInformation("Waiting for value from resource '{ResourceName}'", resource.Name); + } + else + { + logger.LogInformation("Waiting for value for environment variable value '{Name}' from resource '{ResourceName}'", key, resource.Name); + } } else if (valueProvider is ConnectionStringReference { Resource: var cs }) { @@ -1161,7 +1184,14 @@ private async Task CreateExecutableAsync(AppResource er, ILogger resourceLogger, } else { - logger.LogInformation("Waiting for value for environment variable value '{Name}' from {ValueProvider}.", key, valueProvider.ToString()); + if (key is null) + { + logger.LogInformation("Waiting for value from {ValueProvider}.", valueProvider.ToString()); + } + else + { + logger.LogInformation("Waiting for value for environment variable value '{Name}' from {ValueProvider}.", key, valueProvider.ToString()); + } } } @@ -1427,12 +1457,30 @@ private async Task CreateContainerAsync(AppResource cr, ILogger resourceLogger, { dcpContainerResource.Spec.Args ??= []; - var commandLineArgsContext = new CommandLineArgsCallbackContext(dcpContainerResource.Spec.Args, cancellationToken); + var args = new List(); + + var commandLineArgsContext = new CommandLineArgsCallbackContext(args, cancellationToken); foreach (var callback in argsCallback) { await callback.Callback(commandLineArgsContext).ConfigureAwait(false); } + + foreach (var arg in args) + { + var value = arg switch + { + string s => s, + IValueProvider valueProvider => await GetValue(key: null, valueProvider, resourceLogger, isContainer: true, cancellationToken).ConfigureAwait(false), + null => null, + _ => throw new InvalidOperationException($"Unexpected value for {arg}") + }; + + if (value is not null) + { + dcpContainerResource.Spec.Args.Add(value); + } + } } if (modelContainerResource is ContainerResource containerResource) diff --git a/src/Aspire.Hosting/Extensions/ContainerResourceBuilderExtensions.cs b/src/Aspire.Hosting/Extensions/ContainerResourceBuilderExtensions.cs index df82e3bb89b..4d989132348 100644 --- a/src/Aspire.Hosting/Extensions/ContainerResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/Extensions/ContainerResourceBuilderExtensions.cs @@ -67,22 +67,6 @@ public static IResourceBuilder WithBindMount(this IResourceBuilder buil return builder.WithAnnotation(annotation); } - /// - /// Adds the arguments to be passed to a container resource when the container is started. - /// - /// The resource type. - /// The resource builder. - /// The arguments to be passed to the container when it is started. - /// The . - public static IResourceBuilder WithArgs(this IResourceBuilder builder, params string[] args) where T : ContainerResource - { - var annotation = new CommandLineArgsCallbackAnnotation(updatedArgs => - { - updatedArgs.AddRange(args); - }); - return builder.WithAnnotation(annotation); - } - /// /// Sets the Entrypoint for the container. /// diff --git a/src/Aspire.Hosting/Extensions/ExecutableResourceBuilderExtensions.cs b/src/Aspire.Hosting/Extensions/ExecutableResourceBuilderExtensions.cs index 194cf98f4f1..9785c4dd783 100644 --- a/src/Aspire.Hosting/Extensions/ExecutableResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/Extensions/ExecutableResourceBuilderExtensions.cs @@ -25,8 +25,15 @@ public static IResourceBuilder AddExecutable(this IDistribut { workingDirectory = PathNormalizer.NormalizePathForCurrentPlatform(Path.Combine(builder.AppHostDirectory, workingDirectory)); - var executable = new ExecutableResource(name, command, workingDirectory, args); - return builder.AddResource(executable); + var executable = new ExecutableResource(name, command, workingDirectory); + return builder.AddResource(executable) + .WithArgs(context => + { + if (args is not null) + { + context.Args.AddRange(args); + } + }); } /// @@ -52,22 +59,6 @@ public static IResourceBuilder PublishAsDockerFile(this IResourceBuilder WriteExecutableAsDockerfileResourceAsync(context, builder.Resource)); } - /// - /// Adds the arguments to be passed to an executable resource when the executable is started. - /// - /// The resource type. - /// The resource builder. - /// The arguments to be passed to the executable when it is started. - /// The . - public static IResourceBuilder WithArgs(this IResourceBuilder builder, params string[] args) where T : ExecutableResource - { - var annotation = new CommandLineArgsCallbackAnnotation(updatedArgs => - { - updatedArgs.AddRange(args); - }); - return builder.WithAnnotation(annotation); - } - private static async Task WriteExecutableAsDockerfileResourceAsync(ManifestPublishingContext context, ExecutableResource executable) { context.Writer.WriteString("type", "dockerfile.v0"); diff --git a/src/Aspire.Hosting/Extensions/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/Extensions/ResourceBuilderExtensions.cs index b2001bd87a6..4ff4dfc2153 100644 --- a/src/Aspire.Hosting/Extensions/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/Extensions/ResourceBuilderExtensions.cs @@ -115,6 +115,46 @@ public static IResourceBuilder WithEnvironment(this IResourceBuilder bu }); } + /// + /// Adds the arguments to be passed to a container resource when the container is started. + /// + /// The resource type. + /// The resource builder. + /// The arguments to be passed to the container when it is started. + /// The . + public static IResourceBuilder WithArgs(this IResourceBuilder builder, params string[] args) where T : IResourceWithArgs + { + return builder.WithArgs(context => context.Args.AddRange(args)); + } + + /// + /// Adds a callback to be executed with a list of command-line arguments when a container resource is started. + /// + /// + /// The resource builder. + /// A callback that allows for deferred execution for computing arguments. This runs after resources have been allocated by the orchestrator and allows access to other resources to resolve computed data, e.g. connection strings, ports. + /// The . + public static IResourceBuilder WithArgs(this IResourceBuilder builder, Action callback) where T : IResourceWithArgs + { + return builder.WithArgs(context => + { + callback(context); + return Task.CompletedTask; + }); + } + + /// + /// Adds a callback to be executed with a list of command-line arguments when a container resource is started. + /// + /// The resource type. + /// The resource builder. + /// A callback that allows for deferred execution for computing arguments. This runs after resources have been allocated by the orchestrator and allows access to other resources to resolve computed data, e.g. connection strings, ports. + /// The . + public static IResourceBuilder WithArgs(this IResourceBuilder builder, Func callback) where T : IResourceWithArgs + { + return builder.WithAnnotation(new CommandLineArgsCallbackAnnotation(callback)); + } + /// /// Registers a callback which is invoked when manifest is generated for the app model. /// diff --git a/src/Aspire.Hosting/Node/NodeAppResource.cs b/src/Aspire.Hosting/Node/NodeAppResource.cs index 438f41da122..6bb55c880d6 100644 --- a/src/Aspire.Hosting/Node/NodeAppResource.cs +++ b/src/Aspire.Hosting/Node/NodeAppResource.cs @@ -10,9 +10,8 @@ namespace Aspire.Hosting; /// The name of the resource. /// The command to execute. /// The working directory to use for the command. If null, the working directory of the current process is used. -/// The arguments to pass to the command. -public class NodeAppResource(string name, string command, string workingDirectory, string[]? args) - : ExecutableResource(name, command, workingDirectory, args), IResourceWithServiceDiscovery +public class NodeAppResource(string name, string command, string workingDirectory) + : ExecutableResource(name, command, workingDirectory), IResourceWithServiceDiscovery { } diff --git a/src/Aspire.Hosting/Node/NodeExtensions.cs b/src/Aspire.Hosting/Node/NodeExtensions.cs index d670d9043fc..e0dba211468 100644 --- a/src/Aspire.Hosting/Node/NodeExtensions.cs +++ b/src/Aspire.Hosting/Node/NodeExtensions.cs @@ -27,10 +27,11 @@ public static IResourceBuilder AddNodeApp(this IDistributedAppl workingDirectory ??= Path.GetDirectoryName(scriptPath)!; workingDirectory = PathNormalizer.NormalizePathForCurrentPlatform(Path.Combine(builder.AppHostDirectory, workingDirectory)); - var resource = new NodeAppResource(name, "node", workingDirectory, effectiveArgs); + var resource = new NodeAppResource(name, "node", workingDirectory); return builder.AddResource(resource) - .WithNodeDefaults(); + .WithNodeDefaults() + .WithArgs(effectiveArgs); } /// @@ -49,10 +50,11 @@ public static IResourceBuilder AddNpmApp(this IDistributedAppli : ["run", scriptName]; workingDirectory = PathNormalizer.NormalizePathForCurrentPlatform(Path.Combine(builder.AppHostDirectory, workingDirectory)); - var resource = new NodeAppResource(name, "npm", workingDirectory, allArgs); + var resource = new NodeAppResource(name, "npm", workingDirectory); return builder.AddResource(resource) - .WithNodeDefaults(); + .WithNodeDefaults() + .WithArgs(allArgs); } private static IResourceBuilder WithNodeDefaults(this IResourceBuilder builder) => diff --git a/src/Aspire.Hosting/Publishing/ManifestPublisher.cs b/src/Aspire.Hosting/Publishing/ManifestPublisher.cs index 490594cc720..734b1e92127 100644 --- a/src/Aspire.Hosting/Publishing/ManifestPublisher.cs +++ b/src/Aspire.Hosting/Publishing/ManifestPublisher.cs @@ -140,28 +140,7 @@ private static async Task WriteExecutableAsync(ExecutableResource executable, Ma context.Writer.WriteString("command", executable.Command); - var args = new List(executable.Args ?? []); - - if (executable.TryGetAnnotationsOfType(out var argsCallback)) - { - var commandLineArgsContext = new CommandLineArgsCallbackContext(args, context.CancellationToken); - - foreach (var callback in argsCallback) - { - await callback.Callback(commandLineArgsContext).ConfigureAwait(false); - } - } - - if (args.Count > 0) - { - context.Writer.WriteStartArray("args"); - - foreach (var arg in args) - { - context.Writer.WriteStringValue(arg); - } - context.Writer.WriteEndArray(); - } + await context.WriteCommandLineArgumentsAsync(executable).ConfigureAwait(false); await context.WriteEnvironmentVariablesAsync(executable).ConfigureAwait(false); context.WriteBindings(executable); diff --git a/src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs b/src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs index c4d0ee3d63e..2f2b41e06a4 100644 --- a/src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs +++ b/src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs @@ -84,28 +84,7 @@ public async Task WriteContainerAsync(ContainerResource container) } // Write args if they are present - if (container.TryGetAnnotationsOfType(out var argsCallback)) - { - var args = new List(); - - var commandLineArgsContext = new CommandLineArgsCallbackContext(args, CancellationToken); - - foreach (var callback in argsCallback) - { - await callback.Callback(commandLineArgsContext).ConfigureAwait(false); - } - - if (args.Count > 0) - { - Writer.WriteStartArray("args"); - - foreach (var arg in args) - { - Writer.WriteStringValue(arg); - } - Writer.WriteEndArray(); - } - } + await WriteCommandLineArgumentsAsync(container).ConfigureAwait(false); // Write volume & bind mount details WriteContainerMounts(container); @@ -197,6 +176,45 @@ public async Task WriteEnvironmentVariablesAsync(IResource resource) } } + /// + /// TODO: Doc Comments + /// + /// + /// + public async Task WriteCommandLineArgumentsAsync(IResource resource) + { + var args = new List(); + + if (resource.TryGetAnnotationsOfType(out var argsCallback)) + { + var commandLineArgsContext = new CommandLineArgsCallbackContext(args, CancellationToken); + + foreach (var callback in argsCallback) + { + await callback.Callback(commandLineArgsContext).ConfigureAwait(false); + } + } + + if (args.Count > 0) + { + Writer.WriteStartArray("args"); + + foreach (var arg in args) + { + var valueString = arg switch + { + string stringValue => stringValue, + IManifestExpressionProvider manifestExpression => manifestExpression.ValueExpression, + _ => throw new DistributedApplicationException($"The value of the argument '{arg}' is not supported.") + }; + + Writer.WriteStringValue(valueString); + } + + Writer.WriteEndArray(); + } + } + /// /// Writes the "inputs" annotations for the underlying resource. /// diff --git a/tests/Aspire.Hosting.Tests/ContainerResourceTests.cs b/tests/Aspire.Hosting.Tests/ContainerResourceTests.cs index 293ec2c93dc..8fe3a055d81 100644 --- a/tests/Aspire.Hosting.Tests/ContainerResourceTests.cs +++ b/tests/Aspire.Hosting.Tests/ContainerResourceTests.cs @@ -1,6 +1,8 @@ // 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.Tests.Utils; +using Aspire.Hosting.Utils; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -45,4 +47,61 @@ public void AddContainerAddsAnnotationMetadataWithTag() Assert.Equal("none", containerAnnotation.Image); Assert.Null(containerAnnotation.Registry); } + + [Fact] + public async Task AddContainerWithArgs() + { + var appBuilder = DistributedApplication.CreateBuilder(); + + var testResource = new TestResource("test", "connectionString"); + + var c1 = appBuilder.AddContainer("c1", "image2") + .WithEndpoint("ep", e => + { + e.UriScheme = "http"; + e.AllocatedEndpoint = new(e, "localhost", 1234); + }); + + var c2 = appBuilder.AddContainer("container", "none") + .WithArgs(context => + { + context.Args.Add("arg1"); + context.Args.Add(c1.GetEndpoint("ep")); + context.Args.Add(testResource); + }); + + using var app = appBuilder.Build(); + + var args = await ArgumentEvaluator.GetArgumentListAsync(c2.Resource); + + Assert.Collection(args, + arg => Assert.Equal("arg1", arg), + arg => Assert.Equal("http://localhost:1234", arg), + arg => Assert.Equal("connectionString", arg)); + + var manifest = await ManifestUtils.GetManifest(c2.Resource); + + var expectedManifest = + """ + { + "type": "container.v0", + "image": "none:latest", + "args": [ + "arg1", + "{c1.bindings.ep.url}", + "{test.connectionString}" + ] + } + """; + + Assert.Equal(expectedManifest, manifest.ToString()); + } + + private sealed class TestResource(string name, string connectionString) : Resource(name), IResourceWithConnectionString + { + public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) + { + return new(connectionString); + } + } } diff --git a/tests/Aspire.Hosting.Tests/ExecutableResourceTests.cs b/tests/Aspire.Hosting.Tests/ExecutableResourceTests.cs new file mode 100644 index 00000000000..2cafa8da0ea --- /dev/null +++ b/tests/Aspire.Hosting.Tests/ExecutableResourceTests.cs @@ -0,0 +1,71 @@ +// 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.Tests.Utils; +using Aspire.Hosting.Utils; +using Xunit; + +namespace Aspire.Hosting.Tests; + +public class ExecutableResourceTests +{ + [Fact] + public async Task AddExecutableWithArgs() + { + var appBuilder = DistributedApplication.CreateBuilder(); + + var testResource = new TestResource("test", "connectionString"); + + var exe1 = appBuilder.AddExecutable("e1", "ruby", ".", "app.rb") + .WithEndpoint("ep", e => + { + e.UriScheme = "http"; + e.AllocatedEndpoint = new(e, "localhost", 1234); + }); + + var exe2 = appBuilder.AddExecutable("e2", "python", ".", "app.py") + .WithArgs(context => + { + context.Args.Add("arg1"); + context.Args.Add(exe1.GetEndpoint("ep")); + context.Args.Add(testResource); + }); + + using var app = appBuilder.Build(); + + var args = await ArgumentEvaluator.GetArgumentListAsync(exe2.Resource); + + Assert.Collection(args, + arg => Assert.Equal("app.py", arg), + arg => Assert.Equal("arg1", arg), + arg => Assert.Equal("http://localhost:1234", arg), + arg => Assert.Equal("connectionString", arg)); + + var manifest = await ManifestUtils.GetManifest(exe2.Resource); + + var expectedManifest = + """ + { + "type": "executable.v0", + "workingDirectory": "net8.0", + "command": "python", + "args": [ + "app.py", + "arg1", + "{e1.bindings.ep.url}", + "{test.connectionString}" + ] + } + """; + + Assert.Equal(expectedManifest, manifest.ToString()); + } + + private sealed class TestResource(string name, string connectionString) : Resource(name), IResourceWithConnectionString + { + public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) + { + return new(connectionString); + } + } +} diff --git a/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs b/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs index 4683c01e774..99d16bea418 100644 --- a/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs +++ b/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs @@ -512,6 +512,7 @@ public void NodeAppIsExecutableResource() static void AssertNodeResource(string resourceName, JsonElement jsonElement, string expectedCommand, string[] expectedArgs) { + var s = jsonElement.ToString(); Assert.Equal("executable.v0", jsonElement.GetProperty("type").GetString()); var bindings = jsonElement.GetProperty("bindings"); @@ -526,8 +527,6 @@ static void AssertNodeResource(string resourceName, JsonElement jsonElement, str var command = jsonElement.GetProperty("command"); Assert.Equal(expectedCommand, command.GetString()); Assert.Equal(expectedArgs, jsonElement.GetProperty("args").EnumerateArray().Select(e => e.GetString()).ToArray()); - - var args = jsonElement.GetProperty("args"); } AssertNodeResource("nodeapp", nodeApp, "node", ["..\\foo\\app.js"]); diff --git a/tests/Aspire.Hosting.Tests/Nats/AddNatsTests.cs b/tests/Aspire.Hosting.Tests/Nats/AddNatsTests.cs index 2f6a0c748a2..41625977960 100644 --- a/tests/Aspire.Hosting.Tests/Nats/AddNatsTests.cs +++ b/tests/Aspire.Hosting.Tests/Nats/AddNatsTests.cs @@ -58,7 +58,7 @@ public void AddNatsContainerAddsAnnotationMetadata() var argsAnnotation = Assert.Single(containerResource.Annotations.OfType()); Assert.NotNull(argsAnnotation.Callback); - var args = new List(); + var args = new List(); argsAnnotation.Callback(new CommandLineArgsCallbackContext(args)); Assert.Equal("-js -sd /data".Split(' '), args); diff --git a/tests/Aspire.Hosting.Tests/Utils/ArgumentEvaluator.cs b/tests/Aspire.Hosting.Tests/Utils/ArgumentEvaluator.cs new file mode 100644 index 00000000000..e6aebe23c4c --- /dev/null +++ b/tests/Aspire.Hosting.Tests/Utils/ArgumentEvaluator.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting.Tests.Utils; + +internal sealed class ArgumentEvaluator +{ + public static async ValueTask> GetArgumentListAsync(IResource resource) + { + var finalArgs = new List(); + + if (resource.TryGetAnnotationsOfType(out var exeArgsCallbacks)) + { + var args = new List(); + var commandLineContext = new CommandLineArgsCallbackContext(args, default); + + foreach (var exeArgsCallback in exeArgsCallbacks) + { + await exeArgsCallback.Callback(commandLineContext).ConfigureAwait(false); + } + + foreach (var arg in args) + { + var value = arg switch + { + string s => s, + IValueProvider valueProvider => await valueProvider.GetValueAsync().ConfigureAwait(false), + null => null, + _ => throw new InvalidOperationException($"Unexpected value for {arg}") + }; + + if (value is not null) + { + finalArgs.Add(value); + } + } + } + + return finalArgs; + } +} From 909e67bdb42ac23c2fe855d501f643335a053775 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Tue, 12 Mar 2024 14:43:26 +1100 Subject: [PATCH 48/50] Azure Open AI provisioning via CDK. (#2771) --- Directory.Packages.props | 8 +-- .../OpenAIEndToEnd.AppHost.csproj | 2 +- .../OpenAIEndToEnd.AppHost/Program.cs | 4 +- .../Properties/launchSettings.json | 10 +++ .../aspire-manifest.json | 20 +----- .../openai.module.bicep | 52 +++++++++++++++ .../AzureOpenAIResource.cs | 41 ++++++++++++ .../AzureKeyVaultResourceExtensions.cs | 2 +- .../Extensions/AzureOpenAIExtensions.cs | 64 +++++++++++++++++++ .../Extensions/AzureServiceBusExtensions.cs | 2 +- .../Extensions/AzureSqlExtensions.cs | 2 +- .../Extensions/AzureStorageExtensions.cs | 6 +- .../Azure/AzureBicepResourceTests.cs | 2 +- 13 files changed, 184 insertions(+), 31 deletions(-) create mode 100644 playground/OpenAIEndToEnd/OpenAIEndToEnd.AppHost/openai.module.bicep diff --git a/Directory.Packages.props b/Directory.Packages.props index 9f6e4a9c76e..94304ac66a3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -22,10 +22,10 @@ - - - - + + + + diff --git a/playground/OpenAIEndToEnd/OpenAIEndToEnd.AppHost/OpenAIEndToEnd.AppHost.csproj b/playground/OpenAIEndToEnd/OpenAIEndToEnd.AppHost/OpenAIEndToEnd.AppHost.csproj index 795f21eaa78..d1859e4c425 100644 --- a/playground/OpenAIEndToEnd/OpenAIEndToEnd.AppHost/OpenAIEndToEnd.AppHost.csproj +++ b/playground/OpenAIEndToEnd/OpenAIEndToEnd.AppHost/OpenAIEndToEnd.AppHost.csproj @@ -6,7 +6,7 @@ enable enable true - b757dbc4-6942-448f-9f26-d15de0f2e760 + 55ef4430-77f1-4dfa-9af3-e2e39d665e2a diff --git a/playground/OpenAIEndToEnd/OpenAIEndToEnd.AppHost/Program.cs b/playground/OpenAIEndToEnd/OpenAIEndToEnd.AppHost/Program.cs index 2516dd1b44e..39a4685e43b 100644 --- a/playground/OpenAIEndToEnd/OpenAIEndToEnd.AppHost/Program.cs +++ b/playground/OpenAIEndToEnd/OpenAIEndToEnd.AppHost/Program.cs @@ -5,8 +5,8 @@ builder.AddAzureProvisioning(); -var openai = builder.AddAzureOpenAI("openai") - .WithDeployment(new("gpt-35-turbo", "gpt-35-turbo", "0613")); +var openai = builder.AddAzureOpenAIConstruct("openai") + .AddDeployment(new("gpt-35-turbo", "gpt-35-turbo", "0613")); builder.AddProject("webstory") .WithReference(openai); diff --git a/playground/OpenAIEndToEnd/OpenAIEndToEnd.AppHost/Properties/launchSettings.json b/playground/OpenAIEndToEnd/OpenAIEndToEnd.AppHost/Properties/launchSettings.json index 1b2de78f9dd..3185d212b1b 100644 --- a/playground/OpenAIEndToEnd/OpenAIEndToEnd.AppHost/Properties/launchSettings.json +++ b/playground/OpenAIEndToEnd/OpenAIEndToEnd.AppHost/Properties/launchSettings.json @@ -11,6 +11,16 @@ "DOTNET_ENVIRONMENT": "Development", "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16195" } + }, + "generate-manifest": { + "commandName": "Project", + "launchBrowser": true, + "dotnetRunMessages": true, + "commandLineArgs": "--publisher manifest --output-path aspire-manifest.json", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development" + } } } } diff --git a/playground/OpenAIEndToEnd/OpenAIEndToEnd.AppHost/aspire-manifest.json b/playground/OpenAIEndToEnd/OpenAIEndToEnd.AppHost/aspire-manifest.json index 90e9489321e..25b9dfc402f 100644 --- a/playground/OpenAIEndToEnd/OpenAIEndToEnd.AppHost/aspire-manifest.json +++ b/playground/OpenAIEndToEnd/OpenAIEndToEnd.AppHost/aspire-manifest.json @@ -3,23 +3,8 @@ "openai": { "type": "azure.bicep.v0", "connectionString": "{openai.outputs.connectionString}", - "path": "aspire.hosting.azure.bicep.openai.bicep", + "path": "openai.module.bicep", "params": { - "name": "openai", - "deployments": [ - { - "name": "gpt-35-turbo", - "sku": { - "name": "Standard", - "capacity": 1 - }, - "model": { - "format": "OpenAI", - "name": "gpt-35-turbo", - "version": "0613" - } - } - ], "principalId": "", "principalType": "" } @@ -30,6 +15,7 @@ "env": { "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true", "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true", + "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true", "ConnectionStrings__openai": "{openai.connectionString}" }, "bindings": { @@ -46,4 +32,4 @@ } } } -} +} \ No newline at end of file diff --git a/playground/OpenAIEndToEnd/OpenAIEndToEnd.AppHost/openai.module.bicep b/playground/OpenAIEndToEnd/OpenAIEndToEnd.AppHost/openai.module.bicep new file mode 100644 index 00000000000..9ce1cf7cd8f --- /dev/null +++ b/playground/OpenAIEndToEnd/OpenAIEndToEnd.AppHost/openai.module.bicep @@ -0,0 +1,52 @@ +targetScope = 'resourceGroup' + +@description('') +param location string = resourceGroup().location + +@description('') +param principalId string + +@description('') +param principalType string + + +resource cognitiveServicesAccount_6g8jyEjX5 'Microsoft.CognitiveServices/accounts@2023-05-01' = { + name: toLower(take(concat('openai', uniqueString(resourceGroup().id)), 24)) + location: location + kind: 'OpenAI' + sku: { + name: 'S0' + } + properties: { + customSubDomainName: toLower(take(concat('openai', uniqueString(resourceGroup().id)), 24)) + publicNetworkAccess: 'Enabled' + } +} + +resource roleAssignment_X7ie0XqR2 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: cognitiveServicesAccount_6g8jyEjX5 + name: guid(cognitiveServicesAccount_6g8jyEjX5.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a001fd3d-188f-4b5d-821b-7da978bf7442')) + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a001fd3d-188f-4b5d-821b-7da978bf7442') + principalId: principalId + principalType: principalType + } +} + +resource cognitiveServicesAccountDeployment_f9rYX6SRK 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = { + parent: cognitiveServicesAccount_6g8jyEjX5 + name: 'gpt-35-turbo' + sku: { + name: 'Standard' + capacity: 1 + } + properties: { + model: { + name: 'gpt-35-turbo' + format: 'OpenAI' + version: '0613' + } + } +} + +output connectionString string = 'Endpoint=${cognitiveServicesAccount_6g8jyEjX5.properties.endpoint}' diff --git a/src/Aspire.Hosting.Azure/AzureOpenAIResource.cs b/src/Aspire.Hosting.Azure/AzureOpenAIResource.cs index cafca733b79..35d774005d7 100644 --- a/src/Aspire.Hosting.Azure/AzureOpenAIResource.cs +++ b/src/Aspire.Hosting.Azure/AzureOpenAIResource.cs @@ -43,3 +43,44 @@ internal void AddDeployment(AzureOpenAIDeployment deployment) _deployments.Add(deployment); } } + +/// +/// Represents an Azure OpenAI resource. +/// +/// The name of the resource. +/// Configures the underlying Azure resource using the CDK. +public class AzureOpenAIConstructResource(string name, Action configureConstruct) : + AzureConstructResource(name, configureConstruct), + IResourceWithConnectionString +{ + private readonly List _deployments = []; + + /// + /// Gets the "connectionString" output reference from the Azure OpenAI resource. + /// + public BicepOutputReference ConnectionString => new("connectionString", this); + + /// + /// Gets the connection string template for the manifest for the resource. + /// + public string ConnectionStringExpression => ConnectionString.ValueExpression; + + /// + /// Gets the connection string for the resource. + /// + /// The connection string for the resource. + public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken) + => ConnectionString.GetValueAsync(cancellationToken); + + /// + /// Gets the list of deployments of the Azure OpenAI resource. + /// + public IReadOnlyList Deployments => _deployments; + + internal void AddDeployment(AzureOpenAIDeployment deployment) + { + ArgumentNullException.ThrowIfNull(deployment); + + _deployments.Add(deployment); + } +} diff --git a/src/Aspire.Hosting.Azure/Extensions/AzureKeyVaultResourceExtensions.cs b/src/Aspire.Hosting.Azure/Extensions/AzureKeyVaultResourceExtensions.cs index e755bdc7a6d..9c52b9a9557 100644 --- a/src/Aspire.Hosting.Azure/Extensions/AzureKeyVaultResourceExtensions.cs +++ b/src/Aspire.Hosting.Azure/Extensions/AzureKeyVaultResourceExtensions.cs @@ -41,7 +41,7 @@ public static IResourceBuilder AddAzureKeyVaultC var configureConstruct = (ResourceModuleConstruct construct) => { var keyVault = construct.AddKeyVault(name: construct.Resource.Name); - keyVault.AddOutput(x => x.Properties.VaultUri, "vaultUri"); + keyVault.AddOutput("vaultUri", x => x.Properties.VaultUri); keyVault.Properties.Tags["aspire-resource-name"] = construct.Resource.Name; diff --git a/src/Aspire.Hosting.Azure/Extensions/AzureOpenAIExtensions.cs b/src/Aspire.Hosting.Azure/Extensions/AzureOpenAIExtensions.cs index cfd2c463b59..e05f8af0c59 100644 --- a/src/Aspire.Hosting.Azure/Extensions/AzureOpenAIExtensions.cs +++ b/src/Aspire.Hosting.Azure/Extensions/AzureOpenAIExtensions.cs @@ -4,6 +4,9 @@ using System.Text.Json.Nodes; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Azure; +using Azure.Provisioning.Authorization; +using Azure.Provisioning.CognitiveServices; +using Azure.ResourceManager.CognitiveServices.Models; namespace Aspire.Hosting; @@ -29,6 +32,55 @@ public static IResourceBuilder AddAzureOpenAI(this IDistrib .WithManifestPublishingCallback(resource.WriteToManifest); } + /// + /// Adds an Azure OpenAI resource to the application model. + /// + /// The . + /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. + /// + /// A reference to the . + public static IResourceBuilder AddAzureOpenAIConstruct(this IDistributedApplicationBuilder builder, string name, Action, ResourceModuleConstruct, CognitiveServicesAccount, IEnumerable>? configureResource = null) + { + var configureConstruct = (ResourceModuleConstruct construct) => + { + var cogServicesAccount = new CognitiveServicesAccount(construct, "OpenAI", name: name); + cogServicesAccount.AssignProperty(x => x.Properties.CustomSubDomainName, $"toLower(take(concat('{name}', uniqueString(resourceGroup().id)), 24))"); + cogServicesAccount.AssignProperty(x => x.Properties.PublicNetworkAccess, "'Enabled'"); + cogServicesAccount.AddOutput("connectionString", """'Endpoint=${{{0}}}'""", x => x.Properties.Endpoint); + + var roleAssignment = cogServicesAccount.AssignRole(RoleDefinition.CognitiveServicesOpenAIContributor); + roleAssignment.AssignProperty(x => x.PrincipalId, construct.PrincipalIdParameter); + roleAssignment.AssignProperty(x => x.PrincipalType, construct.PrincipalTypeParameter); + + var resource = (AzureOpenAIConstructResource)construct.Resource; + + var cdkDeployments = new List(); + foreach (var deployment in resource.Deployments) + { + var model = new CognitiveServicesAccountDeploymentModel(); + model.Name = deployment.ModelName; + model.Version = deployment.ModelVersion; + model.Format = "OpenAI"; + + var cdkDeployment = new CognitiveServicesAccountDeployment(construct, model, parent: cogServicesAccount, name: deployment.Name); + cdkDeployment.AssignProperty(x => x.Sku.Name, $"'{deployment.SkuName}'"); + cdkDeployment.AssignProperty(x => x.Sku.Capacity, $"{deployment.SkuCapacity}"); + } + + if (configureResource != null) + { + var resourceBuilder = builder.CreateResourceBuilder(resource); + configureResource(resourceBuilder, construct, cogServicesAccount, cdkDeployments); + } + }; + + var resource = new AzureOpenAIConstructResource(name, configureConstruct); + return builder.AddResource(resource) + .WithParameter(AzureBicepResource.KnownParameters.PrincipalId) + .WithParameter(AzureBicepResource.KnownParameters.PrincipalType) + .WithManifestPublishingCallback(resource.WriteToManifest); + } + /// /// Adds an Azure OpenAI Deployment resource to the application model. This resource requires an to be added to the application model. /// @@ -41,6 +93,18 @@ public static IResourceBuilder WithDeployment(this IResourc return builder; } + /// + /// Adds an Azure OpenAI Deployment resource to the application model. This resource requires an to be added to the application model. + /// + /// The Azure OpenAI resource builder. + /// The deployment to add. + /// A reference to the . + public static IResourceBuilder AddDeployment(this IResourceBuilder builder, AzureOpenAIDeployment deployment) + { + builder.Resource.AddDeployment(deployment); + return builder; + } + internal static JsonArray GetDeploymentsAsJson(AzureOpenAIResource resource) { return new JsonArray( diff --git a/src/Aspire.Hosting.Azure/Extensions/AzureServiceBusExtensions.cs b/src/Aspire.Hosting.Azure/Extensions/AzureServiceBusExtensions.cs index 5074180f9d4..261bba4ca47 100644 --- a/src/Aspire.Hosting.Azure/Extensions/AzureServiceBusExtensions.cs +++ b/src/Aspire.Hosting.Azure/Extensions/AzureServiceBusExtensions.cs @@ -60,7 +60,7 @@ public static IResourceBuilder AddAzureService var serviceBusDataOwnerRole = serviceBusNamespace.AssignRole(RoleDefinition.ServiceBusDataOwner); serviceBusDataOwnerRole.AssignProperty(p => p.PrincipalType, construct.PrincipalTypeParameter); - serviceBusNamespace.AddOutput(sa => sa.ServiceBusEndpoint, "serviceBusEndpoint"); + serviceBusNamespace.AddOutput("serviceBusEndpoint", sa => sa.ServiceBusEndpoint); configureResource?.Invoke(construct, serviceBusNamespace); diff --git a/src/Aspire.Hosting.Azure/Extensions/AzureSqlExtensions.cs b/src/Aspire.Hosting.Azure/Extensions/AzureSqlExtensions.cs index d65d573f18d..b18c28a13c7 100644 --- a/src/Aspire.Hosting.Azure/Extensions/AzureSqlExtensions.cs +++ b/src/Aspire.Hosting.Azure/Extensions/AzureSqlExtensions.cs @@ -110,7 +110,7 @@ internal static IResourceBuilder PublishAsAzureSqlDatab sqlDatabases.Add(sqlDatabase); } - sqlServer.AddOutput(x => x.FullyQualifiedDomainName, "sqlServerFqdn"); + sqlServer.AddOutput("sqlServerFqdn", x => x.FullyQualifiedDomainName); if (configureResource != null) { diff --git a/src/Aspire.Hosting.Azure/Extensions/AzureStorageExtensions.cs b/src/Aspire.Hosting.Azure/Extensions/AzureStorageExtensions.cs index e32800d76fc..b042984a38e 100644 --- a/src/Aspire.Hosting.Azure/Extensions/AzureStorageExtensions.cs +++ b/src/Aspire.Hosting.Azure/Extensions/AzureStorageExtensions.cs @@ -62,9 +62,9 @@ public static IResourceBuilder AddAzureConstructS var queueRole = storageAccount.AssignRole(RoleDefinition.StorageQueueDataContributor); queueRole.AssignProperty(p => p.PrincipalType, construct.PrincipalTypeParameter); - storageAccount.AddOutput(sa => sa.PrimaryEndpoints.BlobUri, "blobEndpoint"); - storageAccount.AddOutput(sa => sa.PrimaryEndpoints.QueueUri, "queueEndpoint"); - storageAccount.AddOutput(sa => sa.PrimaryEndpoints.TableUri, "tableEndpoint"); + storageAccount.AddOutput("blobEndpoint", sa => sa.PrimaryEndpoints.BlobUri); + storageAccount.AddOutput("queueEndpoint", sa => sa.PrimaryEndpoints.QueueUri); + storageAccount.AddOutput("tableEndpoint", sa => sa.PrimaryEndpoints.TableUri); if (configureResource != null) { diff --git a/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs b/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs index ece58fbcfec..9cc0a1a279e 100644 --- a/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs +++ b/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs @@ -226,7 +226,7 @@ public async Task AddAzureConstructGenertesCorrectManifestEntry() kind: StorageKind.StorageV2, sku: StorageSkuName.StandardLrs ); - storage.AddOutput(sa => sa.Name, "storageAccountName"); + storage.AddOutput("storageAccountName", sa => sa.Name); }); var manifest = await ManifestUtils.GetManifest(construct1.Resource); From cd7972c356dd484a204b19439b07709ad4cdce85 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Tue, 12 Mar 2024 00:29:56 -0400 Subject: [PATCH 49/50] [tests] Refactor MongoDB.Driver tests to use testcontainers (#2718) * [tests] Refactor MongoDB.Driver tests to use testcontainers * Run MongoDB.Driver tests on helix * RequiredDocker: disable for windows on CI - build machine, and helix * fix condition * Fix the health tests when container is not available * Use a property for the nuget version * move package version property to Directory.Packages.props * fix typo * address review feedback * Move RequiresDockerTheoryAttribute.cs to the Common project - addresses review feedback * Remove /test_db from the connection string, so it defaults to using admin db to auth - And explicitly use mongo:7.0.5 TODO: pick up the version to match what we use for aspire.hosting * Create a test_db with user mongo, pwd:mongo as expected by the tests * mongodb: don't use auth in the tests * fix build --- Directory.Packages.props | 4 +- .../RequiresDockerTheoryAttribute.cs | 2 +- .../Aspire.MongoDB.Driver.Tests.csproj | 3 ++ .../AspireMongoDBDriverExtensionsTests.cs | 21 ++++++--- .../ConformanceTests.cs | 43 ++++++++----------- .../MongoDbContainerFixture.cs | 39 +++++++++++++++++ .../AspireRabbitMQExtensionsTests.cs | 1 + .../AspireRabbitMQLoggingTests.cs | 1 + .../ConformanceTests.cs | 1 + .../RabbitMQContainerFixture.cs | 1 + 10 files changed, 84 insertions(+), 32 deletions(-) rename tests/{Aspire.RabbitMQ.Client.Tests => Aspire.Components.Common.Tests}/RequiresDockerTheoryAttribute.cs (96%) create mode 100644 tests/Aspire.MongoDB.Driver.Tests/MongoDbContainerFixture.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 94304ac66a3..d8cec96fa0f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,6 +5,7 @@ true + 3.7.0 @@ -131,7 +132,8 @@ - + + diff --git a/tests/Aspire.RabbitMQ.Client.Tests/RequiresDockerTheoryAttribute.cs b/tests/Aspire.Components.Common.Tests/RequiresDockerTheoryAttribute.cs similarity index 96% rename from tests/Aspire.RabbitMQ.Client.Tests/RequiresDockerTheoryAttribute.cs rename to tests/Aspire.Components.Common.Tests/RequiresDockerTheoryAttribute.cs index 76dd906cccd..26781718cb5 100644 --- a/tests/Aspire.RabbitMQ.Client.Tests/RequiresDockerTheoryAttribute.cs +++ b/tests/Aspire.Components.Common.Tests/RequiresDockerTheoryAttribute.cs @@ -3,7 +3,7 @@ using Xunit; -namespace Aspire.RabbitMQ.Client.Tests; +namespace Aspire.Components.Common.Tests; // TODO: remove these attributes when Helix has a Windows agent with Docker support public class RequiresDockerTheoryAttribute : TheoryAttribute diff --git a/tests/Aspire.MongoDB.Driver.Tests/Aspire.MongoDB.Driver.Tests.csproj b/tests/Aspire.MongoDB.Driver.Tests/Aspire.MongoDB.Driver.Tests.csproj index 8e8bacaffe9..add98da2b44 100644 --- a/tests/Aspire.MongoDB.Driver.Tests/Aspire.MongoDB.Driver.Tests.csproj +++ b/tests/Aspire.MongoDB.Driver.Tests/Aspire.MongoDB.Driver.Tests.csproj @@ -4,6 +4,7 @@ $(NetCurrent) $(NoWarn);CS8002 + true @@ -11,6 +12,8 @@ + + diff --git a/tests/Aspire.MongoDB.Driver.Tests/AspireMongoDBDriverExtensionsTests.cs b/tests/Aspire.MongoDB.Driver.Tests/AspireMongoDBDriverExtensionsTests.cs index 1b3b952a931..a140cc8f936 100644 --- a/tests/Aspire.MongoDB.Driver.Tests/AspireMongoDBDriverExtensionsTests.cs +++ b/tests/Aspire.MongoDB.Driver.Tests/AspireMongoDBDriverExtensionsTests.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 Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; @@ -10,11 +11,19 @@ namespace Aspire.MongoDB.Driver.Tests; -public class AspireMongoDBDriverExtensionsTests +public class AspireMongoDBDriverExtensionsTests : IClassFixture { - private const string DefaultConnectionString = "mongodb://localhost:27017/mydatabase"; private const string DefaultConnectionName = "mongodb"; + private readonly MongoDbContainerFixture _containerFixture; + + public AspireMongoDBDriverExtensionsTests(MongoDbContainerFixture containerFixture) + { + _containerFixture = containerFixture; + } + + private string DefaultConnectionString => _containerFixture.GetConnectionString(); + [Theory] [InlineData("mongodb://localhost:27017/mydatabase", true)] [InlineData("mongodb://localhost:27017", false)] @@ -79,7 +88,7 @@ public void AddKeyedMongoDBDataSource_ReadsFromConnectionStringsCorrectly(string } } - [Fact] + [RequiresDockerFact] public async Task AddMongoDBDataSource_HealthCheckShouldBeRegisteredWhenEnabled() { var builder = CreateBuilder(DefaultConnectionString); @@ -101,7 +110,7 @@ public async Task AddMongoDBDataSource_HealthCheckShouldBeRegisteredWhenEnabled( Assert.Contains(healthCheckReport.Entries, x => x.Key == healthCheckName); } - [Fact] + [RequiresDockerFact] public void AddKeyedMongoDBDataSource_HealthCheckShouldNotBeRegisteredWhenDisabled() { var builder = CreateBuilder(DefaultConnectionString); @@ -119,7 +128,7 @@ public void AddKeyedMongoDBDataSource_HealthCheckShouldNotBeRegisteredWhenDisabl } - [Fact] + [RequiresDockerFact] public async Task AddKeyedMongoDBDataSource_HealthCheckShouldBeRegisteredWhenEnabled() { var key = DefaultConnectionName; @@ -143,7 +152,7 @@ public async Task AddKeyedMongoDBDataSource_HealthCheckShouldBeRegisteredWhenEna Assert.Contains(healthCheckReport.Entries, x => x.Key == healthCheckName); } - [Fact] + [RequiresDockerFact] public void AddMongoDBDataSource_HealthCheckShouldNotBeRegisteredWhenDisabled() { var builder = CreateBuilder(DefaultConnectionString); diff --git a/tests/Aspire.MongoDB.Driver.Tests/ConformanceTests.cs b/tests/Aspire.MongoDB.Driver.Tests/ConformanceTests.cs index 38ae3488064..b8c2179d167 100644 --- a/tests/Aspire.MongoDB.Driver.Tests/ConformanceTests.cs +++ b/tests/Aspire.MongoDB.Driver.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,11 +11,9 @@ namespace Aspire.MongoDB.Driver.Tests; -public class ConformanceTests : ConformanceTests +public class ConformanceTests : ConformanceTests, IClassFixture { - private const string ConnectionSting = "mongodb://root:password@localhost:27017/test_db"; - - private static readonly Lazy s_canConnectToServer = new(GetCanConnect); + private readonly MongoDbContainerFixture _containerFixture; protected override ServiceLifetime ServiceLifetime => ServiceLifetime.Singleton; @@ -22,7 +21,7 @@ public class ConformanceTests : ConformanceTests protected override bool SupportsKeyedRegistrations => true; - protected override bool CanConnectToServer => s_canConnectToServer.Value; + protected override bool CanConnectToServer => RequiresDockerTheoryAttribute.IsSupported; protected override string ValidJsonConfig => """ { @@ -39,6 +38,11 @@ public class ConformanceTests : ConformanceTests } """; + public ConformanceTests(MongoDbContainerFixture containerFixture) + { + _containerFixture = containerFixture; + } + protected override (string json, string error)[] InvalidJsonToErrorMessage => new[] { ("""{"Aspire": { "MongoDB":{ "Driver": { "HealthChecks": "true"}}}}""", "Value is \"string\" but should be \"boolean\""), @@ -53,12 +57,18 @@ protected override (string json, string error)[] InvalidJsonToErrorMessage => ne ]; protected override void PopulateConfiguration(ConfigurationManager configuration, string? key = null) - => configuration.AddInMemoryCollection(new KeyValuePair[1] - { + { + var connectionString = RequiresDockerTheoryAttribute.IsSupported ? + $"{_containerFixture.GetConnectionString()}test_db" : + "mongodb://root:password@localhost:27017/test_db"; + + configuration.AddInMemoryCollection( + [ new KeyValuePair( CreateConfigKey("Aspire:MongoDB:Driver", key, "ConnectionString"), - ConnectionSting) - }); + connectionString) + ]); + } protected override void RegisterComponent(HostApplicationBuilder builder, Action? configure = null, string? key = null) { @@ -107,19 +117,4 @@ public void ClientAndDatabaseInstancesShouldBeResolved(string? key) T? Resolve() => key is null ? host.Services.GetService() : host.Services.GetKeyedService(key); } - - private static bool GetCanConnect() - { - var client = new MongoClient(ConnectionSting); - - try - { - client.ListDatabaseNames(); - return true; - } - catch (Exception) - { - return false; - } - } } diff --git a/tests/Aspire.MongoDB.Driver.Tests/MongoDbContainerFixture.cs b/tests/Aspire.MongoDB.Driver.Tests/MongoDbContainerFixture.cs new file mode 100644 index 00000000000..e51ef26f0ad --- /dev/null +++ b/tests/Aspire.MongoDB.Driver.Tests/MongoDbContainerFixture.cs @@ -0,0 +1,39 @@ +// 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 Testcontainers.MongoDb; +using Xunit; + +namespace Aspire.MongoDB.Driver.Tests; + +public sealed class MongoDbContainerFixture : IAsyncLifetime +{ + public MongoDbContainer? Container { get; private set; } + + public string GetConnectionString() => Container?.GetConnectionString() ?? + throw new InvalidOperationException("The test container was not initialized."); + + public async Task InitializeAsync() + { + if (RequiresDockerTheoryAttribute.IsSupported) + { + // testcontainers uses mongo:mongo by default, + // resetting that for tests + Container = new MongoDbBuilder() + .WithImage("mongo:7.0.5") + .WithUsername(null) + .WithPassword(null) + .Build(); + await Container.StartAsync(); + } + } + + public async Task DisposeAsync() + { + if (Container is not null) + { + await Container.DisposeAsync(); + } + } +} diff --git a/tests/Aspire.RabbitMQ.Client.Tests/AspireRabbitMQExtensionsTests.cs b/tests/Aspire.RabbitMQ.Client.Tests/AspireRabbitMQExtensionsTests.cs index e8c2cb2512b..4a4b44d8d04 100644 --- a/tests/Aspire.RabbitMQ.Client.Tests/AspireRabbitMQExtensionsTests.cs +++ b/tests/Aspire.RabbitMQ.Client.Tests/AspireRabbitMQExtensionsTests.cs @@ -4,6 +4,7 @@ using System.Net.Security; using System.Security.Authentication; using System.Text; +using Aspire.Components.Common.Tests; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; diff --git a/tests/Aspire.RabbitMQ.Client.Tests/AspireRabbitMQLoggingTests.cs b/tests/Aspire.RabbitMQ.Client.Tests/AspireRabbitMQLoggingTests.cs index 2519964eb9d..edbf9185e25 100644 --- a/tests/Aspire.RabbitMQ.Client.Tests/AspireRabbitMQLoggingTests.cs +++ b/tests/Aspire.RabbitMQ.Client.Tests/AspireRabbitMQLoggingTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Concurrent; +using Aspire.Components.Common.Tests; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; diff --git a/tests/Aspire.RabbitMQ.Client.Tests/ConformanceTests.cs b/tests/Aspire.RabbitMQ.Client.Tests/ConformanceTests.cs index 20de0283d2b..2799c9395e1 100644 --- a/tests/Aspire.RabbitMQ.Client.Tests/ConformanceTests.cs +++ b/tests/Aspire.RabbitMQ.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.DotNet.XUnitExtensions; using Microsoft.Extensions.Configuration; diff --git a/tests/Aspire.RabbitMQ.Client.Tests/RabbitMQContainerFixture.cs b/tests/Aspire.RabbitMQ.Client.Tests/RabbitMQContainerFixture.cs index 9dda9e692f7..3155f408c3a 100644 --- a/tests/Aspire.RabbitMQ.Client.Tests/RabbitMQContainerFixture.cs +++ b/tests/Aspire.RabbitMQ.Client.Tests/RabbitMQContainerFixture.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 Testcontainers.RabbitMq; using Xunit; From c1f59e5c1b8f77821015074ec4c8fb53d311d46d Mon Sep 17 00:00:00 2001 From: Igor Velikorossov Date: Tue, 12 Mar 2024 16:23:09 +1100 Subject: [PATCH 50/50] Add missing variable --- eng/pipelines/azure-pipelines-public.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/eng/pipelines/azure-pipelines-public.yml b/eng/pipelines/azure-pipelines-public.yml index 04249a0d815..9eb16777b23 100644 --- a/eng/pipelines/azure-pipelines-public.yml +++ b/eng/pipelines/azure-pipelines-public.yml @@ -36,6 +36,8 @@ variables: value: '' - name: _Sign value: false + - name: HelixApiAccessToken + value: '' resources: containers: