From 3dee0ad65920a31b725ac3c77cdaa7a62da14dae Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Mon, 26 Feb 2024 23:47:51 +1100 Subject: [PATCH 01/13] WIP --- Aspire.sln | 19 ++- Directory.Packages.props | 6 +- NuGet.config | 8 +- .../CdkSample.ApiService.csproj | 13 ++ .../CdkSample.ApiService.http | 6 + .../cdk/CdkSample.ApiService/Program.cs | 15 +++ .../Properties/launchSettings.json | 14 +++ .../appsettings.Development.json | 8 ++ .../cdk/CdkSample.ApiService/appsettings.json | 9 ++ .../CdkSample.AppHost.csproj | 23 ++++ .../CdkSample.AppHost/Directory.Build.props | 8 ++ .../CdkSample.AppHost/Directory.Build.targets | 9 ++ playground/cdk/CdkSample.AppHost/Program.cs | 19 +++ .../Properties/launchSettings.json | 29 +++++ .../appsettings.Development.json | 8 ++ .../cdk/CdkSample.AppHost/appsettings.json | 12 ++ .../Aspire.Hosting.Azure.csproj | 1 + src/Aspire.Hosting.Azure/CdkResource.cs | 112 ++++++++++++++++++ .../DistributedApplicationTests.cs | 2 +- 19 files changed, 316 insertions(+), 5 deletions(-) create mode 100644 playground/cdk/CdkSample.ApiService/CdkSample.ApiService.csproj create mode 100644 playground/cdk/CdkSample.ApiService/CdkSample.ApiService.http create mode 100644 playground/cdk/CdkSample.ApiService/Program.cs create mode 100644 playground/cdk/CdkSample.ApiService/Properties/launchSettings.json create mode 100644 playground/cdk/CdkSample.ApiService/appsettings.Development.json create mode 100644 playground/cdk/CdkSample.ApiService/appsettings.json create mode 100644 playground/cdk/CdkSample.AppHost/CdkSample.AppHost.csproj create mode 100644 playground/cdk/CdkSample.AppHost/Directory.Build.props create mode 100644 playground/cdk/CdkSample.AppHost/Directory.Build.targets create mode 100644 playground/cdk/CdkSample.AppHost/Program.cs create mode 100644 playground/cdk/CdkSample.AppHost/Properties/launchSettings.json create mode 100644 playground/cdk/CdkSample.AppHost/appsettings.Development.json create mode 100644 playground/cdk/CdkSample.AppHost/appsettings.json create mode 100644 src/Aspire.Hosting.Azure/CdkResource.cs diff --git a/Aspire.sln b/Aspire.sln index 1a2f61d857..47e9d113ac 100644 --- a/Aspire.sln +++ b/Aspire.sln @@ -267,7 +267,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DatabaseMigration.AppHost", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DatabaseMigration.MigrationService", "playground\DatabaseMigration\DatabaseMigration.MigrationService\DatabaseMigration.MigrationService.csproj", "{E7DB736B-C316-460E-A609-2200E58BF0C2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DatabaseMigration.ApiModel", "playground\DatabaseMigration\DatabaseMigration.ApiModel\DatabaseMigration.ApiModel.csproj", "{C15F3F13-AB63-47CF-AAFE-D319F02E7B33}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DatabaseMigration.ApiModel", "playground\DatabaseMigration\DatabaseMigration.ApiModel\DatabaseMigration.ApiModel.csproj", "{C15F3F13-AB63-47CF-AAFE-D319F02E7B33}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "signalr", "signalr", "{E6985EED-47E3-4EAC-8222-074E5410CEDC}" EndProject @@ -281,6 +281,12 @@ 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("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "cdk", "cdk", "{C3F48531-87D9-4E52-90AC-715A3E55751A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CdkSample.AppHost", "playground\cdk\CdkSample.AppHost\CdkSample.AppHost.csproj", "{A357411A-5909-4A49-9519-12A935F84395}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CdkSample.ApiService", "playground\cdk\CdkSample.ApiService\CdkSample.ApiService.csproj", "{4601F5A2-E445-41B2-9C1F-2CE016642E62}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -735,6 +741,14 @@ 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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -869,6 +883,9 @@ Global {9C30FFD6-2262-45E7-B010-24B30E0433C2} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0} {51654CD7-2E05-4664-B2EB-95308A300609} = {9C30FFD6-2262-45E7-B010-24B30E0433C2} {0244203D-7491-4414-9C88-10BFED9C5B2D} = {9C30FFD6-2262-45E7-B010-24B30E0433C2} + {C3F48531-87D9-4E52-90AC-715A3E55751A} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0} + {A357411A-5909-4A49-9519-12A935F84395} = {C3F48531-87D9-4E52-90AC-715A3E55751A} + {4601F5A2-E445-41B2-9C1F-2CE016642E62} = {C3F48531-87D9-4E52-90AC-715A3E55751A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6DCEDFEC-988E-4CB3-B45B-191EB5086E0C} diff --git a/Directory.Packages.props b/Directory.Packages.props index c1070fe622..c8d49cf2fa 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -16,9 +16,11 @@ + - - + + + diff --git a/NuGet.config b/NuGet.config index e8f26f0e63..960384ec28 100644 --- a/NuGet.config +++ b/NuGet.config @@ -17,9 +17,15 @@ + - + + + + + + diff --git a/playground/cdk/CdkSample.ApiService/CdkSample.ApiService.csproj b/playground/cdk/CdkSample.ApiService/CdkSample.ApiService.csproj new file mode 100644 index 0000000000..88b35ed33a --- /dev/null +++ b/playground/cdk/CdkSample.ApiService/CdkSample.ApiService.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/playground/cdk/CdkSample.ApiService/CdkSample.ApiService.http b/playground/cdk/CdkSample.ApiService/CdkSample.ApiService.http new file mode 100644 index 0000000000..59fcd09ca5 --- /dev/null +++ b/playground/cdk/CdkSample.ApiService/CdkSample.ApiService.http @@ -0,0 +1,6 @@ +@CosmosEndToEnd.ApiService_HostAddress = http://localhost:5193 + +GET {{SqlServerEndToEnd.ApiService_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/playground/cdk/CdkSample.ApiService/Program.cs b/playground/cdk/CdkSample.ApiService/Program.cs new file mode 100644 index 0000000000..d2eca60016 --- /dev/null +++ b/playground/cdk/CdkSample.ApiService/Program.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. + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +var app = builder.Build(); + +app.MapGet("/", () => +{ + return "Hello, Azure!"; +}); + +app.Run(); diff --git a/playground/cdk/CdkSample.ApiService/Properties/launchSettings.json b/playground/cdk/CdkSample.ApiService/Properties/launchSettings.json new file mode 100644 index 0000000000..f7bf310e7a --- /dev/null +++ b/playground/cdk/CdkSample.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/cdk/CdkSample.ApiService/appsettings.Development.json b/playground/cdk/CdkSample.ApiService/appsettings.Development.json new file mode 100644 index 0000000000..0c208ae918 --- /dev/null +++ b/playground/cdk/CdkSample.ApiService/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/playground/cdk/CdkSample.ApiService/appsettings.json b/playground/cdk/CdkSample.ApiService/appsettings.json new file mode 100644 index 0000000000..10f68b8c8b --- /dev/null +++ b/playground/cdk/CdkSample.ApiService/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/playground/cdk/CdkSample.AppHost/CdkSample.AppHost.csproj b/playground/cdk/CdkSample.AppHost/CdkSample.AppHost.csproj new file mode 100644 index 0000000000..1b89b5d187 --- /dev/null +++ b/playground/cdk/CdkSample.AppHost/CdkSample.AppHost.csproj @@ -0,0 +1,23 @@ + + + + Exe + net8.0 + enable + enable + true + dafae173-3ac0-4100-8cab-602852cb28dd + + + + + + + + + + + + + + diff --git a/playground/cdk/CdkSample.AppHost/Directory.Build.props b/playground/cdk/CdkSample.AppHost/Directory.Build.props new file mode 100644 index 0000000000..b9b39c05e8 --- /dev/null +++ b/playground/cdk/CdkSample.AppHost/Directory.Build.props @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/playground/cdk/CdkSample.AppHost/Directory.Build.targets b/playground/cdk/CdkSample.AppHost/Directory.Build.targets new file mode 100644 index 0000000000..b7ba77268f --- /dev/null +++ b/playground/cdk/CdkSample.AppHost/Directory.Build.targets @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/playground/cdk/CdkSample.AppHost/Program.cs b/playground/cdk/CdkSample.AppHost/Program.cs new file mode 100644 index 0000000000..c04965eb76 --- /dev/null +++ b/playground/cdk/CdkSample.AppHost/Program.cs @@ -0,0 +1,19 @@ +// 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); + +// This is just an empty resource. Not referenced, but +// just part of the .NET Aspire application model. +builder.AddCdkResource("empty"); + +builder.AddProject("api"); + +// 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/cdk/CdkSample.AppHost/Properties/launchSettings.json b/playground/cdk/CdkSample.AppHost/Properties/launchSettings.json new file mode 100644 index 0000000000..32514f6378 --- /dev/null +++ b/playground/cdk/CdkSample.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/cdk/CdkSample.AppHost/appsettings.Development.json b/playground/cdk/CdkSample.AppHost/appsettings.Development.json new file mode 100644 index 0000000000..0c208ae918 --- /dev/null +++ b/playground/cdk/CdkSample.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/playground/cdk/CdkSample.AppHost/appsettings.json b/playground/cdk/CdkSample.AppHost/appsettings.json new file mode 100644 index 0000000000..a40f92dadf --- /dev/null +++ b/playground/cdk/CdkSample.AppHost/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + }, + "Parameters": { + "val": "value from config" + } +} diff --git a/src/Aspire.Hosting.Azure/Aspire.Hosting.Azure.csproj b/src/Aspire.Hosting.Azure/Aspire.Hosting.Azure.csproj index a38080183b..898ddf5116 100644 --- a/src/Aspire.Hosting.Azure/Aspire.Hosting.Azure.csproj +++ b/src/Aspire.Hosting.Azure/Aspire.Hosting.Azure.csproj @@ -18,6 +18,7 @@ + diff --git a/src/Aspire.Hosting.Azure/CdkResource.cs b/src/Aspire.Hosting.Azure/CdkResource.cs new file mode 100644 index 0000000000..b60f6949a4 --- /dev/null +++ b/src/Aspire.Hosting.Azure/CdkResource.cs @@ -0,0 +1,112 @@ +// 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.Lifecycle; +using Azure.Provisioning; +using Azure.Provisioning.ResourceManager; +using AspireResource = Aspire.Hosting.ApplicationModel.Resource; + +namespace Aspire.Hosting; + +/// +/// An Aspire resource which is also a CDK construct. +/// +/// +public class CdkResource(string name) : AspireResource(name) +{ + +} + +/// +/// TODO: +/// +/// +public class AzureStorageCdkResource(string name) : CdkResource(name) +{ + +} + +/// +/// Extensions for working with CDK resources in the .NET Aspire application model. +/// +public static class CdkResourceExtensions +{ + /// + /// Adds a CDK resource to the application model. + /// + /// The distributed application builder. + /// The name of the resource being added. + /// A callback used to configure the resource. + /// + public static IResourceBuilder AddCdkResource(this IDistributedApplicationBuilder builder, string name, Action? callback = null) + { + builder.Services.TryAddLifecycleHook(); + + callback ??= static (construct) => { }; + + var resource = new CdkResource(name); + return builder.AddResource(resource) + .WithAnnotation(new AspireResourceConstructCallbackAnnotation(callback), ResourceAnnotationMutationBehavior.Replace); + } +} + +/// +/// An Azure.Provisioning construct which maps to an Aspire Resource. +/// +public class AspireResourceConstruct(IConstruct? scope, string name, ConstructScope constructScope = ConstructScope.ResourceGroup, Guid? tenantId = null, Guid? subscriptionId = null, string? envName = null, ResourceGroup? resourceGroup = null) : Construct(scope, name, constructScope, tenantId, subscriptionId, envName, resourceGroup) +{ +} + +internal class AspireInfrastructureConstruct(string envName) : Infrastructure(envName: envName) +{ +} + +/// +/// TODO: Doc comments. +/// +public class AspireResourceConstructCallbackAnnotation(Action callback) : IResourceAnnotation +{ + /// + /// TODO: Doc comments. + /// + public Action Callback = callback; +} + +internal class CdkLifecycleHook(DistributedApplicationExecutionContext executionContext) : IDistributedApplicationLifecycleHook +{ + + public Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default) + { + if (executionContext.IsRunMode) + { + return Task.CompletedTask; + } + + var cdkResources = appModel.Resources.OfType(); + var infrastructure = new AspireInfrastructureConstruct("test"); + + // Firstly suppress individual publishing of all CDK resources to the manifest. + foreach (var resource in cdkResources) + { + if (resource.Annotations.OfType().SingleOrDefault() is { } existingAnnotation) + { + resource.Annotations.Remove(existingAnnotation); + } + + resource.Annotations.Add(ManifestPublishingCallbackAnnotation.Ignore); + + var resourceConstruct = new AspireResourceConstruct(infrastructure, resource.Name); + var callbackAnnotation = resource.Annotations.OfType().Single(); + callbackAnnotation.Callback(resourceConstruct); + } + + var path = Path.GetTempFileName(); + infrastructure.Build(path); + // TODO: Insert code that spits out a Bicep resource to the manifest and generates + // the Bicep based on the CDK resources? + + return Task.CompletedTask; + } + +} diff --git a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs index 8897b3c160..6d21f5381f 100644 --- a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs +++ b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs @@ -469,7 +469,7 @@ public async Task KubernetesHasResourceNameForContainersAndExes() } } - await foreach(var resource in s.WatchAsync(cancellationToken: token)) + await foreach (var resource in s.WatchAsync(cancellationToken: token)) { Assert.True(resource.Item2.Metadata.Annotations.TryGetValue(Executable.ResourceNameAnnotation, out var value)); if (expectedExeResources.Contains(value)) From 0370a85e5758b7872059b9251e9adc6b2aef3758 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Tue, 27 Feb 2024 13:15:45 +1100 Subject: [PATCH 02/13] WIP. --- Directory.Packages.props | 8 +- playground/cdk/CdkSample.AppHost/Program.cs | 12 +- .../CdkSample.AppHost/aspire-manifest.json | 3 + .../AzureConstructResource.cs | 78 ++++++++++++ src/Aspire.Hosting.Azure/CdkResource.cs | 112 ------------------ 5 files changed, 96 insertions(+), 117 deletions(-) create mode 100644 playground/cdk/CdkSample.AppHost/aspire-manifest.json create mode 100644 src/Aspire.Hosting.Azure/AzureConstructResource.cs delete mode 100644 src/Aspire.Hosting.Azure/CdkResource.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index faf8c96467..03703f4072 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,11 +17,11 @@ - + - - - + + + diff --git a/playground/cdk/CdkSample.AppHost/Program.cs b/playground/cdk/CdkSample.AppHost/Program.cs index c04965eb76..9274a37240 100644 --- a/playground/cdk/CdkSample.AppHost/Program.cs +++ b/playground/cdk/CdkSample.AppHost/Program.cs @@ -1,11 +1,21 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Azure.Provisioning.Storage; +using Azure.ResourceManager.Storage.Models; + var builder = DistributedApplication.CreateBuilder(args); // This is just an empty resource. Not referenced, but // just part of the .NET Aspire application model. -builder.AddCdkResource("empty"); +builder.AddAzureConstruct("empty", (construct) => +{ + construct.AddStorageAccount( + name: "bob", + kind: StorageKind.BlobStorage, + sku: StorageSkuName.StandardLrs + ); +}); builder.AddProject("api"); diff --git a/playground/cdk/CdkSample.AppHost/aspire-manifest.json b/playground/cdk/CdkSample.AppHost/aspire-manifest.json new file mode 100644 index 0000000000..83b81e990c --- /dev/null +++ b/playground/cdk/CdkSample.AppHost/aspire-manifest.json @@ -0,0 +1,3 @@ +{ + "resources": { + "empty": { \ No newline at end of file diff --git a/src/Aspire.Hosting.Azure/AzureConstructResource.cs b/src/Aspire.Hosting.Azure/AzureConstructResource.cs new file mode 100644 index 0000000000..73cf9b500c --- /dev/null +++ b/src/Aspire.Hosting.Azure/AzureConstructResource.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 Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Azure; +using Aspire.Hosting.Publishing; +using Azure.Provisioning; + +namespace Aspire.Hosting; + +/// +/// An Aspire resource which is also a CDK construct. +/// +/// +/// +public class AzureConstructResource(string name, Func createConstruct) : AzureBicepResource(name, templateFile: $"{name}.generated.bicep") +{ + /// + /// TODO: + /// + public Func CreateConstruct { get; } = createConstruct; + + /// + public override void WriteToManifest(ManifestPublishingContext context) + { + var path = context.GetManifestRelativePath($"{Name}.generated.bicep"); + var construct = CreateConstruct(); + construct.Build(path); + + base.WriteToManifest(context); + } +} + +/// +/// Extensions for working with CDK resources in the .NET Aspire application model. +/// +public static class CdkResourceExtensions +{ + /// + /// Adds a CDK resource to the application model. + /// + /// The distributed application builder. + /// The name of the resource being added. + /// A callback used to configure the resource. + /// + public static IResourceBuilder AddAzureConstruct(this IDistributedApplicationBuilder builder, string name, Action configureConstruct) + { + var createConstruct = () => + { + var resourceConstruct = new AspireResourceConstruct(builder.Environment.EnvironmentName); + configureConstruct(resourceConstruct); + return resourceConstruct; + }; + return builder.AddAzureConstruct(name, createConstruct); + } + + /// + /// Adds a CDK resource to the application model. + /// + /// The distributed application builder. + /// The name of the resource being added. + /// + /// + public static IResourceBuilder AddAzureConstruct(this IDistributedApplicationBuilder builder, string name, Func createConstruct) + { + var resource = new AzureConstructResource(name, createConstruct); + return builder.AddResource(resource) + .WithManifestPublishingCallback(resource.WriteToManifest); + } +} + +/// +/// TODO: +/// +/// +public class AspireResourceConstruct(string envName) : Infrastructure(ConstructScope.ResourceGroup, envName: envName) +{ +} diff --git a/src/Aspire.Hosting.Azure/CdkResource.cs b/src/Aspire.Hosting.Azure/CdkResource.cs deleted file mode 100644 index b60f6949a4..0000000000 --- a/src/Aspire.Hosting.Azure/CdkResource.cs +++ /dev/null @@ -1,112 +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.Lifecycle; -using Azure.Provisioning; -using Azure.Provisioning.ResourceManager; -using AspireResource = Aspire.Hosting.ApplicationModel.Resource; - -namespace Aspire.Hosting; - -/// -/// An Aspire resource which is also a CDK construct. -/// -/// -public class CdkResource(string name) : AspireResource(name) -{ - -} - -/// -/// TODO: -/// -/// -public class AzureStorageCdkResource(string name) : CdkResource(name) -{ - -} - -/// -/// Extensions for working with CDK resources in the .NET Aspire application model. -/// -public static class CdkResourceExtensions -{ - /// - /// Adds a CDK resource to the application model. - /// - /// The distributed application builder. - /// The name of the resource being added. - /// A callback used to configure the resource. - /// - public static IResourceBuilder AddCdkResource(this IDistributedApplicationBuilder builder, string name, Action? callback = null) - { - builder.Services.TryAddLifecycleHook(); - - callback ??= static (construct) => { }; - - var resource = new CdkResource(name); - return builder.AddResource(resource) - .WithAnnotation(new AspireResourceConstructCallbackAnnotation(callback), ResourceAnnotationMutationBehavior.Replace); - } -} - -/// -/// An Azure.Provisioning construct which maps to an Aspire Resource. -/// -public class AspireResourceConstruct(IConstruct? scope, string name, ConstructScope constructScope = ConstructScope.ResourceGroup, Guid? tenantId = null, Guid? subscriptionId = null, string? envName = null, ResourceGroup? resourceGroup = null) : Construct(scope, name, constructScope, tenantId, subscriptionId, envName, resourceGroup) -{ -} - -internal class AspireInfrastructureConstruct(string envName) : Infrastructure(envName: envName) -{ -} - -/// -/// TODO: Doc comments. -/// -public class AspireResourceConstructCallbackAnnotation(Action callback) : IResourceAnnotation -{ - /// - /// TODO: Doc comments. - /// - public Action Callback = callback; -} - -internal class CdkLifecycleHook(DistributedApplicationExecutionContext executionContext) : IDistributedApplicationLifecycleHook -{ - - public Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default) - { - if (executionContext.IsRunMode) - { - return Task.CompletedTask; - } - - var cdkResources = appModel.Resources.OfType(); - var infrastructure = new AspireInfrastructureConstruct("test"); - - // Firstly suppress individual publishing of all CDK resources to the manifest. - foreach (var resource in cdkResources) - { - if (resource.Annotations.OfType().SingleOrDefault() is { } existingAnnotation) - { - resource.Annotations.Remove(existingAnnotation); - } - - resource.Annotations.Add(ManifestPublishingCallbackAnnotation.Ignore); - - var resourceConstruct = new AspireResourceConstruct(infrastructure, resource.Name); - var callbackAnnotation = resource.Annotations.OfType().Single(); - callbackAnnotation.Callback(resourceConstruct); - } - - var path = Path.GetTempFileName(); - infrastructure.Build(path); - // TODO: Insert code that spits out a Bicep resource to the manifest and generates - // the Bicep based on the CDK resources? - - return Task.CompletedTask; - } - -} From 996efd5078d1c2f650602ab14a6276f5a602d52e Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Tue, 27 Feb 2024 15:09:44 +1100 Subject: [PATCH 03/13] Hacks to workaround needing tenant and sub. --- .../CdkSample.AppHost.csproj | 2 +- .../CdkSample.AppHost/aspire-manifest.json | 27 ++++++++++++- .../cdk/CdkSample.AppHost/empty.module.bicep | 11 ++++++ .../AzureBicepResource.cs | 2 +- .../AzureConstructResource.cs | 38 ++++++++++++++----- 5 files changed, 67 insertions(+), 13 deletions(-) create mode 100644 playground/cdk/CdkSample.AppHost/empty.module.bicep diff --git a/playground/cdk/CdkSample.AppHost/CdkSample.AppHost.csproj b/playground/cdk/CdkSample.AppHost/CdkSample.AppHost.csproj index 1b89b5d187..40ab6d9383 100644 --- a/playground/cdk/CdkSample.AppHost/CdkSample.AppHost.csproj +++ b/playground/cdk/CdkSample.AppHost/CdkSample.AppHost.csproj @@ -6,7 +6,7 @@ enable enable true - dafae173-3ac0-4100-8cab-602852cb28dd + 44b9bf37-1892-4852-8b5f-153e3ac5d24c diff --git a/playground/cdk/CdkSample.AppHost/aspire-manifest.json b/playground/cdk/CdkSample.AppHost/aspire-manifest.json index 83b81e990c..9cb9db1594 100644 --- a/playground/cdk/CdkSample.AppHost/aspire-manifest.json +++ b/playground/cdk/CdkSample.AppHost/aspire-manifest.json @@ -1,3 +1,28 @@ { "resources": { - "empty": { \ No newline at end of file + "empty": { + "type": "azure.bicep.v0", + "path": "empty.module.bicep" + }, + "api": { + "type": "project.v0", + "path": "../CdkSample.ApiService/CdkSample.ApiService.csproj", + "env": { + "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true", + "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true" + }, + "bindings": { + "http": { + "scheme": "http", + "protocol": "tcp", + "transport": "http" + }, + "https": { + "scheme": "https", + "protocol": "tcp", + "transport": "http" + } + } + } + } +} \ No newline at end of file diff --git a/playground/cdk/CdkSample.AppHost/empty.module.bicep b/playground/cdk/CdkSample.AppHost/empty.module.bicep new file mode 100644 index 0000000000..6027ace21e --- /dev/null +++ b/playground/cdk/CdkSample.AppHost/empty.module.bicep @@ -0,0 +1,11 @@ + +resource storageAccount_9CYYlnXQ8 'Microsoft.Storage/storageAccounts@2022-09-01' = { + name: 'bob35db3c6bf6254fcd86c21' + location: 'westus' + sku: { + name: 'Standard_LRS' + } + kind: 'StorageV2' + properties: { + } +} diff --git a/src/Aspire.Hosting.Azure/AzureBicepResource.cs b/src/Aspire.Hosting.Azure/AzureBicepResource.cs index 6c549ca0f3..6552f5ad33 100644 --- a/src/Aspire.Hosting.Azure/AzureBicepResource.cs +++ b/src/Aspire.Hosting.Azure/AzureBicepResource.cs @@ -22,7 +22,7 @@ public class AzureBicepResource(string name, string? templateFile = null, string { internal string? TemplateFile { get; } = templateFile; - internal string? TemplateString { get; } = templateString; + internal string? TemplateString { get; set; } = templateString; internal string? TemplateResourceName { get; } = templateResouceName; diff --git a/src/Aspire.Hosting.Azure/AzureConstructResource.cs b/src/Aspire.Hosting.Azure/AzureConstructResource.cs index 73cf9b500c..4dee26854c 100644 --- a/src/Aspire.Hosting.Azure/AzureConstructResource.cs +++ b/src/Aspire.Hosting.Azure/AzureConstructResource.cs @@ -12,20 +12,29 @@ namespace Aspire.Hosting; /// An Aspire resource which is also a CDK construct. /// /// +/// /// -public class AzureConstructResource(string name, Func createConstruct) : AzureBicepResource(name, templateFile: $"{name}.generated.bicep") +public class AzureConstructResource(string name, Infrastructure infrastructure, Func createConstruct) : AzureBicepResource(name, templateFile: $"{name}.module.bicep") { /// /// TODO: /// - public Func CreateConstruct { get; } = createConstruct; + public Func CreateConstruct { get; } = createConstruct; /// public override void WriteToManifest(ManifestPublishingContext context) { - var path = context.GetManifestRelativePath($"{Name}.generated.bicep"); - var construct = CreateConstruct(); - construct.Build(path); + // HACK: Using CDK to generate files but then copying just the module + // to where it needs to be. + var generationPath = Directory.CreateTempSubdirectory("aspire").FullName; + CreateConstruct(infrastructure); + infrastructure.Build(generationPath); + + var moduleSourcePath = Path.Combine(generationPath, "resources", "rg_temp_module", "rg_temp_module.bicep"); + var moduleDestinationPath = context.GetManifestRelativePath(TemplateFile); + File.Copy(moduleSourcePath, moduleDestinationPath!, true); + + Directory.Delete(generationPath, true); base.WriteToManifest(context); } @@ -45,9 +54,9 @@ public static class CdkResourceExtensions /// public static IResourceBuilder AddAzureConstruct(this IDistributedApplicationBuilder builder, string name, Action configureConstruct) { - var createConstruct = () => + var createConstruct = (IConstruct subscriptionConstruct) => { - var resourceConstruct = new AspireResourceConstruct(builder.Environment.EnvironmentName); + var resourceConstruct = new AspireResourceConstruct(subscriptionConstruct, name, builder.Environment.EnvironmentName); configureConstruct(resourceConstruct); return resourceConstruct; }; @@ -61,9 +70,12 @@ public static IResourceBuilder AddAzureConstruct(this ID /// The name of the resource being added. /// /// - public static IResourceBuilder AddAzureConstruct(this IDistributedApplicationBuilder builder, string name, Func createConstruct) + public static IResourceBuilder AddAzureConstruct(this IDistributedApplicationBuilder builder, string name, Func createConstruct) { - var resource = new AzureConstructResource(name, createConstruct); + // HACK: We shouldn't need this. + var infrastructure = new DummyInfrastructure(Guid.NewGuid(), Guid.NewGuid(), "temp"); + + var resource = new AzureConstructResource(name, infrastructure, createConstruct); return builder.AddResource(resource) .WithManifestPublishingCallback(resource.WriteToManifest); } @@ -72,7 +84,13 @@ public static IResourceBuilder AddAzureConstruct(this ID /// /// TODO: /// +/// +/// /// -public class AspireResourceConstruct(string envName) : Infrastructure(ConstructScope.ResourceGroup, envName: envName) +public class AspireResourceConstruct(IConstruct scope, string resourceName, string envName) : Construct(scope, resourceName, ConstructScope.ResourceGroup, tenantId: Guid.NewGuid(), subscriptionId: Guid.NewGuid(), envName: envName) +{ +} + +internal class DummyInfrastructure(Guid tenantId, Guid subscriptionId, string envName) : Infrastructure(tenantId: tenantId, subscriptionId: subscriptionId, envName: envName) { } From f0f2a244bd3fe68392516b735bbfce01466df399 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Tue, 27 Feb 2024 18:49:50 +1100 Subject: [PATCH 04/13] WIP: Deployable via AZD. --- playground/cdk/CdkSample.AppHost/Program.cs | 28 +++++++++++++------ .../CdkSample.AppHost/aspire-manifest.json | 14 +++++++++- .../cdk/CdkSample.AppHost/empty.module.bicep | 16 ++++++++--- .../AzureConstructResource.cs | 24 +++++++++++----- 4 files changed, 61 insertions(+), 21 deletions(-) diff --git a/playground/cdk/CdkSample.AppHost/Program.cs b/playground/cdk/CdkSample.AppHost/Program.cs index 9274a37240..c1bdd2a9bc 100644 --- a/playground/cdk/CdkSample.AppHost/Program.cs +++ b/playground/cdk/CdkSample.AppHost/Program.cs @@ -6,18 +6,28 @@ var builder = DistributedApplication.CreateBuilder(args); +var sku = builder.AddParameter("storagesku"); + // This is just an empty resource. Not referenced, but // just part of the .NET Aspire application model. -builder.AddAzureConstruct("empty", (construct) => +var storage = builder.AddAzureConstruct("empty", (resource, construct) => { - construct.AddStorageAccount( - name: "bob", - kind: StorageKind.BlobStorage, - sku: StorageSkuName.StandardLrs - ); -}); - -builder.AddProject("api"); + var parameters = construct.GetParameters().ToDictionary(p => p.Name); + + var account = construct.AddStorageAccount( + name: "bob", + kind: StorageKind.BlobStorage, + sku: StorageSkuName.StandardLrs + ); + + account.AssignParameter(a => a.Sku.Name, parameters["storagesku"]); + account.AssignParameter(a => a.Location, parameters["location"]); + + account.AddOutput(data => data.PrimaryEndpoints.TableUri, "tableUri", isSecure: true); +}).WithParameter("storagesku", sku); + +builder.AddProject("api") + .WithEnvironment("TABLE_URI", storage.GetOutput("tableUri")); // 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 9cb9db1594..8f6d24577a 100644 --- a/playground/cdk/CdkSample.AppHost/aspire-manifest.json +++ b/playground/cdk/CdkSample.AppHost/aspire-manifest.json @@ -1,8 +1,20 @@ { "resources": { + "storagesku": { + "type": "parameter.v0", + "value": "{storagesku.inputs.value}", + "inputs": { + "value": { + "type": "string" + } + } + }, "empty": { "type": "azure.bicep.v0", - "path": "empty.module.bicep" + "path": "empty.module.bicep", + "params": { + "storagesku": "{storagesku.value}" + } }, "api": { "type": "project.v0", diff --git a/playground/cdk/CdkSample.AppHost/empty.module.bicep b/playground/cdk/CdkSample.AppHost/empty.module.bicep index 6027ace21e..8742dd29e0 100644 --- a/playground/cdk/CdkSample.AppHost/empty.module.bicep +++ b/playground/cdk/CdkSample.AppHost/empty.module.bicep @@ -1,11 +1,19 @@ +@description('') +param storagesku string -resource storageAccount_9CYYlnXQ8 'Microsoft.Storage/storageAccounts@2022-09-01' = { - name: 'bob35db3c6bf6254fcd86c21' - location: 'westus' +@description('') +param location string = 'West US 3' + + +resource storageAccount_9e2cRkGOF 'Microsoft.Storage/storageAccounts@2022-09-01' = { + name: 'bob99e053564f774257ad1d5' + location: location sku: { - name: 'Standard_LRS' + name: storagesku } kind: 'StorageV2' properties: { } } + +output tableUri string = storageAccount_9e2cRkGOF.properties.primaryEndpoints.table diff --git a/src/Aspire.Hosting.Azure/AzureConstructResource.cs b/src/Aspire.Hosting.Azure/AzureConstructResource.cs index 4dee26854c..54a84e0b3d 100644 --- a/src/Aspire.Hosting.Azure/AzureConstructResource.cs +++ b/src/Aspire.Hosting.Azure/AzureConstructResource.cs @@ -14,12 +14,12 @@ namespace Aspire.Hosting; /// /// /// -public class AzureConstructResource(string name, Infrastructure infrastructure, Func createConstruct) : AzureBicepResource(name, templateFile: $"{name}.module.bicep") +public class AzureConstructResource(string name, Infrastructure infrastructure, Func createConstruct) : AzureBicepResource(name, templateFile: $"{name}.module.bicep") { /// /// TODO: /// - public Func CreateConstruct { get; } = createConstruct; + public Func CreateConstruct { get; } = createConstruct; /// public override void WriteToManifest(ManifestPublishingContext context) @@ -27,7 +27,7 @@ public override void WriteToManifest(ManifestPublishingContext context) // HACK: Using CDK to generate files but then copying just the module // to where it needs to be. var generationPath = Directory.CreateTempSubdirectory("aspire").FullName; - CreateConstruct(infrastructure); + CreateConstruct(this, infrastructure); infrastructure.Build(generationPath); var moduleSourcePath = Path.Combine(generationPath, "resources", "rg_temp_module", "rg_temp_module.bicep"); @@ -52,12 +52,22 @@ public static class CdkResourceExtensions /// The name of the resource being added. /// A callback used to configure the resource. /// - public static IResourceBuilder AddAzureConstruct(this IDistributedApplicationBuilder builder, string name, Action configureConstruct) + public static IResourceBuilder AddAzureConstruct(this IDistributedApplicationBuilder builder, string name, Action configureConstruct) { - var createConstruct = (IConstruct subscriptionConstruct) => + var createConstruct = (AzureConstructResource resource, IConstruct subscriptionConstruct) => { var resourceConstruct = new AspireResourceConstruct(subscriptionConstruct, name, builder.Environment.EnvironmentName); - configureConstruct(resourceConstruct); + + var locationParameter = new Parameter("location", defaultValue: "West US 3"); + resourceConstruct.AddParameter(locationParameter); + + foreach (var aspireParameter in resource.Parameters) + { + var constructParameter = new Parameter(aspireParameter.Key); + resourceConstruct.AddParameter(constructParameter); + } + + configureConstruct(resource, resourceConstruct); return resourceConstruct; }; return builder.AddAzureConstruct(name, createConstruct); @@ -70,7 +80,7 @@ public static IResourceBuilder AddAzureConstruct(this ID /// The name of the resource being added. /// /// - public static IResourceBuilder AddAzureConstruct(this IDistributedApplicationBuilder builder, string name, Func createConstruct) + public static IResourceBuilder AddAzureConstruct(this IDistributedApplicationBuilder builder, string name, Func createConstruct) { // HACK: We shouldn't need this. var infrastructure = new DummyInfrastructure(Guid.NewGuid(), Guid.NewGuid(), "temp"); From 0719a96b7ba6a40132cbc9913e061b008fba1954 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Tue, 27 Feb 2024 23:34:52 +1100 Subject: [PATCH 05/13] WIP. Chaining resources. --- playground/cdk/CdkSample.AppHost/Program.cs | 43 ++++-- .../CdkSample.AppHost/aspire-manifest.json | 16 ++- .../CdkSample.AppHost/construct1.module.bicep | 19 +++ .../CdkSample.AppHost/construct2.module.bicep | 29 ++++ .../cdk/CdkSample.AppHost/empty.module.bicep | 19 --- .../AzureConstructResource.cs | 129 +++++++++++++----- 6 files changed, 181 insertions(+), 74 deletions(-) create mode 100644 playground/cdk/CdkSample.AppHost/construct1.module.bicep create mode 100644 playground/cdk/CdkSample.AppHost/construct2.module.bicep delete mode 100644 playground/cdk/CdkSample.AppHost/empty.module.bicep diff --git a/playground/cdk/CdkSample.AppHost/Program.cs b/playground/cdk/CdkSample.AppHost/Program.cs index c1bdd2a9bc..73b2f0b234 100644 --- a/playground/cdk/CdkSample.AppHost/Program.cs +++ b/playground/cdk/CdkSample.AppHost/Program.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 Azure.Provisioning.KeyVaults; using Azure.Provisioning.Storage; using Azure.ResourceManager.Storage.Models; @@ -8,26 +9,35 @@ var sku = builder.AddParameter("storagesku"); -// This is just an empty resource. Not referenced, but -// just part of the .NET Aspire application model. -var storage = builder.AddAzureConstruct("empty", (resource, construct) => +var construct1 = builder.AddAzureConstruct("construct1", (construct) => { - var parameters = construct.GetParameters().ToDictionary(p => p.Name); - + // Create a storage account and set its SKU and location. The + // sku is bound from an app model parameter. var account = construct.AddStorageAccount( - name: "bob", - kind: StorageKind.BlobStorage, - sku: StorageSkuName.StandardLrs - ); - - account.AssignParameter(a => a.Sku.Name, parameters["storagesku"]); - account.AssignParameter(a => a.Location, parameters["location"]); + name: "bob", + kind: StorageKind.BlobStorage, + sku: StorageSkuName.StandardLrs + ); + account.AssignParameter(a => a.Location, construct.LocationParameter); + account.AssignParameter(a => a.Sku.Name, construct.AddParameter(sku)); account.AddOutput(data => data.PrimaryEndpoints.TableUri, "tableUri", isSecure: true); -}).WithParameter("storagesku", sku); +}); + +var construct2 = builder.AddAzureConstruct("construct2", (construct) => +{ + // Create a keyvault and set its location and add a secret. The secret + // value is bound from the app model parameter. + var kv = construct.AddKeyVault(name: "jane"); + kv.AssignParameter(k => k.Location, construct.LocationParameter); + + var myConnectionStringSecret = new KeyVaultSecret(construct, "mysecret"); + myConnectionStringSecret.AssignParameter(s => s.Properties.Value, construct.AddParameter(construct1.GetOutput("tableUri"))); + +}); builder.AddProject("api") - .WithEnvironment("TABLE_URI", storage.GetOutput("tableUri")); + .WithEnvironment("TABLE_URI", construct1.GetOutput("tableUri")); // 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 @@ -37,3 +47,8 @@ builder.AddProject(KnownResourceNames.AspireDashboard); builder.Build().Run(); + +// OPEN QUESTIONS: +// 1. Is it possible to express resourceGroup().location +// 2. Outputting a module and sub-modules (no main.bicep) +// 3. Assigning parameters to mandatory properties (without duplications) diff --git a/playground/cdk/CdkSample.AppHost/aspire-manifest.json b/playground/cdk/CdkSample.AppHost/aspire-manifest.json index 8f6d24577a..3051b74063 100644 --- a/playground/cdk/CdkSample.AppHost/aspire-manifest.json +++ b/playground/cdk/CdkSample.AppHost/aspire-manifest.json @@ -9,11 +9,18 @@ } } }, - "empty": { + "construct1": { "type": "azure.bicep.v0", - "path": "empty.module.bicep", + "path": "construct1.module.bicep", "params": { - "storagesku": "{storagesku.value}" + "storagesku": "Aspire.Hosting.ApplicationModel.ParameterResource" + } + }, + "construct2": { + "type": "azure.bicep.v0", + "path": "construct2.module.bicep", + "params": { + "tableUri": "{construct1.outputs.tableUri}" } }, "api": { @@ -21,7 +28,8 @@ "path": "../CdkSample.ApiService/CdkSample.ApiService.csproj", "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", + "TABLE_URI": "{construct1.outputs.tableUri}" }, "bindings": { "http": { diff --git a/playground/cdk/CdkSample.AppHost/construct1.module.bicep b/playground/cdk/CdkSample.AppHost/construct1.module.bicep new file mode 100644 index 0000000000..d5fae4df8e --- /dev/null +++ b/playground/cdk/CdkSample.AppHost/construct1.module.bicep @@ -0,0 +1,19 @@ +@description('West US 3') +param location string + +@description('') +param storagesku string + + +resource storageAccount_Jdw5JxFxB 'Microsoft.Storage/storageAccounts@2022-09-01' = { + name: 'bobb45549e63c694665b351f' + location: location + sku: { + name: storagesku + } + kind: 'StorageV2' + properties: { + } +} + +output tableUri string = storageAccount_Jdw5JxFxB.properties.primaryEndpoints.table diff --git a/playground/cdk/CdkSample.AppHost/construct2.module.bicep b/playground/cdk/CdkSample.AppHost/construct2.module.bicep new file mode 100644 index 0000000000..0839f021a9 --- /dev/null +++ b/playground/cdk/CdkSample.AppHost/construct2.module.bicep @@ -0,0 +1,29 @@ +@description('West US 3') +param location string + +@description('') +param tableUri string + + +resource keyVault_Mc6LeAH6Q 'Microsoft.KeyVault/vaults@2023-02-01' = { + name: 'jane-temp' + location: location + properties: { + tenantId: '5f85e12e-4fd7-475a-8acf-b6f07b4ff684' + sku: { + name: 'standard' + family: 'A' + } + enableRbacAuthorization: true + } +} + +resource keyVaultSecret_ZTFvs0PgN 'Microsoft.KeyVault/vaults/secrets@2023-02-01' = { + parent: keyVault_Mc6LeAH6Q + name: 'mysecret-temp' + properties: { + value: tableUri + } +} + +output vaultUri string = keyVault_Mc6LeAH6Q.properties.vaultUri diff --git a/playground/cdk/CdkSample.AppHost/empty.module.bicep b/playground/cdk/CdkSample.AppHost/empty.module.bicep deleted file mode 100644 index 8742dd29e0..0000000000 --- a/playground/cdk/CdkSample.AppHost/empty.module.bicep +++ /dev/null @@ -1,19 +0,0 @@ -@description('') -param storagesku string - -@description('') -param location string = 'West US 3' - - -resource storageAccount_9e2cRkGOF 'Microsoft.Storage/storageAccounts@2022-09-01' = { - name: 'bob99e053564f774257ad1d5' - location: location - sku: { - name: storagesku - } - kind: 'StorageV2' - properties: { - } -} - -output tableUri string = storageAccount_9e2cRkGOF.properties.primaryEndpoints.table diff --git a/src/Aspire.Hosting.Azure/AzureConstructResource.cs b/src/Aspire.Hosting.Azure/AzureConstructResource.cs index 54a84e0b3d..89b92a1cff 100644 --- a/src/Aspire.Hosting.Azure/AzureConstructResource.cs +++ b/src/Aspire.Hosting.Azure/AzureConstructResource.cs @@ -12,23 +12,35 @@ namespace Aspire.Hosting; /// An Aspire resource which is also a CDK construct. /// /// -/// -/// -public class AzureConstructResource(string name, Infrastructure infrastructure, Func createConstruct) : AzureBicepResource(name, templateFile: $"{name}.module.bicep") +/// +public class AzureConstructResource(string name, Action configureConstruct) : AzureBicepResource(name, templateFile: $"{name}.module.bicep") { /// /// TODO: /// - public Func CreateConstruct { get; } = createConstruct; + public Action ConfigureConstruct { get; } = configureConstruct; /// public override void WriteToManifest(ManifestPublishingContext context) { // HACK: Using CDK to generate files but then copying just the module // to where it needs to be. + var tempInfrastructure = new TempInfrastructure(); + var resourceModuleConstruct = new ResourceModuleConstruct(this, tempInfrastructure); + + var locationParameter = new Parameter("location", defaultValue: "West US 3"); + resourceModuleConstruct.AddParameter(locationParameter); + + foreach (var aspireParameter in this.Parameters) + { + var constructParameter = new Parameter(aspireParameter.Key); + resourceModuleConstruct.AddParameter(constructParameter); + } + + ConfigureConstruct(resourceModuleConstruct); + var generationPath = Directory.CreateTempSubdirectory("aspire").FullName; - CreateConstruct(this, infrastructure); - infrastructure.Build(generationPath); + tempInfrastructure.Build(generationPath); var moduleSourcePath = Path.Combine(generationPath, "resources", "rg_temp_module", "rg_temp_module.bicep"); var moduleDestinationPath = context.GetManifestRelativePath(TemplateFile); @@ -52,55 +64,98 @@ public static class CdkResourceExtensions /// The name of the resource being added. /// A callback used to configure the resource. /// - public static IResourceBuilder AddAzureConstruct(this IDistributedApplicationBuilder builder, string name, Action configureConstruct) + public static IResourceBuilder AddAzureConstruct(this IDistributedApplicationBuilder builder, string name, Action configureConstruct) { - var createConstruct = (AzureConstructResource resource, IConstruct subscriptionConstruct) => - { - var resourceConstruct = new AspireResourceConstruct(subscriptionConstruct, name, builder.Environment.EnvironmentName); + var resource = new AzureConstructResource(name, configureConstruct); + return builder.AddResource(resource) + .WithManifestPublishingCallback(resource.WriteToManifest); + } - var locationParameter = new Parameter("location", defaultValue: "West US 3"); - resourceConstruct.AddParameter(locationParameter); + /// + /// TODO: + /// + /// + /// + /// + public static Parameter AddParameter(this ResourceModuleConstruct resourceModuleConstruct, IResourceBuilder parameterResourceBuilder) + { + return resourceModuleConstruct.AddParameter(parameterResourceBuilder.Resource.Name, parameterResourceBuilder); + } - foreach (var aspireParameter in resource.Parameters) - { - var constructParameter = new Parameter(aspireParameter.Key); - resourceConstruct.AddParameter(constructParameter); - } + /// + /// TODO: + /// + /// + /// + /// + /// + public static Parameter AddParameter(this ResourceModuleConstruct resourceModuleConstruct, string name, IResourceBuilder parameterResourceBuilder) + { + // Ensure the parameter is added to the Aspire resource. + resourceModuleConstruct.Resource.Parameters.Add(name, parameterResourceBuilder); - configureConstruct(resource, resourceConstruct); - return resourceConstruct; - }; - return builder.AddAzureConstruct(name, createConstruct); + var parameter = new Parameter(name, isSecure: parameterResourceBuilder.Resource.Secret); + resourceModuleConstruct.AddParameter(parameter); + return parameter; } /// - /// Adds a CDK resource to the application model. + /// TODO: /// - /// The distributed application builder. - /// The name of the resource being added. - /// + /// + /// /// - public static IResourceBuilder AddAzureConstruct(this IDistributedApplicationBuilder builder, string name, Func createConstruct) + public static Parameter AddParameter(this ResourceModuleConstruct resourceModuleConstruct, BicepOutputReference outputReference) { - // HACK: We shouldn't need this. - var infrastructure = new DummyInfrastructure(Guid.NewGuid(), Guid.NewGuid(), "temp"); + return resourceModuleConstruct.AddParameter(outputReference.Name, outputReference); + } - var resource = new AzureConstructResource(name, infrastructure, createConstruct); - return builder.AddResource(resource) - .WithManifestPublishingCallback(resource.WriteToManifest); + /// + /// TODO: + /// + /// + /// + /// + /// + public static Parameter AddParameter(this ResourceModuleConstruct resourceModuleConstruct, string name, BicepOutputReference outputReference) + { + resourceModuleConstruct.Resource.Parameters.Add(name, outputReference); + + var parameter = new Parameter(name); + resourceModuleConstruct.AddParameter(parameter); + return parameter; } } /// -/// TODO: +/// TODO: Can't think of a better name right now /// -/// -/// -/// -public class AspireResourceConstruct(IConstruct scope, string resourceName, string envName) : Construct(scope, resourceName, ConstructScope.ResourceGroup, tenantId: Guid.NewGuid(), subscriptionId: Guid.NewGuid(), envName: envName) +public class ResourceModuleConstruct : Construct { + /// + /// + /// + /// + /// + public ResourceModuleConstruct(AzureConstructResource resource, IConstruct scope) : base(scope, resource.Name, ConstructScope.ResourceGroup, tenantId: Guid.NewGuid(), subscriptionId: Guid.NewGuid(), envName: "temp") + { + Resource = resource; + LocationParameter = new Parameter("location", "West US 3"); + AddParameter(LocationParameter); + + } + + /// + /// TODO: + /// + public AzureConstructResource Resource { get; } + + /// + /// TODO: + /// + public Parameter LocationParameter { get; } } -internal class DummyInfrastructure(Guid tenantId, Guid subscriptionId, string envName) : Infrastructure(tenantId: tenantId, subscriptionId: subscriptionId, envName: envName) +internal class TempInfrastructure() : Infrastructure(tenantId: Guid.NewGuid(), subscriptionId: Guid.NewGuid(), envName: "temp") { } From 1a4cfb33cdc3a83a1b5beef48790899d85b01150 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Wed, 28 Feb 2024 13:12:28 +1100 Subject: [PATCH 06/13] Update reference outputs. --- playground/cdk/CdkSample.AppHost/aspire-manifest.json | 2 +- .../cdk/CdkSample.AppHost/construct1.module.bicep | 6 +++--- .../cdk/CdkSample.AppHost/construct2.module.bicep | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/playground/cdk/CdkSample.AppHost/aspire-manifest.json b/playground/cdk/CdkSample.AppHost/aspire-manifest.json index 3051b74063..a2c21b5a9f 100644 --- a/playground/cdk/CdkSample.AppHost/aspire-manifest.json +++ b/playground/cdk/CdkSample.AppHost/aspire-manifest.json @@ -13,7 +13,7 @@ "type": "azure.bicep.v0", "path": "construct1.module.bicep", "params": { - "storagesku": "Aspire.Hosting.ApplicationModel.ParameterResource" + "storagesku": "{storagesku.value}" } }, "construct2": { diff --git a/playground/cdk/CdkSample.AppHost/construct1.module.bicep b/playground/cdk/CdkSample.AppHost/construct1.module.bicep index d5fae4df8e..d8ee093ad8 100644 --- a/playground/cdk/CdkSample.AppHost/construct1.module.bicep +++ b/playground/cdk/CdkSample.AppHost/construct1.module.bicep @@ -5,8 +5,8 @@ param location string param storagesku string -resource storageAccount_Jdw5JxFxB 'Microsoft.Storage/storageAccounts@2022-09-01' = { - name: 'bobb45549e63c694665b351f' +resource storageAccount_jUWh3LB5G 'Microsoft.Storage/storageAccounts@2022-09-01' = { + name: 'bob814ae87fd35d4c2988917' location: location sku: { name: storagesku @@ -16,4 +16,4 @@ resource storageAccount_Jdw5JxFxB 'Microsoft.Storage/storageAccounts@2022-09-01' } } -output tableUri string = storageAccount_Jdw5JxFxB.properties.primaryEndpoints.table +output tableUri string = storageAccount_jUWh3LB5G.properties.primaryEndpoints.table diff --git a/playground/cdk/CdkSample.AppHost/construct2.module.bicep b/playground/cdk/CdkSample.AppHost/construct2.module.bicep index 0839f021a9..946294225d 100644 --- a/playground/cdk/CdkSample.AppHost/construct2.module.bicep +++ b/playground/cdk/CdkSample.AppHost/construct2.module.bicep @@ -5,11 +5,11 @@ param location string param tableUri string -resource keyVault_Mc6LeAH6Q 'Microsoft.KeyVault/vaults@2023-02-01' = { +resource keyVault_jIQqamCos 'Microsoft.KeyVault/vaults@2023-02-01' = { name: 'jane-temp' location: location properties: { - tenantId: '5f85e12e-4fd7-475a-8acf-b6f07b4ff684' + tenantId: '83da9385-a5f2-44d6-8190-6d0c4184a19a' sku: { name: 'standard' family: 'A' @@ -18,12 +18,12 @@ resource keyVault_Mc6LeAH6Q 'Microsoft.KeyVault/vaults@2023-02-01' = { } } -resource keyVaultSecret_ZTFvs0PgN 'Microsoft.KeyVault/vaults/secrets@2023-02-01' = { - parent: keyVault_Mc6LeAH6Q +resource keyVaultSecret_BJ0gguPoz 'Microsoft.KeyVault/vaults/secrets@2023-02-01' = { + parent: keyVault_jIQqamCos name: 'mysecret-temp' properties: { value: tableUri } } -output vaultUri string = keyVault_Mc6LeAH6Q.properties.vaultUri +output vaultUri string = keyVault_jIQqamCos.properties.vaultUri From 164ce4aea4d798b7965df8c6c80a66ebe77b4472 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Wed, 28 Feb 2024 16:35:42 +1100 Subject: [PATCH 07/13] Use anonymous resource groups. --- Directory.Packages.props | 4 ++-- .../CdkSample.AppHost/construct1.module.bicep | 8 +++++--- .../CdkSample.AppHost/construct2.module.bicep | 12 +++++++----- .../AzureConstructResource.cs | 19 +++++-------------- 4 files changed, 19 insertions(+), 24 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 03703f4072..51e58c6fd6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,11 +17,11 @@ - + - + diff --git a/playground/cdk/CdkSample.AppHost/construct1.module.bicep b/playground/cdk/CdkSample.AppHost/construct1.module.bicep index d8ee093ad8..badaf6f32e 100644 --- a/playground/cdk/CdkSample.AppHost/construct1.module.bicep +++ b/playground/cdk/CdkSample.AppHost/construct1.module.bicep @@ -1,3 +1,5 @@ +targetScope = 'resourceGroup' + @description('West US 3') param location string @@ -5,8 +7,8 @@ param location string param storagesku string -resource storageAccount_jUWh3LB5G 'Microsoft.Storage/storageAccounts@2022-09-01' = { - name: 'bob814ae87fd35d4c2988917' +resource storageAccount_WLTg4zEgJ 'Microsoft.Storage/storageAccounts@2022-09-01' = { + name: 'bobd3c49aedcdfc40eb8f3d8' location: location sku: { name: storagesku @@ -16,4 +18,4 @@ resource storageAccount_jUWh3LB5G 'Microsoft.Storage/storageAccounts@2022-09-01' } } -output tableUri string = storageAccount_jUWh3LB5G.properties.primaryEndpoints.table +output tableUri string = storageAccount_WLTg4zEgJ.properties.primaryEndpoints.table diff --git a/playground/cdk/CdkSample.AppHost/construct2.module.bicep b/playground/cdk/CdkSample.AppHost/construct2.module.bicep index 946294225d..c68306845e 100644 --- a/playground/cdk/CdkSample.AppHost/construct2.module.bicep +++ b/playground/cdk/CdkSample.AppHost/construct2.module.bicep @@ -1,3 +1,5 @@ +targetScope = 'resourceGroup' + @description('West US 3') param location string @@ -5,11 +7,11 @@ param location string param tableUri string -resource keyVault_jIQqamCos 'Microsoft.KeyVault/vaults@2023-02-01' = { +resource keyVault_kb5kSO8cv 'Microsoft.KeyVault/vaults@2023-02-01' = { name: 'jane-temp' location: location properties: { - tenantId: '83da9385-a5f2-44d6-8190-6d0c4184a19a' + tenantId: '00000000-0000-0000-0000-000000000000' sku: { name: 'standard' family: 'A' @@ -18,12 +20,12 @@ resource keyVault_jIQqamCos 'Microsoft.KeyVault/vaults@2023-02-01' = { } } -resource keyVaultSecret_BJ0gguPoz 'Microsoft.KeyVault/vaults/secrets@2023-02-01' = { - parent: keyVault_jIQqamCos +resource keyVaultSecret_BirhV1djm 'Microsoft.KeyVault/vaults/secrets@2023-02-01' = { + parent: keyVault_kb5kSO8cv name: 'mysecret-temp' properties: { value: tableUri } } -output vaultUri string = keyVault_jIQqamCos.properties.vaultUri +output vaultUri string = keyVault_kb5kSO8cv.properties.vaultUri diff --git a/src/Aspire.Hosting.Azure/AzureConstructResource.cs b/src/Aspire.Hosting.Azure/AzureConstructResource.cs index 89b92a1cff..9cd5432543 100644 --- a/src/Aspire.Hosting.Azure/AzureConstructResource.cs +++ b/src/Aspire.Hosting.Azure/AzureConstructResource.cs @@ -23,10 +23,7 @@ public class AzureConstructResource(string name, Action /// public override void WriteToManifest(ManifestPublishingContext context) { - // HACK: Using CDK to generate files but then copying just the module - // to where it needs to be. - var tempInfrastructure = new TempInfrastructure(); - var resourceModuleConstruct = new ResourceModuleConstruct(this, tempInfrastructure); + var resourceModuleConstruct = new ResourceModuleConstruct(this); var locationParameter = new Parameter("location", defaultValue: "West US 3"); resourceModuleConstruct.AddParameter(locationParameter); @@ -40,9 +37,9 @@ public override void WriteToManifest(ManifestPublishingContext context) ConfigureConstruct(resourceModuleConstruct); var generationPath = Directory.CreateTempSubdirectory("aspire").FullName; - tempInfrastructure.Build(generationPath); + resourceModuleConstruct.Build(generationPath); - var moduleSourcePath = Path.Combine(generationPath, "resources", "rg_temp_module", "rg_temp_module.bicep"); + var moduleSourcePath = Path.Combine(generationPath, "main.bicep"); var moduleDestinationPath = context.GetManifestRelativePath(TemplateFile); File.Copy(moduleSourcePath, moduleDestinationPath!, true); @@ -130,19 +127,17 @@ public static Parameter AddParameter(this ResourceModuleConstruct resourceModule /// /// TODO: Can't think of a better name right now /// -public class ResourceModuleConstruct : Construct +public class ResourceModuleConstruct : Infrastructure { /// /// /// /// - /// - public ResourceModuleConstruct(AzureConstructResource resource, IConstruct scope) : base(scope, resource.Name, ConstructScope.ResourceGroup, tenantId: Guid.NewGuid(), subscriptionId: Guid.NewGuid(), envName: "temp") + public ResourceModuleConstruct(AzureConstructResource resource) : base(constructScope: ConstructScope.ResourceGroup, tenantId: Guid.Empty, subscriptionId: Guid.Empty, envName: "temp", useAnonymousResourceGroup: true) { Resource = resource; LocationParameter = new Parameter("location", "West US 3"); AddParameter(LocationParameter); - } /// @@ -155,7 +150,3 @@ public ResourceModuleConstruct(AzureConstructResource resource, IConstruct scope /// public Parameter LocationParameter { get; } } - -internal class TempInfrastructure() : Infrastructure(tenantId: Guid.NewGuid(), subscriptionId: Guid.NewGuid(), envName: "temp") -{ -} From c16adb02353189fed4fd8119bd7370324912893f Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Wed, 28 Feb 2024 21:34:17 +1100 Subject: [PATCH 08/13] WIP. Work around naming. --- playground/cdk/CdkSample.AppHost/Program.cs | 25 +++++++-------- .../CdkSample.AppHost/aspire-manifest.json | 7 ----- .../CdkSample.AppHost/construct1.module.bicep | 9 ++++-- .../CdkSample.AppHost/construct2.module.bicep | 31 ------------------- .../AzureBicepResource.cs | 2 +- .../AzureConstructResource.cs | 11 +++---- 6 files changed, 23 insertions(+), 62 deletions(-) delete mode 100644 playground/cdk/CdkSample.AppHost/construct2.module.bicep diff --git a/playground/cdk/CdkSample.AppHost/Program.cs b/playground/cdk/CdkSample.AppHost/Program.cs index 73b2f0b234..0ac9eab6ce 100644 --- a/playground/cdk/CdkSample.AppHost/Program.cs +++ b/playground/cdk/CdkSample.AppHost/Program.cs @@ -1,16 +1,18 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Azure.Provisioning.KeyVaults; +using Azure.Provisioning; using Azure.Provisioning.Storage; using Azure.ResourceManager.Storage.Models; var builder = DistributedApplication.CreateBuilder(args); +builder.AddAzureProvisioning(); var sku = builder.AddParameter("storagesku"); var construct1 = builder.AddAzureConstruct("construct1", (construct) => { + // Create a storage account and set its SKU and location. The // sku is bound from an app model parameter. var account = construct.AddStorageAccount( @@ -18,24 +20,21 @@ kind: StorageKind.BlobStorage, sku: StorageSkuName.StandardLrs ); + + // HACK: Doing this to get a deterministic unique name for the storage + // account. This creates a parameter with a function embedded in + // the default value and then assigns that parameter to the name + // so that the emitted Bicep is in the final format we want. + var storageNameParameter = new Parameter("storageName", defaultValue: "bob${uniqueString(resourceGroup().id)}"); + construct.AddParameter(storageNameParameter); + account.AssignParameter(a => a.Name, storageNameParameter); + account.AssignParameter(a => a.Location, construct.LocationParameter); account.AssignParameter(a => a.Sku.Name, construct.AddParameter(sku)); account.AddOutput(data => data.PrimaryEndpoints.TableUri, "tableUri", isSecure: true); }); -var construct2 = builder.AddAzureConstruct("construct2", (construct) => -{ - // Create a keyvault and set its location and add a secret. The secret - // value is bound from the app model parameter. - var kv = construct.AddKeyVault(name: "jane"); - kv.AssignParameter(k => k.Location, construct.LocationParameter); - - var myConnectionStringSecret = new KeyVaultSecret(construct, "mysecret"); - myConnectionStringSecret.AssignParameter(s => s.Properties.Value, construct.AddParameter(construct1.GetOutput("tableUri"))); - -}); - builder.AddProject("api") .WithEnvironment("TABLE_URI", construct1.GetOutput("tableUri")); diff --git a/playground/cdk/CdkSample.AppHost/aspire-manifest.json b/playground/cdk/CdkSample.AppHost/aspire-manifest.json index a2c21b5a9f..556354092c 100644 --- a/playground/cdk/CdkSample.AppHost/aspire-manifest.json +++ b/playground/cdk/CdkSample.AppHost/aspire-manifest.json @@ -16,13 +16,6 @@ "storagesku": "{storagesku.value}" } }, - "construct2": { - "type": "azure.bicep.v0", - "path": "construct2.module.bicep", - "params": { - "tableUri": "{construct1.outputs.tableUri}" - } - }, "api": { "type": "project.v0", "path": "../CdkSample.ApiService/CdkSample.ApiService.csproj", diff --git a/playground/cdk/CdkSample.AppHost/construct1.module.bicep b/playground/cdk/CdkSample.AppHost/construct1.module.bicep index badaf6f32e..5f07407a56 100644 --- a/playground/cdk/CdkSample.AppHost/construct1.module.bicep +++ b/playground/cdk/CdkSample.AppHost/construct1.module.bicep @@ -1,5 +1,8 @@ targetScope = 'resourceGroup' +@description('') +param storageName string = 'bob${uniqueString(resourceGroup().id)}' + @description('West US 3') param location string @@ -7,8 +10,8 @@ param location string param storagesku string -resource storageAccount_WLTg4zEgJ 'Microsoft.Storage/storageAccounts@2022-09-01' = { - name: 'bobd3c49aedcdfc40eb8f3d8' +resource storageAccount_zianFGpKu 'Microsoft.Storage/storageAccounts@2022-09-01' = { + name: storageName location: location sku: { name: storagesku @@ -18,4 +21,4 @@ resource storageAccount_WLTg4zEgJ 'Microsoft.Storage/storageAccounts@2022-09-01' } } -output tableUri string = storageAccount_WLTg4zEgJ.properties.primaryEndpoints.table +output tableUri string = storageAccount_zianFGpKu.properties.primaryEndpoints.table diff --git a/playground/cdk/CdkSample.AppHost/construct2.module.bicep b/playground/cdk/CdkSample.AppHost/construct2.module.bicep deleted file mode 100644 index c68306845e..0000000000 --- a/playground/cdk/CdkSample.AppHost/construct2.module.bicep +++ /dev/null @@ -1,31 +0,0 @@ -targetScope = 'resourceGroup' - -@description('West US 3') -param location string - -@description('') -param tableUri string - - -resource keyVault_kb5kSO8cv 'Microsoft.KeyVault/vaults@2023-02-01' = { - name: 'jane-temp' - location: location - properties: { - tenantId: '00000000-0000-0000-0000-000000000000' - sku: { - name: 'standard' - family: 'A' - } - enableRbacAuthorization: true - } -} - -resource keyVaultSecret_BirhV1djm 'Microsoft.KeyVault/vaults/secrets@2023-02-01' = { - parent: keyVault_kb5kSO8cv - name: 'mysecret-temp' - properties: { - value: tableUri - } -} - -output vaultUri string = keyVault_kb5kSO8cv.properties.vaultUri diff --git a/src/Aspire.Hosting.Azure/AzureBicepResource.cs b/src/Aspire.Hosting.Azure/AzureBicepResource.cs index 6552f5ad33..e74e1791b4 100644 --- a/src/Aspire.Hosting.Azure/AzureBicepResource.cs +++ b/src/Aspire.Hosting.Azure/AzureBicepResource.cs @@ -48,7 +48,7 @@ public class AzureBicepResource(string name, string? templateFile = null, string /// A boolean that determines if the file should be deleted on disposal of the . /// A that represents the bicep file. /// - public BicepTemplateFile GetBicepTemplateFile(string? directory = null, bool deleteTemporaryFileOnDispose = true) + public virtual BicepTemplateFile GetBicepTemplateFile(string? directory = null, bool deleteTemporaryFileOnDispose = true) { // Throw if multiple template sources are specified if (TemplateFile is not null && (TemplateString is not null || TemplateResourceName is not null)) diff --git a/src/Aspire.Hosting.Azure/AzureConstructResource.cs b/src/Aspire.Hosting.Azure/AzureConstructResource.cs index 9cd5432543..3a38c76824 100644 --- a/src/Aspire.Hosting.Azure/AzureConstructResource.cs +++ b/src/Aspire.Hosting.Azure/AzureConstructResource.cs @@ -3,7 +3,6 @@ using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Azure; -using Aspire.Hosting.Publishing; using Azure.Provisioning; namespace Aspire.Hosting; @@ -20,8 +19,8 @@ public class AzureConstructResource(string name, Action /// public Action ConfigureConstruct { get; } = configureConstruct; - /// - public override void WriteToManifest(ManifestPublishingContext context) + /// + public override BicepTemplateFile GetBicepTemplateFile(string? directory = null, bool deleteTemporaryFileOnDispose = true) { var resourceModuleConstruct = new ResourceModuleConstruct(this); @@ -40,12 +39,10 @@ public override void WriteToManifest(ManifestPublishingContext context) resourceModuleConstruct.Build(generationPath); var moduleSourcePath = Path.Combine(generationPath, "main.bicep"); - var moduleDestinationPath = context.GetManifestRelativePath(TemplateFile); + var moduleDestinationPath = Path.Combine(directory ?? generationPath, $"{Name}.module.bicep"); File.Copy(moduleSourcePath, moduleDestinationPath!, true); - Directory.Delete(generationPath, true); - - base.WriteToManifest(context); + return new BicepTemplateFile(moduleDestinationPath, directory is null); } } From 4aa6d83f7d8686ba35116eb1e1efb195f2ccf94e Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Thu, 29 Feb 2024 14:50:17 +1100 Subject: [PATCH 09/13] Updated CDK package. --- Directory.Packages.props | 2 +- playground/cdk/CdkSample.ApiService/Program.cs | 4 ++-- playground/cdk/CdkSample.AppHost/Program.cs | 13 ------------- .../cdk/CdkSample.AppHost/construct1.module.bicep | 14 ++++---------- .../Provisioners/BicepProvisioner.cs | 7 +++++-- src/Aspire.Hosting.Azure/AzureConstructResource.cs | 11 +++++++---- 6 files changed, 19 insertions(+), 32 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 51e58c6fd6..4a1e3bb0a8 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -21,7 +21,7 @@ - + diff --git a/playground/cdk/CdkSample.ApiService/Program.cs b/playground/cdk/CdkSample.ApiService/Program.cs index d2eca60016..643f6ce15e 100644 --- a/playground/cdk/CdkSample.ApiService/Program.cs +++ b/playground/cdk/CdkSample.ApiService/Program.cs @@ -7,9 +7,9 @@ var app = builder.Build(); -app.MapGet("/", () => +app.MapGet("/", (IConfiguration config) => { - return "Hello, Azure!"; + return $"TABLE_URI is: {config["TABLE_URI"]}"; }); app.Run(); diff --git a/playground/cdk/CdkSample.AppHost/Program.cs b/playground/cdk/CdkSample.AppHost/Program.cs index 0ac9eab6ce..8d0f8091ee 100644 --- a/playground/cdk/CdkSample.AppHost/Program.cs +++ b/playground/cdk/CdkSample.AppHost/Program.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 Azure.Provisioning; using Azure.Provisioning.Storage; using Azure.ResourceManager.Storage.Models; @@ -12,24 +11,12 @@ var construct1 = builder.AddAzureConstruct("construct1", (construct) => { - - // Create a storage account and set its SKU and location. The - // sku is bound from an app model parameter. var account = construct.AddStorageAccount( name: "bob", kind: StorageKind.BlobStorage, sku: StorageSkuName.StandardLrs ); - // HACK: Doing this to get a deterministic unique name for the storage - // account. This creates a parameter with a function embedded in - // the default value and then assigns that parameter to the name - // so that the emitted Bicep is in the final format we want. - var storageNameParameter = new Parameter("storageName", defaultValue: "bob${uniqueString(resourceGroup().id)}"); - construct.AddParameter(storageNameParameter); - account.AssignParameter(a => a.Name, storageNameParameter); - - account.AssignParameter(a => a.Location, construct.LocationParameter); account.AssignParameter(a => a.Sku.Name, construct.AddParameter(sku)); account.AddOutput(data => data.PrimaryEndpoints.TableUri, "tableUri", isSecure: true); diff --git a/playground/cdk/CdkSample.AppHost/construct1.module.bicep b/playground/cdk/CdkSample.AppHost/construct1.module.bicep index 5f07407a56..1a8b2d676a 100644 --- a/playground/cdk/CdkSample.AppHost/construct1.module.bicep +++ b/playground/cdk/CdkSample.AppHost/construct1.module.bicep @@ -1,18 +1,12 @@ targetScope = 'resourceGroup' -@description('') -param storageName string = 'bob${uniqueString(resourceGroup().id)}' - -@description('West US 3') -param location string - @description('') param storagesku string -resource storageAccount_zianFGpKu 'Microsoft.Storage/storageAccounts@2022-09-01' = { - name: storageName - location: location +resource storageAccount_SOTvKjFQy 'Microsoft.Storage/storageAccounts@2022-09-01' = { + name: toLower(take(concat('bob', uniqueString(resourceGroup().id)), 24)) + location: resourceGroup().location sku: { name: storagesku } @@ -21,4 +15,4 @@ resource storageAccount_zianFGpKu 'Microsoft.Storage/storageAccounts@2022-09-01' } } -output tableUri string = storageAccount_zianFGpKu.properties.primaryEndpoints.table +output tableUri string = storageAccount_SOTvKjFQy.properties.primaryEndpoints.table diff --git a/src/Aspire.Hosting.Azure.Provisioning/Provisioners/BicepProvisioner.cs b/src/Aspire.Hosting.Azure.Provisioning/Provisioners/BicepProvisioner.cs index 0373bd52ba..604b65c0a6 100644 --- a/src/Aspire.Hosting.Azure.Provisioning/Provisioners/BicepProvisioner.cs +++ b/src/Aspire.Hosting.Azure.Provisioning/Provisioners/BicepProvisioner.cs @@ -240,8 +240,11 @@ private static void PopulateWellKnownParameters(AzureBicepResource resource, Pro resource.Parameters.Remove(AzureBicepResource.KnownParameters.LogAnalyticsWorkspaceId); } - // Always specify the location - resource.Parameters[AzureBicepResource.KnownParameters.Location] = context.Location.Name; + if (resource.Parameters.TryGetValue(AzureBicepResource.KnownParameters.Location, out var location) && location is null) + { + // Always specify the location + resource.Parameters[AzureBicepResource.KnownParameters.Location] = context.Location.Name; + } } private static async Task ExecuteCommand(ProcessSpec processSpec) diff --git a/src/Aspire.Hosting.Azure/AzureConstructResource.cs b/src/Aspire.Hosting.Azure/AzureConstructResource.cs index 3a38c76824..04464e253c 100644 --- a/src/Aspire.Hosting.Azure/AzureConstructResource.cs +++ b/src/Aspire.Hosting.Azure/AzureConstructResource.cs @@ -22,10 +22,12 @@ public class AzureConstructResource(string name, Action /// public override BicepTemplateFile GetBicepTemplateFile(string? directory = null, bool deleteTemporaryFileOnDispose = true) { - var resourceModuleConstruct = new ResourceModuleConstruct(this); + var configuration = new Configuration() + { + UsePromptMode = true + }; - var locationParameter = new Parameter("location", defaultValue: "West US 3"); - resourceModuleConstruct.AddParameter(locationParameter); + var resourceModuleConstruct = new ResourceModuleConstruct(this, configuration); foreach (var aspireParameter in this.Parameters) { @@ -130,7 +132,8 @@ public class ResourceModuleConstruct : Infrastructure /// /// /// - public ResourceModuleConstruct(AzureConstructResource resource) : base(constructScope: ConstructScope.ResourceGroup, tenantId: Guid.Empty, subscriptionId: Guid.Empty, envName: "temp", useAnonymousResourceGroup: true) + /// + public ResourceModuleConstruct(AzureConstructResource resource, Configuration configuration) : base(constructScope: ConstructScope.ResourceGroup, tenantId: Guid.Empty, subscriptionId: Guid.Empty, envName: "temp", configuration: configuration) { Resource = resource; LocationParameter = new Parameter("location", "West US 3"); From 0845b78030b588c12e5a2c14543da4b5b13595e8 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 1 Mar 2024 10:32:40 +1100 Subject: [PATCH 10/13] Ready for review. --- Directory.Packages.props | 2 +- playground/cdk/CdkSample.AppHost/Program.cs | 1 - .../CdkSample.AppHost/construct1.module.bicep | 9 ++- .../AzureConstructResource.cs | 58 ++++++++----------- 4 files changed, 30 insertions(+), 40 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 4a1e3bb0a8..c505a97c8c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -21,7 +21,7 @@ - + diff --git a/playground/cdk/CdkSample.AppHost/Program.cs b/playground/cdk/CdkSample.AppHost/Program.cs index 8d0f8091ee..904c091429 100644 --- a/playground/cdk/CdkSample.AppHost/Program.cs +++ b/playground/cdk/CdkSample.AppHost/Program.cs @@ -16,7 +16,6 @@ kind: StorageKind.BlobStorage, sku: StorageSkuName.StandardLrs ); - account.AssignParameter(a => a.Sku.Name, construct.AddParameter(sku)); account.AddOutput(data => data.PrimaryEndpoints.TableUri, "tableUri", isSecure: true); diff --git a/playground/cdk/CdkSample.AppHost/construct1.module.bicep b/playground/cdk/CdkSample.AppHost/construct1.module.bicep index 1a8b2d676a..7cfb12d091 100644 --- a/playground/cdk/CdkSample.AppHost/construct1.module.bicep +++ b/playground/cdk/CdkSample.AppHost/construct1.module.bicep @@ -1,12 +1,15 @@ targetScope = 'resourceGroup' +@description('') +param location string = resourceGroup().location + @description('') param storagesku string -resource storageAccount_SOTvKjFQy 'Microsoft.Storage/storageAccounts@2022-09-01' = { +resource storageAccount_tgMUsfGUA 'Microsoft.Storage/storageAccounts@2022-09-01' = { name: toLower(take(concat('bob', uniqueString(resourceGroup().id)), 24)) - location: resourceGroup().location + location: location sku: { name: storagesku } @@ -15,4 +18,4 @@ resource storageAccount_SOTvKjFQy 'Microsoft.Storage/storageAccounts@2022-09-01' } } -output tableUri string = storageAccount_SOTvKjFQy.properties.primaryEndpoints.table +output tableUri string = storageAccount_tgMUsfGUA.properties.primaryEndpoints.table diff --git a/src/Aspire.Hosting.Azure/AzureConstructResource.cs b/src/Aspire.Hosting.Azure/AzureConstructResource.cs index 04464e253c..44f1a3a092 100644 --- a/src/Aspire.Hosting.Azure/AzureConstructResource.cs +++ b/src/Aspire.Hosting.Azure/AzureConstructResource.cs @@ -8,14 +8,14 @@ namespace Aspire.Hosting; /// -/// An Aspire resource which is also a CDK construct. +/// An Aspire resource that supports use of Azure Provisioning APIs to create Azure resources. /// /// /// public class AzureConstructResource(string name, Action configureConstruct) : AzureBicepResource(name, templateFile: $"{name}.module.bicep") { /// - /// TODO: + /// Callback for configuring construct. /// public Action ConfigureConstruct { get; } = configureConstruct; @@ -49,16 +49,16 @@ public override BicepTemplateFile GetBicepTemplateFile(string? directory = null, } /// -/// Extensions for working with CDK resources in the .NET Aspire application model. +/// Extensions for working with and related types. /// -public static class CdkResourceExtensions +public static class AzureConstructResourceExtensions { /// - /// Adds a CDK resource to the application model. + /// Adds an Azure construct resource to the application model. /// /// The distributed application builder. /// The name of the resource being added. - /// A callback used to configure the resource. + /// A callback used to configure the construct resource. /// public static IResourceBuilder AddAzureConstruct(this IDistributedApplicationBuilder builder, string name, Action configureConstruct) { @@ -68,10 +68,10 @@ public static IResourceBuilder AddAzureConstruct(this ID } /// - /// TODO: + /// Adds a parameter to the Azure construct resource based on an Aspire parameter. /// - /// - /// + /// The Azure construct resource. + /// The Aspire parameter resource builder. /// public static Parameter AddParameter(this ResourceModuleConstruct resourceModuleConstruct, IResourceBuilder parameterResourceBuilder) { @@ -79,11 +79,11 @@ public static Parameter AddParameter(this ResourceModuleConstruct resourceModule } /// - /// TODO: + /// Adds a parameter to the Azure construct resource based on an Aspire parameter. /// - /// - /// - /// + /// The Azure construct resource. + /// The name to be used for the Azure construct parameter. + /// The Aspire parameter resource builder. /// public static Parameter AddParameter(this ResourceModuleConstruct resourceModuleConstruct, string name, IResourceBuilder parameterResourceBuilder) { @@ -96,10 +96,10 @@ public static Parameter AddParameter(this ResourceModuleConstruct resourceModule } /// - /// TODO: + /// Adds a parameter to the Azure construct resource based on an /// - /// - /// + /// The Azure construct resource. + /// The Aspire Bicep output reference. /// public static Parameter AddParameter(this ResourceModuleConstruct resourceModuleConstruct, BicepOutputReference outputReference) { @@ -107,11 +107,11 @@ public static Parameter AddParameter(this ResourceModuleConstruct resourceModule } /// - /// TODO: + /// Adds a parameter to the Azure construct resource based on an /// - /// - /// - /// + /// The Azure construct resource. + /// The name to be used for the Azure construct parameter. + /// The Aspire Bicep output reference. /// public static Parameter AddParameter(this ResourceModuleConstruct resourceModuleConstruct, string name, BicepOutputReference outputReference) { @@ -124,29 +124,17 @@ public static Parameter AddParameter(this ResourceModuleConstruct resourceModule } /// -/// TODO: Can't think of a better name right now +/// An Azure Provisioning construct which represents the root Bicep module that is generated for an Azure construct resource. /// public class ResourceModuleConstruct : Infrastructure { - /// - /// - /// - /// - /// - public ResourceModuleConstruct(AzureConstructResource resource, Configuration configuration) : base(constructScope: ConstructScope.ResourceGroup, tenantId: Guid.Empty, subscriptionId: Guid.Empty, envName: "temp", configuration: configuration) + internal ResourceModuleConstruct(AzureConstructResource resource, Configuration configuration) : base(constructScope: ConstructScope.ResourceGroup, tenantId: Guid.Empty, subscriptionId: Guid.Empty, envName: "temp", configuration: configuration) { Resource = resource; - LocationParameter = new Parameter("location", "West US 3"); - AddParameter(LocationParameter); } /// - /// TODO: + /// The Azure cosntruct resource that this resource module construct represents. /// public AzureConstructResource Resource { get; } - - /// - /// TODO: - /// - public Parameter LocationParameter { get; } } From 8e924ce467c7391d9ed24a328cfd33f17d49306a Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 1 Mar 2024 10:46:10 +1100 Subject: [PATCH 11/13] Remove questions block. --- playground/cdk/CdkSample.AppHost/Program.cs | 5 ----- playground/cdk/CdkSample.AppHost/construct1.module.bicep | 4 ++-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/playground/cdk/CdkSample.AppHost/Program.cs b/playground/cdk/CdkSample.AppHost/Program.cs index 904c091429..27354cf9fc 100644 --- a/playground/cdk/CdkSample.AppHost/Program.cs +++ b/playground/cdk/CdkSample.AppHost/Program.cs @@ -32,8 +32,3 @@ builder.AddProject(KnownResourceNames.AspireDashboard); builder.Build().Run(); - -// OPEN QUESTIONS: -// 1. Is it possible to express resourceGroup().location -// 2. Outputting a module and sub-modules (no main.bicep) -// 3. Assigning parameters to mandatory properties (without duplications) diff --git a/playground/cdk/CdkSample.AppHost/construct1.module.bicep b/playground/cdk/CdkSample.AppHost/construct1.module.bicep index 7cfb12d091..d1892b40ed 100644 --- a/playground/cdk/CdkSample.AppHost/construct1.module.bicep +++ b/playground/cdk/CdkSample.AppHost/construct1.module.bicep @@ -7,7 +7,7 @@ param location string = resourceGroup().location param storagesku string -resource storageAccount_tgMUsfGUA 'Microsoft.Storage/storageAccounts@2022-09-01' = { +resource storageAccount_unUi1Obb4 'Microsoft.Storage/storageAccounts@2022-09-01' = { name: toLower(take(concat('bob', uniqueString(resourceGroup().id)), 24)) location: location sku: { @@ -18,4 +18,4 @@ resource storageAccount_tgMUsfGUA 'Microsoft.Storage/storageAccounts@2022-09-01' } } -output tableUri string = storageAccount_tgMUsfGUA.properties.primaryEndpoints.table +output tableUri string = storageAccount_unUi1Obb4.properties.primaryEndpoints.table From 1534bd357584e3d4ce7a24fff589e070cbbe7356 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 1 Mar 2024 12:09:41 +1100 Subject: [PATCH 12/13] Add a test case. --- .../Azure/AzureBicepResourceTests.cs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs b/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs index 938c6ed630..fdab4ef277 100644 --- a/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs +++ b/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs @@ -5,6 +5,8 @@ using System.Text.Json.Nodes; using Aspire.Hosting.Azure; using Aspire.Hosting.Utils; +using Azure.Provisioning.Storage; +using Azure.ResourceManager.Storage.Models; using Xunit; namespace Aspire.Hosting.Tests.Azure; @@ -184,6 +186,54 @@ public void WithReferenceAppInsightsSetsEnvironmentVariable() Assert.Equal("myinstrumentationkey", config["APPLICATIONINSIGHTS_CONNECTION_STRING"]); } + [Fact] + public void AddAzureConstructGenertesCorrectManifestEntry() + { + var builder = DistributedApplication.CreateBuilder(); + var construct1 = builder.AddAzureConstruct("construct1", (construct) => + { + var storage = construct.AddStorageAccount( + kind: StorageKind.StorageV2, + sku: StorageSkuName.StandardLrs + ); + storage.AddOutput(sa => sa.Name, "storageAccountName"); + }); + + var manifest = ManifestUtils.GetManifest(construct1.Resource); + Assert.Equal("azure.bicep.v0", manifest["type"]?.ToString()); + Assert.Equal("construct1.module.bicep", manifest["path"]?.ToString()); + } + + // TODO: This test to be reenabled once we figure out what is going on in CDK + // around parameters being injected twice. + //[Fact] + //public void AddParameterOnResourceModuleConstructPopulatesParametersEverywhere() + //{ + // var builder = DistributedApplication.CreateBuilder(); + // builder.Configuration["Parameters:skuName"] = "Standard_ZRS"; + + // var skuName = builder.AddParameter("skuName"); + + // ResourceModuleConstruct? moduleConstruct = null; + // var construct1 = builder.AddAzureConstruct("construct1", (construct) => + // { + // var storage = construct.AddStorageAccount( + // kind: StorageKind.StorageV2, + // sku: StorageSkuName.StandardLrs + // ); + // storage.AssignParameter(sa => sa.Sku.Name, construct.AddParameter(skuName)); + // moduleConstruct = construct; + // }); + + // var manifest = ManifestUtils.GetManifest(construct1.Resource); + + // Assert.NotNull(moduleConstruct); + // var constructParameters = moduleConstruct.GetParameters(false).ToDictionary(p => p.Name); + // Assert.True(constructParameters.ContainsKey("skuName")); + // Assert.Equal(skuName.Resource, construct1.Resource.Parameters["skuName"]); + // Assert.Equal("{skuName.value}", manifest["params"]?["skuName"]?.ToString()); + //} + [Fact] public void PublishAsRedisPublishesRedisAsAzureRedis() { From bf05ed72308bda5c9c1c13ed3f8fc0b03860b004 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 1 Mar 2024 12:51:36 +1100 Subject: [PATCH 13/13] Fix tests after merging from main. --- tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs b/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs index 1515139700..c178fe75b4 100644 --- a/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs +++ b/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs @@ -187,7 +187,7 @@ public async Task WithReferenceAppInsightsSetsEnvironmentVariable() } [Fact] - public void AddAzureConstructGenertesCorrectManifestEntry() + public async Task AddAzureConstructGenertesCorrectManifestEntry() { var builder = DistributedApplication.CreateBuilder(); var construct1 = builder.AddAzureConstruct("construct1", (construct) => @@ -199,7 +199,7 @@ public void AddAzureConstructGenertesCorrectManifestEntry() storage.AddOutput(sa => sa.Name, "storageAccountName"); }); - var manifest = ManifestUtils.GetManifest(construct1.Resource); + var manifest = await ManifestUtils.GetManifest(construct1.Resource); Assert.Equal("azure.bicep.v0", manifest["type"]?.ToString()); Assert.Equal("construct1.module.bicep", manifest["path"]?.ToString()); } @@ -235,7 +235,7 @@ public void AddAzureConstructGenertesCorrectManifestEntry() //} [Fact] - public void PublishAsRedisPublishesRedisAsAzureRedis() + public async Task PublishAsRedisPublishesRedisAsAzureRedis() { var builder = DistributedApplication.CreateBuilder();