diff --git a/DevProxy.Hosting/DevProxyCommandExtensions.cs b/DevProxy.Hosting/DevProxyCommandExtensions.cs new file mode 100644 index 0000000..b9f2b12 --- /dev/null +++ b/DevProxy.Hosting/DevProxyCommandExtensions.cs @@ -0,0 +1,119 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Security.Cryptography; +using Aspire.Hosting.ApplicationModel; +using DevProxy.Hosting; + +#pragma warning disable IDE0130 +namespace Aspire.Hosting; +#pragma warning restore IDE0130 + +/// +/// Adds dashboard commands for controlling Dev Proxy through its API. +/// +internal static class DevProxyCommandExtensions +{ + internal const string ApiTokenEnvironmentVariable = "DEV_PROXY_API_TOKEN"; + + /// + /// Generates an API token for the Dev Proxy resource, passes it to Dev Proxy + /// and adds commands to start and stop recording. + /// + internal static IResourceBuilder WithRecordingCommands( + this IResourceBuilder builder) + where TResource : IResourceWithEndpoints, IResourceWithEnvironment + { + var apiToken = Convert.ToHexString(RandomNumberGenerator.GetBytes(32)); + + builder.WithEnvironment(ApiTokenEnvironmentVariable, apiToken); + + builder.WithCommand( + name: "start-recording", + displayName: "Start recording", + executeCommand: context => SetRecordingAsync(builder.Resource, apiToken, recording: true, context), + commandOptions: new CommandOptions + { + Description = "Start recording requests in Dev Proxy", + IconName = "Record", + UpdateState = UpdateCommandState + }); + + builder.WithCommand( + name: "stop-recording", + displayName: "Stop recording", + executeCommand: context => SetRecordingAsync(builder.Resource, apiToken, recording: false, context), + commandOptions: new CommandOptions + { + Description = "Stop recording requests in Dev Proxy", + IconName = "RecordStop", + UpdateState = UpdateCommandState + }); + + return builder; + } + + private static ResourceCommandState UpdateCommandState(UpdateCommandStateContext context) => + context.ResourceSnapshot.State?.Text == KnownResourceStates.Running + ? ResourceCommandState.Enabled + : ResourceCommandState.Disabled; + + private static async Task SetRecordingAsync( + IResourceWithEndpoints resource, + string apiToken, + bool recording, + ExecuteCommandContext context) + { + try + { + var endpointUrl = await resource + .GetEndpoint(DevProxyResource.ApiEndpointName) + .GetValueAsync(context.CancellationToken); + + if (string.IsNullOrWhiteSpace(endpointUrl)) + { + return CommandResults.Failure("The Dev Proxy API endpoint is not available."); + } + + using var httpClient = new HttpClient(new HttpClientHandler { UseProxy = false }) + { + BaseAddress = new Uri(endpointUrl), + Timeout = TimeSpan.FromSeconds(10) + }; + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiToken); + + using var response = await httpClient.PostAsJsonAsync( + "/proxy", + new DevProxyUpdate { Recording = recording }, + context.CancellationToken); + + if (response.StatusCode == HttpStatusCode.Unauthorized) + { + return CommandResults.Failure( + $"Dev Proxy rejected the API token. Make sure you're using a version of Dev Proxy that supports the {ApiTokenEnvironmentVariable} environment variable."); + } + + if (!response.IsSuccessStatusCode) + { + var content = await response.Content.ReadAsStringAsync(context.CancellationToken); + return CommandResults.Failure( + $"Dev Proxy returned {(int)response.StatusCode} {response.ReasonPhrase}: {content}"); + } + + return CommandResults.Success(recording ? "Recording started." : "Recording stopped."); + } + catch (HttpRequestException exception) + { + return CommandResults.Failure($"Could not reach the Dev Proxy API: {exception.Message}"); + } + catch (TaskCanceledException) when (!context.CancellationToken.IsCancellationRequested) + { + return CommandResults.Failure("The Dev Proxy API did not respond in time."); + } + } + + private sealed class DevProxyUpdate + { + public bool? Recording { get; set; } + } +} diff --git a/DevProxy.Hosting/DevProxyResourceBuilderExtensions.cs b/DevProxy.Hosting/DevProxyResourceBuilderExtensions.cs index e81b574..bcdb15c 100644 --- a/DevProxy.Hosting/DevProxyResourceBuilderExtensions.cs +++ b/DevProxy.Hosting/DevProxyResourceBuilderExtensions.cs @@ -25,7 +25,8 @@ public static IResourceBuilder AddDevProxyExecutable var resourceBuilder = builder.AddResource(resource) .WithArgs("--as-system-proxy", "false") .WithHttpEndpoint(targetPort: 8000, name: DevProxyResource.ProxyEndpointName) - .WithHttpEndpoint(targetPort: 8897, name: DevProxyResource.ApiEndpointName); + .WithHttpEndpoint(targetPort: 8897, name: DevProxyResource.ApiEndpointName) + .WithRecordingCommands(); return resourceBuilder; } @@ -77,7 +78,8 @@ public static IResourceBuilder AddDevProxyContainer( .WithImageRegistry(DevProxyContainerImageTags.Registry) .WithImageTag(DevProxyContainerImageTags.Tag) .WithHttpEndpoint(targetPort: 8000, name: DevProxyResource.ProxyEndpointName) - .WithHttpEndpoint(targetPort: 8897, name: DevProxyResource.ApiEndpointName); + .WithHttpEndpoint(targetPort: 8897, name: DevProxyResource.ApiEndpointName) + .WithRecordingCommands(); return resourceBuilder; } diff --git a/README.md b/README.md index ea1d596..1b8a434 100644 --- a/README.md +++ b/README.md @@ -91,9 +91,12 @@ builder.Build().Run(); - **Container Resource**: Use `AddDevProxyContainer` to run Dev Proxy as a containerized service. - **Executable Resource**: Use `AddDevProxyExecutable` to run Dev Proxy from the locally installed executable. +- **Recording Commands**: Start and stop Dev Proxy recording from the Aspire dashboard for either resource type. - **Custom Configuration**: Use the extension methods to configure Dev Proxy as needed. - **Service Integration**: Easily integrate Dev Proxy with other services in your application. +Both Dev Proxy resource types expose `Start recording` and `Stop recording` commands in the Aspire dashboard. The commands require a version of Dev Proxy that supports the `DEV_PROXY_API_TOKEN` environment variable. + ## Contributing Contributions are welcome! Please feel free to submit a Pull Request.