From ea1d99841c9bdc070b2cccda73460897932fa7fd Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Tue, 16 Jul 2024 04:13:43 +0000 Subject: [PATCH] SWA emulator component --- CommunityToolkit.Aspire.sln | 16 +- Directory.Packages.props | 5 - ....Aspire.Hosting.Azure.StaticWebApps.csproj | 5 + .../SwaApiEndpointAnnotation.cs | 8 + .../SwaAppEndpointAnnotation.cs | 8 + .../SwaAppHostingExtension.cs | 60 ++++++ .../SwaResource.cs | 7 + ...e.Hosting.Azure.StaticWebApps.Tests.csproj | 23 +++ .../ResourceCreationTests.cs | 178 ++++++++++++++++++ ...t.Aspire.Java.Hosting.EndToEndTests.csproj | 1 - test/Directory.Build.props | 11 ++ 11 files changed, 315 insertions(+), 7 deletions(-) create mode 100644 src/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps.csproj create mode 100644 src/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps/SwaApiEndpointAnnotation.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps/SwaAppEndpointAnnotation.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps/SwaAppHostingExtension.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps/SwaResource.cs create mode 100644 test/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps.Tests/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps.Tests.csproj create mode 100644 test/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps.Tests/ResourceCreationTests.cs create mode 100644 test/Directory.Build.props diff --git a/CommunityToolkit.Aspire.sln b/CommunityToolkit.Aspire.sln index cd84b5fa..d1a76793 100644 --- a/CommunityToolkit.Aspire.sln +++ b/CommunityToolkit.Aspire.sln @@ -7,6 +7,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{414151D4-700 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Aspire.Hosting.Java", "src\CommunityToolkit.Aspire.Hosting.Java\CommunityToolkit.Aspire.Hosting.Java.csproj", "{DAA67050-44B3-458F-9818-5877D606866A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps", "src\CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps\CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps.csproj", "{125DFA83-328D-4F8B-91EC-3057FFF410BE}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{8519CC01-1370-47C8-AD94-B0F326B1563F}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "java", "java", "{F120E99A-FB2A-4C3B-B588-9B7ED1CD4E8A}" @@ -19,10 +21,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Aspire.Jav EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Aspire.Java.ServiceDefaults", "examples\java\CommunityToolkit.Aspire.Java.ServiceDefaults\CommunityToolkit.Aspire.Java.ServiceDefaults.csproj", "{BBAFA814-1026-4A39-AA28-BCAE3951A224}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{899F0713-7FC6-4750-BAFC-AC650B35B453}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{899F0713-7FC6-4750-BAFC-AC650B35B453}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Aspire.Java.Hosting.EndToEndTests", "test\CommunityToolkit.Aspire.Java.Hosting.EndToEndTests\CommunityToolkit.Aspire.Java.Hosting.EndToEndTests.csproj", "{E2905A71-E25D-46FC-A128-D6A366D4D751}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps.Tests", "test\CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps.Tests\CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps.Tests.csproj", "{1B55E682-B518-4E59-8972-07C82ED5A677}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -53,6 +57,14 @@ Global {E2905A71-E25D-46FC-A128-D6A366D4D751}.Debug|Any CPU.Build.0 = Debug|Any CPU {E2905A71-E25D-46FC-A128-D6A366D4D751}.Release|Any CPU.ActiveCfg = Release|Any CPU {E2905A71-E25D-46FC-A128-D6A366D4D751}.Release|Any CPU.Build.0 = Release|Any CPU + {125DFA83-328D-4F8B-91EC-3057FFF410BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {125DFA83-328D-4F8B-91EC-3057FFF410BE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {125DFA83-328D-4F8B-91EC-3057FFF410BE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {125DFA83-328D-4F8B-91EC-3057FFF410BE}.Release|Any CPU.Build.0 = Release|Any CPU + {1B55E682-B518-4E59-8972-07C82ED5A677}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1B55E682-B518-4E59-8972-07C82ED5A677}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1B55E682-B518-4E59-8972-07C82ED5A677}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1B55E682-B518-4E59-8972-07C82ED5A677}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -65,6 +77,8 @@ Global {55AC8E7E-DA0A-489D-8059-4E7190C9EEBF} = {F120E99A-FB2A-4C3B-B588-9B7ED1CD4E8A} {BBAFA814-1026-4A39-AA28-BCAE3951A224} = {F120E99A-FB2A-4C3B-B588-9B7ED1CD4E8A} {E2905A71-E25D-46FC-A128-D6A366D4D751} = {899F0713-7FC6-4750-BAFC-AC650B35B453} + {125DFA83-328D-4F8B-91EC-3057FFF410BE} = {414151D4-7009-4E78-A5C6-D99EBD1E67D1} + {1B55E682-B518-4E59-8972-07C82ED5A677} = {899F0713-7FC6-4750-BAFC-AC650B35B453} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {08B1D4B8-D2C5-4A64-BB8B-E1A2B29525F0} diff --git a/Directory.Packages.props b/Directory.Packages.props index 5debd3f5..5386c6a6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,27 +2,22 @@ true - - - - - diff --git a/src/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps.csproj b/src/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps.csproj new file mode 100644 index 00000000..aa1079e5 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/src/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps/SwaApiEndpointAnnotation.cs b/src/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps/SwaApiEndpointAnnotation.cs new file mode 100644 index 00000000..fd48da1c --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps/SwaApiEndpointAnnotation.cs @@ -0,0 +1,8 @@ +using Aspire.Hosting.ApplicationModel; + +namespace CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps; + +public class SwaApiEndpointAnnotation(IResourceBuilder resource) : IResourceAnnotation +{ + public string Endpoint => resource.Resource.GetEndpoint("http").Url; +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps/SwaAppEndpointAnnotation.cs b/src/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps/SwaAppEndpointAnnotation.cs new file mode 100644 index 00000000..74cc2a0a --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps/SwaAppEndpointAnnotation.cs @@ -0,0 +1,8 @@ +using Aspire.Hosting.ApplicationModel; + +namespace CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps; + +public class SwaAppEndpointAnnotation(IResourceBuilder resource) : IResourceAnnotation +{ + public string Endpoint => resource.Resource.GetEndpoint("http").Url; +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps/SwaAppHostingExtension.cs b/src/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps/SwaAppHostingExtension.cs new file mode 100644 index 00000000..cc07ddab --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps/SwaAppHostingExtension.cs @@ -0,0 +1,60 @@ +using Aspire.Hosting; +using Aspire.Hosting.ApplicationModel; + +namespace CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps; + +public static class SwaAppHostingExtension +{ + /// + /// Adds a Static Web Apps emulator to the application. + /// + /// The to add the resource to. + /// The name of the resource. + /// The to configure the Java application." + /// A reference to the . + /// This resource will not be included in the published manifest. + public static IResourceBuilder AddSwaEmulator(this IDistributedApplicationBuilder builder, string name, int port = 4280) + { + var resource = new SwaResource(name, Environment.CurrentDirectory); + return builder.AddResource(resource) + .WithHttpEndpoint(isProxied: false, port: port) + .WithArgs(ctx => + { + ctx.Args.Add("start"); + + if (resource.TryGetAnnotationsOfType(out var appResource)) + { + ctx.Args.Add("--app-devserver-url"); + ctx.Args.Add(appResource.First().Endpoint); + } + + if (resource.TryGetAnnotationsOfType(out var apiResource)) + { + ctx.Args.Add("--api-devserver-url"); + ctx.Args.Add(apiResource.First().Endpoint); + } + + ctx.Args.Add("--port"); + ctx.Args.Add(port.ToString()); + }) + .ExcludeFromManifest(); + } + + /// + /// Registers the application resource with the Static Web Apps emulator. + /// + /// The to add the resource to. + /// The existing to use as the --app-devserver-url argument. + /// A reference to the . + public static IResourceBuilder WithAppResource(this IResourceBuilder builder, IResourceBuilder appResource) => + builder.WithAnnotation(new(appResource), ResourceAnnotationMutationBehavior.Replace); + + /// + /// Registers the API resource with the Static Web Apps emulator. + /// + /// The to add the resource to. + /// The existing to use as the --api-devserver-url argument. + /// A reference to the . + public static IResourceBuilder WithApiResource(this IResourceBuilder builder, IResourceBuilder apiResource) => + builder.WithAnnotation(new(apiResource), ResourceAnnotationMutationBehavior.Replace); +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps/SwaResource.cs b/src/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps/SwaResource.cs new file mode 100644 index 00000000..d531dd90 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps/SwaResource.cs @@ -0,0 +1,7 @@ +using Aspire.Hosting.ApplicationModel; + +namespace CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps; + +public class SwaResource(string name, string workingDirectory) : ExecutableResource(name, "swa", workingDirectory) +{ +} diff --git a/test/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps.Tests/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps.Tests.csproj b/test/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps.Tests/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps.Tests.csproj new file mode 100644 index 00000000..2ceac793 --- /dev/null +++ b/test/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps.Tests/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps.Tests.csproj @@ -0,0 +1,23 @@ + + + + false + true + + + + + + + + + + + + + + + + + + diff --git a/test/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps.Tests/ResourceCreationTests.cs b/test/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps.Tests/ResourceCreationTests.cs new file mode 100644 index 00000000..90067cfb --- /dev/null +++ b/test/CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps.Tests/ResourceCreationTests.cs @@ -0,0 +1,178 @@ +using Aspire.Hosting; +using Aspire.Hosting.ApplicationModel; +using Microsoft.Extensions.DependencyInjection; + +namespace CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps.Tests; + +public class ResourceCreationTests +{ + [Fact] + public void TargetPort_Defaults_to_4280() + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddSwaEmulator("swa"); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var resource = appModel.Resources.OfType().SingleOrDefault(); + + Assert.NotNull(resource); + + Assert.Equal("swa", resource.Name); + + var httpEndpoint = resource.GetEndpoint("http"); + Assert.Equal(4280, httpEndpoint.TargetPort); + } + + [Fact] + public void TargetPort_Can_Be_Overridden() + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddSwaEmulator("swa", port: 1234); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var resource = appModel.Resources.OfType().SingleOrDefault(); + + Assert.NotNull(resource); + + Assert.Equal("swa", resource.Name); + + var httpEndpoint = resource.GetEndpoint("http"); + Assert.Equal(1234, httpEndpoint.TargetPort); + } + + [Fact] + public void AppResource_Can_Be_Set() + { + var builder = DistributedApplication.CreateBuilder(); + + var appResource = builder + .AddContainer("app", "test/container") // container image doesn't need to be valid as we aren't actually running it + .WithHttpEndpoint(); + + builder.AddSwaEmulator("swa") + .WithAppResource(appResource); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var resource = appModel.Resources.OfType().SingleOrDefault(); + + Assert.NotNull(resource); + + Assert.Equal("swa", resource.Name); + + var result = resource.TryGetAnnotationsOfType(out var appResources); + + Assert.True(result); + Assert.NotNull(appResources); + Assert.Single(appResources); + } + + [Fact] + public void ApiResource_Can_Be_Set() + { + var builder = DistributedApplication.CreateBuilder(); + + var apiResource = builder + .AddContainer("api", "test/container") // container image doesn't need to be valid as we aren't actually running it + .WithHttpEndpoint(); + + builder.AddSwaEmulator("swa") + .WithApiResource(apiResource); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var resource = appModel.Resources.OfType().SingleOrDefault(); + + Assert.NotNull(resource); + + Assert.Equal("swa", resource.Name); + + var result = resource.TryGetAnnotationsOfType(out var apiResources); + + Assert.True(result); + Assert.NotNull(apiResources); + + Assert.Single(apiResources); + } + + [Fact] + public void Start_Will_Be_An_Arg() + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddSwaEmulator("swa"); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var resource = appModel.Resources.OfType().SingleOrDefault(); + + Assert.NotNull(resource); + + Assert.Equal("swa", resource.Name); + + var result = resource.TryGetAnnotationsOfType(out var annotations); + + Assert.True(result); + Assert.NotNull(annotations); + + Assert.Single(annotations); + + var annotation = annotations.Single(); + + List args = []; + var ctx = new CommandLineArgsCallbackContext(args); + + annotation.Callback(ctx); + + Assert.Contains("start", args); + } + + [Fact] + public void Port_Will_Be_An_Arg() + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddSwaEmulator("swa"); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var resource = appModel.Resources.OfType().SingleOrDefault(); + + Assert.NotNull(resource); + + Assert.Equal("swa", resource.Name); + + var result = resource.TryGetAnnotationsOfType(out var annotations); + + Assert.True(result); + Assert.NotNull(annotations); + + Assert.Single(annotations); + + var annotation = annotations.Single(); + + List args = []; + var ctx = new CommandLineArgsCallbackContext(args); + + annotation.Callback(ctx); + + Assert.Contains("--port", args); + Assert.Contains("4280", args); + } +} \ No newline at end of file diff --git a/test/CommunityToolkit.Aspire.Java.Hosting.EndToEndTests/CommunityToolkit.Aspire.Java.Hosting.EndToEndTests.csproj b/test/CommunityToolkit.Aspire.Java.Hosting.EndToEndTests/CommunityToolkit.Aspire.Java.Hosting.EndToEndTests.csproj index d42a6d7b..e3e8c14f 100644 --- a/test/CommunityToolkit.Aspire.Java.Hosting.EndToEndTests/CommunityToolkit.Aspire.Java.Hosting.EndToEndTests.csproj +++ b/test/CommunityToolkit.Aspire.Java.Hosting.EndToEndTests/CommunityToolkit.Aspire.Java.Hosting.EndToEndTests.csproj @@ -6,7 +6,6 @@ - diff --git a/test/Directory.Build.props b/test/Directory.Build.props new file mode 100644 index 00000000..979fee3b --- /dev/null +++ b/test/Directory.Build.props @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file