diff --git a/Aspire.sln b/Aspire.sln
index fe783e6803a..9e3d8531398 100644
--- a/Aspire.sln
+++ b/Aspire.sln
@@ -608,6 +608,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SqlServerEndToEnd.Common",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SqlServerEndToEnd.DbSetup", "playground\SqlServerEndToEnd\SqlServerEndToEnd.DbSetup\SqlServerEndToEnd.DbSetup.csproj", "{125C081D-7E5B-4F35-B5CD-E2B56140380F}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWSCDK.AppHost", "playground\AWS\AWSCDK.AppHost\AWSCDK.AppHost.csproj", "{B5826732-7318-4179-9B85-870CB18AC533}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -1602,6 +1604,10 @@ Global
{125C081D-7E5B-4F35-B5CD-E2B56140380F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{125C081D-7E5B-4F35-B5CD-E2B56140380F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{125C081D-7E5B-4F35-B5CD-E2B56140380F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B5826732-7318-4179-9B85-870CB18AC533}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B5826732-7318-4179-9B85-870CB18AC533}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B5826732-7318-4179-9B85-870CB18AC533}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B5826732-7318-4179-9B85-870CB18AC533}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1895,6 +1901,7 @@ Global
{F0C976EF-EE26-4EA9-B324-0CD21DCEA140} = {3FF3F00C-95C0-46FC-B2BE-A3920C71E393}
{1997067D-8EF2-43B3-AB13-9B2E12B52709} = {2CA6AB88-21EF-4488-BB1B-3A5BAD5FE2AD}
{125C081D-7E5B-4F35-B5CD-E2B56140380F} = {2CA6AB88-21EF-4488-BB1B-3A5BAD5FE2AD}
+ {B5826732-7318-4179-9B85-870CB18AC533} = {EF91843F-C4AB-47F8-909B-C494EABB2BA2}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6DCEDFEC-988E-4CB3-B45B-191EB5086E0C}
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 9f516885a47..16d1986f4d5 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -18,6 +18,8 @@
+
+
diff --git a/playground/AWS/AWS.AppHost/AWS.AppHost.csproj b/playground/AWS/AWS.AppHost/AWS.AppHost.csproj
index 94cf7dbd750..722ea3617ba 100644
--- a/playground/AWS/AWS.AppHost/AWS.AppHost.csproj
+++ b/playground/AWS/AWS.AppHost/AWS.AppHost.csproj
@@ -6,6 +6,7 @@
enable
enable
true
+ $(NoWarn);CS8002
diff --git a/playground/AWS/AWSCDK.AppHost/.gitignore b/playground/AWS/AWSCDK.AppHost/.gitignore
new file mode 100644
index 00000000000..7c884c38240
--- /dev/null
+++ b/playground/AWS/AWSCDK.AppHost/.gitignore
@@ -0,0 +1,2 @@
+# AWS CDK
+cdk.out
\ No newline at end of file
diff --git a/playground/AWS/AWSCDK.AppHost/AWSCDK.AppHost.csproj b/playground/AWS/AWSCDK.AppHost/AWSCDK.AppHost.csproj
new file mode 100644
index 00000000000..c8d0246d7a0
--- /dev/null
+++ b/playground/AWS/AWSCDK.AppHost/AWSCDK.AppHost.csproj
@@ -0,0 +1,23 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ true
+ $(NoWarn);CS8002
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/playground/AWS/AWSCDK.AppHost/CustomStack.cs b/playground/AWS/AWSCDK.AppHost/CustomStack.cs
new file mode 100644
index 00000000000..541d0b48f99
--- /dev/null
+++ b/playground/AWS/AWSCDK.AppHost/CustomStack.cs
@@ -0,0 +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 Amazon.CDK;
+using Amazon.CDK.AWS.S3;
+using Constructs;
+
+namespace AWSCDK.AppHost;
+
+public class CustomStack : Stack
+{
+
+ public IBucket Bucket { get; }
+
+ public CustomStack(Construct scope, string id)
+ : base(scope, id)
+ {
+ Bucket = new Bucket(this, "Bucket");
+ }
+
+}
diff --git a/playground/AWS/AWSCDK.AppHost/Program.cs b/playground/AWS/AWSCDK.AppHost/Program.cs
new file mode 100644
index 00000000000..dd792a5ac69
--- /dev/null
+++ b/playground/AWS/AWSCDK.AppHost/Program.cs
@@ -0,0 +1,24 @@
+using Amazon;
+using AWSCDK.AppHost;
+
+var builder = DistributedApplication.CreateBuilder(args);
+
+// Setup a configuration for the AWS .NET SDK.
+var awsConfig = builder.AddAWSSDKConfig()
+ .WithProfile("default")
+ .WithRegion(RegionEndpoint.EUWest1);
+
+var stack = builder.AddAWSCDKStack("stack", "Aspire-stack").WithReference(awsConfig);
+var customStack = builder.AddAWSCDKStack("custom", scope => new CustomStack(scope, "Aspire-custom"));
+customStack.AddOutput("BucketName", stack => stack.Bucket.BucketName).WithReference(awsConfig);
+
+var topic = stack.AddSNSTopic("topic");
+var queue = stack.AddSQSQueue("queue");
+topic.AddSubscription(queue);
+
+builder.AddProject("frontend")
+ //.WithReference(stack) // Reference all outputs of a construct
+ .WithEnvironment("AWS__Resources__BucketName", customStack.GetOutput("BucketName")) // Reference a construct/stack output
+ .WithEnvironment("AWS__Resources__ChatTopicArn", topic, t => t.TopicArn);
+
+builder.Build().Run();
diff --git a/playground/AWS/AWSCDK.AppHost/Properties/launchSettings.json b/playground/AWS/AWSCDK.AppHost/Properties/launchSettings.json
new file mode 100644
index 00000000000..a9449b4c094
--- /dev/null
+++ b/playground/AWS/AWSCDK.AppHost/Properties/launchSettings.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:15887;http://localhost:15888",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16175",
+ "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:17037"
+ }
+ },
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:15888",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16175",
+ "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:17038",
+ "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true"
+ }
+ },
+ "generate-manifest": {
+ "commandName": "Project",
+ "commandLineArgs": "--publisher manifest --output-path ./aspire-manifest.json",
+ "dotnetRunMessages": true,
+ "applicationUrl": "http://localhost:15888",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16175"
+ }
+ }
+ }
+}
diff --git a/playground/AWS/AWSCDK.AppHost/WebAppStack.cs b/playground/AWS/AWSCDK.AppHost/WebAppStack.cs
new file mode 100644
index 00000000000..2c1b4e404cc
--- /dev/null
+++ b/playground/AWS/AWSCDK.AppHost/WebAppStack.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Amazon.CDK;
+using Amazon.CDK.AWS.DynamoDB;
+using Constructs;
+using Attribute = Amazon.CDK.AWS.DynamoDB.Attribute;
+
+namespace AWSCDK.AppHost;
+
+public class WebAppStackProps : StackProps;
+
+public class WebAppStack : Stack
+{
+ public ITable Table { get; }
+
+ public WebAppStack(Construct scope, string id, WebAppStackProps props)
+ : base(scope, id, props)
+ {
+ Table = new Table(this, "Table", new TableProps
+ {
+ PartitionKey = new Attribute { Name = "id", Type = AttributeType.STRING },
+ BillingMode = BillingMode.PAY_PER_REQUEST
+ });
+ }
+}
diff --git a/playground/AWS/AWSCDK.AppHost/aspire-manifest.json b/playground/AWS/AWSCDK.AppHost/aspire-manifest.json
new file mode 100644
index 00000000000..c3cf57f1743
--- /dev/null
+++ b/playground/AWS/AWSCDK.AppHost/aspire-manifest.json
@@ -0,0 +1,50 @@
+{
+ "$schema": "https://json.schemastore.org/aspire-8.0.json",
+ "resources": {
+ "stack": {
+ "type": "aws.cloudformation.template.v0",
+ "stack-name": "Aspire-stack",
+ "template-path": "cdk.out/Aspire-stack.template.json",
+ "references": [
+ {
+ "target-resource": "frontend"
+ }
+ ]
+ },
+ "custom": {
+ "type": "aws.cloudformation.template.v0",
+ "stack-name": "Aspire-custom",
+ "template-path": "cdk.out/Aspire-custom.template.json",
+ "references": [
+ {
+ "target-resource": "frontend"
+ }
+ ]
+ },
+ "frontend": {
+ "type": "project.v0",
+ "path": "../Frontend/Frontend.csproj",
+ "env": {
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
+ "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
+ "HTTP_PORTS": "{frontend.bindings.http.targetPort}",
+ "AWS__Resources__BucketName": "{custom.output.BucketName}",
+ "AWS__Resources__ChatTopicArn": "{stack.output.topic8C050C71AWSResourcesChatTopicArn}"
+ },
+ "bindings": {
+ "http": {
+ "scheme": "http",
+ "protocol": "tcp",
+ "transport": "http"
+ },
+ "https": {
+ "scheme": "https",
+ "protocol": "tcp",
+ "transport": "http"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/playground/AWS/AWSCDK.AppHost/cdk.json b/playground/AWS/AWSCDK.AppHost/cdk.json
new file mode 100644
index 00000000000..6baff08c68b
--- /dev/null
+++ b/playground/AWS/AWSCDK.AppHost/cdk.json
@@ -0,0 +1,63 @@
+{
+ "app": "dotnet run -- --publisher manifest --output-path ./aspire-manifest.json",
+ "watch": {
+ "include": [
+ "**"
+ ],
+ "exclude": [
+ "README.md",
+ "cdk*.json",
+ "src/*/obj",
+ "src/*/bin",
+ "src/*.sln",
+ "src/*/GlobalSuppressions.cs",
+ "src/*/*.csproj"
+ ]
+ },
+ "context": {
+ "@aws-cdk/aws-lambda:recognizeLayerVersion": true,
+ "@aws-cdk/core:checkSecretUsage": true,
+ "@aws-cdk/core:target-partitions": [
+ "aws",
+ "aws-cn"
+ ],
+ "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
+ "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
+ "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
+ "@aws-cdk/aws-iam:minimizePolicies": true,
+ "@aws-cdk/core:validateSnapshotRemovalPolicy": true,
+ "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
+ "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
+ "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
+ "@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
+ "@aws-cdk/core:enablePartitionLiterals": true,
+ "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
+ "@aws-cdk/aws-iam:standardizedServicePrincipals": true,
+ "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
+ "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
+ "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
+ "@aws-cdk/aws-route53-patters:useCertificate": true,
+ "@aws-cdk/customresources:installLatestAwsSdkDefault": false,
+ "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
+ "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
+ "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
+ "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
+ "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
+ "@aws-cdk/aws-redshift:columnId": true,
+ "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
+ "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
+ "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
+ "@aws-cdk/aws-kms:aliasNameRef": true,
+ "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
+ "@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
+ "@aws-cdk/aws-efs:denyAnonymousAccess": true,
+ "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
+ "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
+ "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true,
+ "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true,
+ "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true,
+ "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
+ "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true,
+ "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true
+ }
+}
diff --git a/src/Aspire.Hosting.AWS/AWSLifecycleHook.cs b/src/Aspire.Hosting.AWS/AWSLifecycleHook.cs
new file mode 100644
index 00000000000..14c58d6824f
--- /dev/null
+++ b/src/Aspire.Hosting.AWS/AWSLifecycleHook.cs
@@ -0,0 +1,178 @@
+// 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.AWS.CDK;
+using Aspire.Hosting.AWS.Provisioning;
+using Aspire.Hosting.Lifecycle;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Aspire.Hosting.AWS;
+
+internal sealed class AWSLifecycleHook(
+ DistributedApplicationExecutionContext executionContext,
+ IServiceProvider serviceProvider,
+ ResourceNotificationService notificationService,
+ ResourceLoggerService loggerService) : IDistributedApplicationLifecycleHook
+{
+ public Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
+ {
+ var awsResources = appModel.Resources.OfType().ToList();
+ if (awsResources.Count == 0) // Skip when no AWS resources are found
+ {
+ return Task.CompletedTask;
+ }
+
+ // Create a lookup for all resources implementing IResourceWithParent and have IAWSResource as parent in the tree.
+ // Typical children that are listed here are IStackResource with IConstructResource as children.
+ // This is important for state reporting so that a stack and it child resources are handled.
+ var parentChildLookup = appModel.Resources.OfType()
+ .Select(x => (Child: x, Root: x.Parent.TrySelectParentResource()))
+ .Where(x => x.Root is not null)
+ .ToLookup(x => x.Root, x => x.Child);
+
+ // Synthesize AWS CDK resources before provisioning or writing the manifest
+ SynthesizeAWSCDKResources(awsResources, parentChildLookup);
+
+ // Provisioning resources is fully async, so we can just fire and forget
+ _ = Task.Run(() => ProvisionAWSResourcesAsync(awsResources, parentChildLookup, cancellationToken), cancellationToken);
+ return Task.CompletedTask;
+ }
+
+ private static void SynthesizeAWSCDKResources(IList awsResources, ILookup parentChildLookup)
+ {
+ // Only look at StackResources
+ var stackResources = awsResources.OfType().ToList();
+ foreach (var stackResource in stackResources)
+ {
+ // Apply construct modifier annotations as some constructs needs te be altered after the fact, like adding outputs.
+ var constructResources = parentChildLookup[stackResource].OfType();
+ foreach (var constructResource in constructResources.Concat([stackResource]))
+ {
+ // Find Construct Modifier Annotations
+ if (!constructResource.TryGetAnnotationsOfType(out var modifiers))
+ {
+ continue;
+ }
+
+ // Modify stack
+ foreach (var modifier in modifiers)
+ {
+ modifier.ChangeConstruct(constructResource.Construct);
+ }
+ }
+ }
+
+ // Create a lookup for stack resources and their AWS CDK app
+ var appLookup = stackResources
+ .Select(r => (Child: r, r.App))
+ .ToLookup(r => r.App, r => r.Child);
+
+ foreach (var app in appLookup)
+ {
+ // Synthesize AWS CDK app
+ var cloudAssembly = app.Key.Synth();
+ // Attach the stack artifact to the stack resources for provisioning
+ foreach (var stackResource in app)
+ {
+ var stackArtifact = cloudAssembly.Stacks.FirstOrDefault(stack => stack.StackName == stackResource.StackName)
+ ?? throw new InvalidOperationException($"Stack '{stackResource.StackName}' not found in synthesized cloud assembly.");
+ // Annotate the resource with information for writing the manifest and provisioning.
+ stackResource.Annotations.Add(new CloudAssemblyResourceAnnotation(stackArtifact));
+ }
+ }
+ }
+
+ #region Provisioning
+
+ private async Task ProvisionAWSResourcesAsync(IList awsResources, ILookup parentChildLookup, CancellationToken cancellationToken)
+ {
+ // Skip when publishing, this is intended for provisioning only.
+ if (executionContext.IsPublishMode)
+ {
+ return;
+ }
+
+ // Mark all resources as starting
+ foreach (var r in awsResources)
+ {
+ r.ProvisioningTaskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ await UpdateStateAsync(r, parentChildLookup, s => s with
+ {
+ State = new ResourceStateSnapshot("Starting", KnownResourceStateStyles.Info)
+ }).ConfigureAwait(false);
+ }
+
+ foreach (var resource in awsResources)
+ {
+ // Resolve a provisioner for the AWS Resource
+ var provisioner = SelectProvisioner(resource);
+
+ var resourceLogger = loggerService.GetLogger(resource);
+
+ if (provisioner is null) // Skip when no provisioner is found
+ {
+ resource.ProvisioningTaskCompletionSource?.TrySetResult();
+
+ resourceLogger.LogWarning("No provisioner found for {ResourceType} skipping", resource.GetType().Name);
+ }
+ else
+ {
+ resourceLogger.LogInformation("Provisioning {ResourceName}...", resource.Name);
+
+ try
+ {
+ // Provision resources
+ await provisioner.GetOrCreateResourceAsync(resource, cancellationToken).ConfigureAwait(false);
+
+ // Mark resources as running
+ await UpdateStateAsync(resource, parentChildLookup, s => s with
+ {
+ State = new ResourceStateSnapshot("Running", KnownResourceStateStyles.Success)
+ }).ConfigureAwait(false);
+ resource.ProvisioningTaskCompletionSource?.TrySetResult();
+ }
+ catch (Exception ex)
+ {
+ resourceLogger.LogError(ex, "Error provisioning {ResourceName}", resource.Name);
+
+ // Mark resources as failed
+ await UpdateStateAsync(resource, parentChildLookup, s => s with
+ {
+ State = new ResourceStateSnapshot("Failed to Provision", KnownResourceStateStyles.Error)
+ }).ConfigureAwait(false);
+ resource.ProvisioningTaskCompletionSource?.TrySetException(ex);
+ }
+ }
+ }
+ }
+
+ private IAWSResourceProvisioner? SelectProvisioner(IAWSResource resource)
+ {
+ var type = resource.GetType();
+ while (type is not null) // Loop through all the base types to find a resource that as a provisioner
+ {
+ var provisioner = serviceProvider.GetKeyedService(type);
+ if (provisioner is not null)
+ {
+ return provisioner;
+ }
+ type = type.BaseType;
+ }
+ return null;
+ }
+
+ private async Task UpdateStateAsync(IAWSResource resource, ILookup parentChildLookup, Func stateFactory)
+ {
+ // Update the state of the IAWSRsource and all it's children
+ await notificationService.PublishUpdateAsync(resource, stateFactory).ConfigureAwait(false);
+ foreach (var child in parentChildLookup[resource])
+ {
+ await notificationService.PublishUpdateAsync(child, stateFactory).ConfigureAwait(false);
+ }
+ }
+
+ #endregion
+}
diff --git a/src/Aspire.Hosting.AWS/Aspire.Hosting.AWS.csproj b/src/Aspire.Hosting.AWS/Aspire.Hosting.AWS.csproj
index 2376b8377b3..0762bd0523f 100644
--- a/src/Aspire.Hosting.AWS/Aspire.Hosting.AWS.csproj
+++ b/src/Aspire.Hosting.AWS/Aspire.Hosting.AWS.csproj
@@ -7,8 +7,7 @@
true
aspire integration hosting aws
Add support for provisioning AWS application resources and configuring the AWS SDK for .NET.
-
- 8.0.1-preview.8.24267.1
+ $(NoWarn);CS8002
@@ -21,6 +20,7 @@
+
diff --git a/src/Aspire.Hosting.AWS/CDK/CDKExtensions.cs b/src/Aspire.Hosting.AWS/CDK/CDKExtensions.cs
new file mode 100644
index 00000000000..256bd4d10c7
--- /dev/null
+++ b/src/Aspire.Hosting.AWS/CDK/CDKExtensions.cs
@@ -0,0 +1,229 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Amazon.CDK;
+using Aspire.Hosting.ApplicationModel;
+using Aspire.Hosting.AWS;
+using Aspire.Hosting.AWS.CDK;
+using Aspire.Hosting.AWS.CloudFormation;
+using Constructs;
+using Environment = System.Environment;
+
+namespace Aspire.Hosting;
+
+///
+/// Extension methods for adding AWS CDK as a provisioning resources.
+///
+public static class CDKExtensions
+{
+ ///
+ /// Adds an AWS CDK stack as resource. The CloudFormation stack name will be the resource name prefixed with 'Aspire-'
+ ///
+ /// The .
+ /// The name of the stack resource.
+ public static IResourceBuilder AddAWSCDKStack(this IDistributedApplicationBuilder builder, string name)
+ => AddAWSCDKStack(builder, name, name);
+
+ ///
+ /// Adds an AWS CDK stack as resource.
+ ///
+ /// The .
+ /// The name of the stack resource.
+ /// Cloud Formation stack same if different from the resource name.
+ ///
+ public static IResourceBuilder AddAWSCDKStack(this IDistributedApplicationBuilder builder, string name,
+ string stackName)
+ {
+ builder.AddAWSProvisioning();
+ var resource = new StackResource(name, new Stack(ResolveCDKApp(builder), stackName));
+ return builder
+ .AddResource(resource)
+ .WithInitialState(new()
+ {
+ Properties = [],
+ ResourceType = GetResourceType(resource),
+ })
+ .WithManifestPublishingCallback(resource.WriteToManifest);
+ }
+
+ ///
+ /// Adds and build an AWS CDK stack as resource.
+ ///
+ /// The .
+ /// The name of the resource.
+ /// The stack builder delegate.
+ ///
+ public static IResourceBuilder> AddAWSCDKStack(this IDistributedApplicationBuilder builder,
+ string name, ConstructBuilderDelegate stackBuilder)
+ where T : Stack
+ {
+ builder.AddAWSProvisioning();
+ var resource = new StackResource(name, stackBuilder(ResolveCDKApp(builder)));
+ return builder
+ .AddResource(resource)
+ .WithInitialState(new()
+ {
+ Properties = [],
+ ResourceType = GetResourceType(resource),
+ })
+ .WithManifestPublishingCallback(resource.WriteToManifest);
+ }
+
+ ///
+ /// Adds and build an AWS CDK construct as resource.
+ ///
+ /// The construct resource builder.
+ /// The name of the resource.
+ /// The construct builder delegate.
+ ///
+ public static IResourceBuilder> AddConstruct(
+ this IResourceBuilder builder, string name,
+ ConstructBuilderDelegate constructBuilder)
+ where T : Construct
+ {
+ var parent = builder.Resource;
+ var resource = new ConstructResource(name, constructBuilder((Construct)parent.Construct), parent);
+ return builder.ApplicationBuilder
+ .AddResource(resource)
+ .WithInitialState(new()
+ {
+ Properties = [],
+ ResourceType = GetResourceType(resource),
+ })
+ .ExcludeFromManifest();
+ }
+
+ ///
+ /// Adds a stack reference to an output from the CloudFormation stack.
+ ///
+ /// The stack resource builder.
+ /// The name of the output.
+ /// The construct output delegate.
+ ///
+ ///
+ /// The following example shows creating a custom stack and reference the exposed ServiceUrl property
+ /// in a project.
+ ///
+ /// var service = builder
+ /// .AddStack("service", scope => new ServiceStack(scope, "ServiceStack")
+ /// .AddOutput("ServiceUrl", stack => stack.Service.ServiceUrl);
+ /// var api = builder.AddProject<Projects.Api>("api")
+ /// .AddReference(service);
+ ///
+ ///
+ public static IResourceBuilder> AddOutput(
+ this IResourceBuilder> builder,
+ string name, ConstructOutputDelegate output)
+ where TStack : Stack
+ {
+ return builder.WithAnnotation(new ConstructOutputAnnotation(name, output));
+ }
+
+ ///
+ /// Adds a construct reference to an output from the CloudFormation stack.
+ ///
+ /// The construct resource builder.
+ /// The name of the output.
+ /// The construct output delegate.
+ ///
+ ///
+ /// The following example shows creating a custom construct and reference the exposed ServiceUrl property
+ /// in a project.
+ ///
+ /// var service = stack
+ /// .AddConstruct("service", scope => new Service(scope, "service")
+ /// .AddOutput("ServiceUrl", construct => construct.ServiceUrl);
+ /// var api = builder.AddProject<Projects.Api>("api")
+ /// .AddReference(service);
+ ///
+ ///
+ public static IResourceBuilder> AddOutput(
+ this IResourceBuilder> builder,
+ string name, ConstructOutputDelegate output)
+ where T : Construct
+ {
+ return builder.WithAnnotation(new ConstructOutputAnnotation(name, output));
+ }
+
+ ///
+ /// Gets a reference to an output from the CloudFormation stack.
+ ///
+ /// The construct resource builder.
+ /// The name of the output.
+ /// The construct output delegate.
+ public static StackOutputReference GetOutput(this IResourceBuilder> builder, string name, ConstructOutputDelegate output)
+ where T : Construct
+ {
+ builder.WithAnnotation(new ConstructOutputAnnotation(name, output));
+ return new StackOutputReference(builder.Resource.Construct.GetStackUniqueId() + name, builder.Resource.Parent.SelectParentResource());
+ }
+
+ ///
+ /// Adds a reference of an AWS CDK construct to a project.The output parameters of the construct are added to the project IConfiguration.
+ ///
+ /// The builder for the resource.
+ /// The construct resource.
+ /// The construct output delegate.
+ /// The name of the construct output
+ /// The optional config section in IConfiguration to add the output parameters.
+ public static IResourceBuilder WithReference(this IResourceBuilder builder, IResourceBuilder> construct, ConstructOutputDelegate outputDelegate, string outputName, string? configSection = null)
+ where TConstruct : IConstruct
+ where TDestination : IResourceWithEnvironment
+ {
+ configSection ??= $"{Constants.DefaultConfigSection}:{construct.Resource.Name}";
+ var prefix = configSection.ToEnvironmentVariables();
+ return builder.WithEnvironment($"{prefix}__{outputName}", construct, outputDelegate, outputName);
+ }
+
+ ///
+ /// Add an environment variable with a reference of a AWS CDK construct to a project. The output parameters of the CloudFormation stack are added to the project IConfiguration.
+ ///
+ /// The resource builder.
+ /// The name of the environment variable.
+ /// The construct resource.
+ /// The construct output delegate.
+ /// The name of the construct output
+ /// ///
+ /// The following example shows creating a custom construct and reference the exposed ServiceUrl property
+ /// in a project as environment variable.
+ ///
+ /// var service = stack.AddConstruct("service", scope => new Service(scope, "service");
+ /// var api = builder.AddProject<Projects.Api>("api")
+ /// .WithEnvironment("Service_ServiceUrl", service, s => s.ServiceUrl);
+ ///
+ ///
+ public static IResourceBuilder WithEnvironment(this IResourceBuilder builder, string name, IResourceBuilder> construct, ConstructOutputDelegate outputDelegate, string? outputName = default)
+ where TConstruct : IConstruct
+ where TDestination : IResourceWithEnvironment
+ {
+ outputName ??= name.Replace("_", string.Empty);
+ if (construct.Resource.Annotations.OfType().All(annotation => annotation.OutputName != outputName))
+ {
+ construct.WithAnnotation(new ConstructOutputAnnotation(outputName, outputDelegate));
+ }
+ construct.WithAnnotation(new ConstructReferenceAnnotation(builder.Resource.Name, outputName));
+ return builder.WithEnvironment(name, new StackOutputReference(construct.Resource.Construct.GetStackUniqueId() + outputName, construct.Resource.Parent.SelectParentResource()));
+ }
+
+ private static string GetResourceType(IResourceWithConstruct constructResource)
+ where T : Construct
+ {
+ var constructType = constructResource.Construct.GetType();
+ var baseConstructType = typeof(T);
+ return constructType == baseConstructType ? baseConstructType.Name : constructType.Name;
+ }
+
+ ///
+ /// Lookup existing stack resources and reuse the AWS CDK app.
+ ///
+ ///
+ private static App ResolveCDKApp(IDistributedApplicationBuilder builder)
+ {
+ var stackResource = builder.Resources.OfType().FirstOrDefault();
+ if (stackResource != null)
+ {
+ return (App)stackResource.Stack.Node.Root;
+ }
+ return new App(new AppProps() { Outdir = Path.Combine(Environment.CurrentDirectory, "cdk.out") });
+ }
+}
diff --git a/src/Aspire.Hosting.AWS/CDK/CloudAssemblyResourceAnnotation.cs b/src/Aspire.Hosting.AWS/CDK/CloudAssemblyResourceAnnotation.cs
new file mode 100644
index 00000000000..d63773e80f9
--- /dev/null
+++ b/src/Aspire.Hosting.AWS/CDK/CloudAssemblyResourceAnnotation.cs
@@ -0,0 +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 System.Diagnostics;
+using Amazon.CDK.CXAPI;
+using Aspire.Hosting.ApplicationModel;
+
+namespace Aspire.Hosting.AWS.CDK;
+
+///
+/// Annotations that stores the to the stack resource.
+///
+///
+[DebuggerDisplay("Type = {GetType().Name,nq}, StackName = {StackArtifact.StackName}")]
+internal sealed class CloudAssemblyResourceAnnotation(CloudFormationStackArtifact stackArtifact) : IResourceAnnotation
+{
+ public CloudFormationStackArtifact StackArtifact { get; } = stackArtifact;
+}
diff --git a/src/Aspire.Hosting.AWS/CDK/ConstructBuilderDelegate.cs b/src/Aspire.Hosting.AWS/CDK/ConstructBuilderDelegate.cs
new file mode 100644
index 00000000000..a99587fb2df
--- /dev/null
+++ b/src/Aspire.Hosting.AWS/CDK/ConstructBuilderDelegate.cs
@@ -0,0 +1,12 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Constructs;
+
+namespace Aspire.Hosting.AWS.CDK;
+
+///
+/// Delegate for building an AWS CDK construct
+///
+/// Construct type
+public delegate T ConstructBuilderDelegate(Construct scope) where T : IConstruct;
diff --git a/src/Aspire.Hosting.AWS/CDK/ConstructOutputAnnotation.cs b/src/Aspire.Hosting.AWS/CDK/ConstructOutputAnnotation.cs
new file mode 100644
index 00000000000..428c30c3b4e
--- /dev/null
+++ b/src/Aspire.Hosting.AWS/CDK/ConstructOutputAnnotation.cs
@@ -0,0 +1,31 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Amazon.CDK;
+using Constructs;
+
+namespace Aspire.Hosting.AWS.CDK;
+
+internal sealed class ConstructOutputAnnotation(string name, ConstructOutputDelegate output)
+ : IConstructModifierAnnotation, IConstructOutputAnnotation
+ where T : IConstruct
+{
+ public string OutputName { get; } = name;
+
+ ///
+ public void ChangeConstruct(IConstruct construct)
+ {
+ // Find the stack where this construct belongs to.
+ if (construct is not Stack stack)
+ {
+ stack = construct.Node.Scopes.OfType().FirstOrDefault() ?? throw new InvalidOperationException("Construct is not part of a Stack");
+ }
+
+ // Add a CloudFormation output on the stack referencing the construct and the resolved value.
+ _ = new CfnOutput(stack, OutputName, new CfnOutputProps
+ {
+ Key = $"{construct.GetStackUniqueId()}{OutputName}",
+ Value = output((T)construct)
+ });
+ }
+}
diff --git a/src/Aspire.Hosting.AWS/CDK/ConstructOutputDelegate.cs b/src/Aspire.Hosting.AWS/CDK/ConstructOutputDelegate.cs
new file mode 100644
index 00000000000..6524e0cbfe0
--- /dev/null
+++ b/src/Aspire.Hosting.AWS/CDK/ConstructOutputDelegate.cs
@@ -0,0 +1,11 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+using Constructs;
+
+namespace Aspire.Hosting.AWS.CDK;
+
+///
+/// Delegate for resolving outputs of a construct.
+///
+/// Construct type
+public delegate string ConstructOutputDelegate(T construct) where T : IConstruct;
diff --git a/src/Aspire.Hosting.AWS/CDK/ConstructReferenceAnnotation.cs b/src/Aspire.Hosting.AWS/CDK/ConstructReferenceAnnotation.cs
new file mode 100644
index 00000000000..89ab12aa584
--- /dev/null
+++ b/src/Aspire.Hosting.AWS/CDK/ConstructReferenceAnnotation.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using Aspire.Hosting.ApplicationModel;
+
+namespace Aspire.Hosting.AWS.CDK;
+
+///
+/// Annotations that records a reference of Construct resources to target resources like projects.
+///
+///
+///
+[DebuggerDisplay("Type = {GetType().Name,nq}, TargetResource = {TargetResource}, OutputName = {OutputName}")]
+internal sealed class ConstructReferenceAnnotation(string targetResource, string outputName) : IResourceAnnotation
+{
+ ///
+ /// The name of the target resource.
+ ///
+ internal string TargetResource { get; } = targetResource;
+
+ ///
+ /// The name of the output.
+ ///
+ internal string OutputName { get; } = outputName;
+}
diff --git a/src/Aspire.Hosting.AWS/CDK/ConstructResource.cs b/src/Aspire.Hosting.AWS/CDK/ConstructResource.cs
new file mode 100644
index 00000000000..5263a37f8f8
--- /dev/null
+++ b/src/Aspire.Hosting.AWS/CDK/ConstructResource.cs
@@ -0,0 +1,24 @@
+// 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 Constructs;
+
+namespace Aspire.Hosting.AWS.CDK;
+
+///
+internal class ConstructResource(string name, IConstruct construct, IResourceWithConstruct parent) : Resource(name), IConstructResource
+{
+ ///
+ public IConstruct Construct { get; } = construct;
+
+ ///
+ public IResourceWithConstruct Parent { get; } = parent;
+}
+
+///
+internal sealed class ConstructResource(string name, T construct, IResourceWithConstruct parent) : ConstructResource(name, construct, parent), IConstructResource
+ where T : IConstruct
+{
+ public new T Construct { get; } = construct;
+}
diff --git a/src/Aspire.Hosting.AWS/CDK/IConstructModifierAnnotation.cs b/src/Aspire.Hosting.AWS/CDK/IConstructModifierAnnotation.cs
new file mode 100644
index 00000000000..5c60248877d
--- /dev/null
+++ b/src/Aspire.Hosting.AWS/CDK/IConstructModifierAnnotation.cs
@@ -0,0 +1,25 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.ApplicationModel;
+using Constructs;
+
+namespace Aspire.Hosting.AWS.CDK;
+
+///
+/// Resource annotation to change an AWS CDK construct. When a construct is created it doesn't have all the information
+/// that it requires. This interface is implemented on resource annotations like
+/// to add additional outputs referencing construct.
+///
+///
+/// This interface is internal and is intended for use by the AWS CDK framework only.
+///
+internal interface IConstructModifierAnnotation : IResourceAnnotation
+{
+ ///
+ /// Changes the AWS CDK construct before starting the . This is useful for
+ /// altering the construct before it is synthesized, like adding additional outputs.
+ ///
+ /// Construct to be changed.
+ void ChangeConstruct(IConstruct construct);
+}
diff --git a/src/Aspire.Hosting.AWS/CDK/IConstructOutputAnnotation.cs b/src/Aspire.Hosting.AWS/CDK/IConstructOutputAnnotation.cs
new file mode 100644
index 00000000000..95685f8f37c
--- /dev/null
+++ b/src/Aspire.Hosting.AWS/CDK/IConstructOutputAnnotation.cs
@@ -0,0 +1,17 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Aspire.Hosting.AWS.CDK;
+
+///
+/// This interface is used to mark a construct resource so that it can be identified as an output
+/// during synthesis. This is needed in to map the output to the environment variables after synthesizing
+/// that would not normally be possible.
+///
+///
+/// This interface is internal and is intended for use by the AWS CDK framework only.
+///
+internal interface IConstructOutputAnnotation
+{
+ string OutputName { get; }
+}
diff --git a/src/Aspire.Hosting.AWS/CDK/IConstructResource.cs b/src/Aspire.Hosting.AWS/CDK/IConstructResource.cs
new file mode 100644
index 00000000000..729ba57c7b2
--- /dev/null
+++ b/src/Aspire.Hosting.AWS/CDK/IConstructResource.cs
@@ -0,0 +1,11 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.ApplicationModel;
+
+namespace Aspire.Hosting.AWS.CDK;
+
+///
+/// Resource representing an AWS CDK construct.
+///
+public interface IConstructResource : IResourceWithParent, IResourceWithConstruct;
diff --git a/src/Aspire.Hosting.AWS/CDK/IConstructResourceOfT.cs b/src/Aspire.Hosting.AWS/CDK/IConstructResourceOfT.cs
new file mode 100644
index 00000000000..dc2a1943b13
--- /dev/null
+++ b/src/Aspire.Hosting.AWS/CDK/IConstructResourceOfT.cs
@@ -0,0 +1,9 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Constructs;
+
+namespace Aspire.Hosting.AWS.CDK;
+
+///
+public interface IConstructResource : IConstructResource, IResourceWithConstruct where T : IConstruct;
diff --git a/src/Aspire.Hosting.AWS/CDK/IResourceWithConstruct.cs b/src/Aspire.Hosting.AWS/CDK/IResourceWithConstruct.cs
new file mode 100644
index 00000000000..76adcb60a67
--- /dev/null
+++ b/src/Aspire.Hosting.AWS/CDK/IResourceWithConstruct.cs
@@ -0,0 +1,18 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.ApplicationModel;
+using Constructs;
+
+namespace Aspire.Hosting.AWS.CDK;
+
+///
+/// Represents a resource that has an AWS CDK construct.
+///
+public interface IResourceWithConstruct : IResource
+{
+ ///
+ /// The AWS CDK construct
+ ///
+ IConstruct Construct { get; }
+}
diff --git a/src/Aspire.Hosting.AWS/CDK/IResourceWithConstructOfT.cs b/src/Aspire.Hosting.AWS/CDK/IResourceWithConstructOfT.cs
new file mode 100644
index 00000000000..d3bb69b928c
--- /dev/null
+++ b/src/Aspire.Hosting.AWS/CDK/IResourceWithConstructOfT.cs
@@ -0,0 +1,14 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Constructs;
+
+namespace Aspire.Hosting.AWS.CDK;
+
+///
+public interface IResourceWithConstruct : IResourceWithConstruct
+ where T : IConstruct
+{
+ ///
+ new T Construct { get; }
+}
diff --git a/src/Aspire.Hosting.AWS/CDK/IStackResource.cs b/src/Aspire.Hosting.AWS/CDK/IStackResource.cs
new file mode 100644
index 00000000000..4021608d02f
--- /dev/null
+++ b/src/Aspire.Hosting.AWS/CDK/IStackResource.cs
@@ -0,0 +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 Amazon.CDK;
+using Aspire.Hosting.AWS.CloudFormation;
+
+namespace Aspire.Hosting.AWS.CDK;
+
+///
+/// Resource representing an AWS CDK stack.
+///
+public interface IStackResource : ICloudFormationTemplateResource, IResourceWithConstruct
+{
+ ///
+ /// The AWS CDK stack
+ ///
+ Stack Stack { get; }
+}
diff --git a/src/Aspire.Hosting.AWS/CDK/IStackResourceOfT.cs b/src/Aspire.Hosting.AWS/CDK/IStackResourceOfT.cs
new file mode 100644
index 00000000000..85ad43a1677
--- /dev/null
+++ b/src/Aspire.Hosting.AWS/CDK/IStackResourceOfT.cs
@@ -0,0 +1,16 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Amazon.CDK;
+
+namespace Aspire.Hosting.AWS.CDK;
+
+///
+public interface IStackResource : IStackResource, IResourceWithConstruct
+ where T : Stack
+{
+ ///
+ /// The AWS CDK stack
+ ///
+ new T Stack { get; }
+}
diff --git a/src/Aspire.Hosting.AWS/CDK/Resources/CognitoResourceExtensions.cs b/src/Aspire.Hosting.AWS/CDK/Resources/CognitoResourceExtensions.cs
new file mode 100644
index 00000000000..37d5161794c
--- /dev/null
+++ b/src/Aspire.Hosting.AWS/CDK/Resources/CognitoResourceExtensions.cs
@@ -0,0 +1,54 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Amazon.CDK.AWS.Cognito;
+using Aspire.Hosting.ApplicationModel;
+using Aspire.Hosting.AWS;
+using Aspire.Hosting.AWS.CDK;
+
+namespace Aspire.Hosting;
+
+///
+/// Provides extension methods for adding Amazon Cognito resources to the application model.
+///
+public static class CognitoResourceExtensions
+{
+
+ private const string UserPoolIdOutputName = "UserPoolId";
+
+ ///
+ /// Adds an Amazon Cognito user pool.
+ ///
+ /// The builder for the AWS CDK stack.
+ /// The name of the resource.
+ /// The properties of the userpool.
+ public static IResourceBuilder> AddCognitoUserPool(this IResourceBuilder builder, string name, IUserPoolProps? props = null)
+ {
+ return builder.AddConstruct(name, scope => new UserPool(scope, name, props));
+ }
+
+ ///
+ /// Adds an Amazon Cognito user pool client.
+ ///
+ /// The builder for the user pool.
+ /// the name of the resource.
+ /// The options of the client.
+ public static IResourceBuilder> AddClient(this IResourceBuilder> builder, string name, IUserPoolClientOptions? options = null)
+ {
+ return builder.AddConstruct(name, _ => builder.Resource.Construct.AddClient(name, options));
+ }
+
+ ///
+ /// Adds a reference of an Amazon Cognito user pool to a project. The output parameters of the user pool are added to the project IConfiguration.
+ ///
+ /// The builder for the resource.
+ /// The Amazon Cognito user pool resource.
+ /// The optional config section in IConfiguration to add the output parameters.
+ public static IResourceBuilder WithReference(this IResourceBuilder builder, IResourceBuilder> userPool, string? configSection = null)
+ where TDestination : IResourceWithEnvironment
+ {
+ configSection ??= $"{Constants.DefaultConfigSection}:{userPool.Resource.Name}";
+ var prefix = configSection.ToEnvironmentVariables();
+ return builder.WithEnvironment($"{prefix}__{UserPoolIdOutputName}", userPool, p => p.UserPoolId, UserPoolIdOutputName);
+ }
+}
diff --git a/src/Aspire.Hosting.AWS/CDK/Resources/DynamoDBResourceExtensions.cs b/src/Aspire.Hosting.AWS/CDK/Resources/DynamoDBResourceExtensions.cs
new file mode 100644
index 00000000000..7a277f969f3
--- /dev/null
+++ b/src/Aspire.Hosting.AWS/CDK/Resources/DynamoDBResourceExtensions.cs
@@ -0,0 +1,65 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Amazon.CDK.AWS.DynamoDB;
+using Aspire.Hosting.ApplicationModel;
+using Aspire.Hosting.AWS;
+using Aspire.Hosting.AWS.CDK;
+
+namespace Aspire.Hosting;
+
+///
+/// Provides extension methods for adding Amazon DynamoDB resources to the application model.
+///
+public static class DynamoDBResourceExtensions
+{
+
+ private const string TableNameOutputName = "TableName";
+
+ ///
+ /// Adds an Amazon DynamoDB table.
+ ///
+ /// The builder for the AWS CDK stack.
+ /// The name of the resource.
+ /// The properties of the table.
+ public static IResourceBuilder> AddDynamoDBTable(this IResourceBuilder builder, string name, ITableProps props)
+ {
+ return builder.AddConstruct(name, scope => new Table(scope, name, props));
+ }
+
+ ///
+ /// Adds an global secondary index to the table.
+ ///
+ /// The builder for the table resource.
+ /// The properties for the global secondary index.
+ public static IResourceBuilder> AddGlobalSecondaryIndex(this IResourceBuilder> builder, IGlobalSecondaryIndexProps props)
+ {
+ builder.Resource.Construct.AddGlobalSecondaryIndex(props);
+ return builder;
+ }
+
+ ///
+ /// Adds a local secondary index to the table.
+ ///
+ /// The builder for the table resource.
+ /// The properties for the local secondary index.
+ public static IResourceBuilder> AddLocalSecondaryIndex(this IResourceBuilder> builder, ILocalSecondaryIndexProps props)
+ {
+ builder.Resource.Construct.AddLocalSecondaryIndex(props);
+ return builder;
+ }
+
+ ///
+ /// Adds a reference of an Amazon DynamoDB table to a project. The output parameters of the table are added to the project IConfiguration.
+ ///
+ /// The builder for the resource.
+ /// The Amazon DynamoDB table resource.
+ /// The optional config section in IConfiguration to add the output parameters.
+ public static IResourceBuilder WithReference(this IResourceBuilder builder, IResourceBuilder> table, string? configSection = null)
+ where TDestination : IResourceWithEnvironment
+ {
+ configSection ??= $"{Constants.DefaultConfigSection}:{table.Resource.Name}";
+ var prefix = configSection.ToEnvironmentVariables();
+ return builder.WithEnvironment($"{prefix}__{TableNameOutputName}", table, t => t.TableName, TableNameOutputName);
+ }
+}
diff --git a/src/Aspire.Hosting.AWS/CDK/Resources/KinesisResourceExtensions.cs b/src/Aspire.Hosting.AWS/CDK/Resources/KinesisResourceExtensions.cs
new file mode 100644
index 00000000000..ecfb14a8c5d
--- /dev/null
+++ b/src/Aspire.Hosting.AWS/CDK/Resources/KinesisResourceExtensions.cs
@@ -0,0 +1,44 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Amazon.CDK.AWS.Kinesis;
+using Aspire.Hosting.ApplicationModel;
+using Aspire.Hosting.AWS;
+using Aspire.Hosting.AWS.CDK;
+using Stream = Amazon.CDK.AWS.Kinesis.Stream;
+
+namespace Aspire.Hosting;
+
+///
+/// Provides extension methods for adding Amazon SNS resources to the application model.
+///
+public static class KinesisResourceExtensions
+{
+
+ private const string StreamArnOutputName = "StreamArn";
+
+ ///
+ /// Adds an Amazon Kinesis stream.
+ ///
+ /// The builder for the AWS CDK stack.
+ /// The name of the resource.
+ /// The properties of the stream.
+ public static IResourceBuilder> AddKinesisStream(this IResourceBuilder builder, string name, IStreamProps? props = null)
+ {
+ return builder.AddConstruct(name, scope => new Stream(scope, name, props));
+ }
+
+ ///
+ /// Adds a reference of an Amazon Kinesis stream to a project. The output parameters of the stream are added to the project IConfiguration.
+ ///
+ /// The builder for the resource.
+ /// The Amazon Kinesis stream resource.
+ /// The optional config section in IConfiguration to add the output parameters.
+ public static IResourceBuilder WithReference(this IResourceBuilder builder, IResourceBuilder> stream, string? configSection = null)
+ where TDestination : IResourceWithEnvironment
+ {
+ configSection ??= $"{Constants.DefaultConfigSection}:{stream.Resource.Name}";
+ var prefix = configSection.ToEnvironmentVariables();
+ return builder.WithEnvironment($"{prefix}__{StreamArnOutputName}", stream, s => s.StreamArn, StreamArnOutputName);
+ }
+}
diff --git a/src/Aspire.Hosting.AWS/CDK/Resources/S3ResourceExtensions.cs b/src/Aspire.Hosting.AWS/CDK/Resources/S3ResourceExtensions.cs
new file mode 100644
index 00000000000..802df8da0f2
--- /dev/null
+++ b/src/Aspire.Hosting.AWS/CDK/Resources/S3ResourceExtensions.cs
@@ -0,0 +1,97 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Amazon.CDK.AWS.S3;
+using Amazon.CDK.AWS.S3.Notifications;
+using Amazon.CDK.AWS.SNS;
+using Amazon.CDK.AWS.SQS;
+using Aspire.Hosting.ApplicationModel;
+using Aspire.Hosting.AWS;
+using Aspire.Hosting.AWS.CDK;
+
+namespace Aspire.Hosting;
+
+///
+/// Provides extension methods for adding Amazon S3 resources to the application model.
+///
+public static class S3ResourceExtensions
+{
+
+ private const string BucketNameOutputName = "BucketName";
+
+ ///
+ /// Adds an Amazon S3 bucket.
+ ///
+ /// The builder for the AWS CDK stack.
+ /// The name of the resource.
+ /// The properties of the bucket.
+ public static IResourceBuilder> AddS3Bucket(this IResourceBuilder builder, string name, IBucketProps? props = null)
+ {
+ return builder.AddConstruct(name, scope => new Bucket(scope, name, props));
+ }
+
+ /// Subscribes a destination to receive notifications when an object is created in the bucket.
+ /// The builder for the bucket resource.
+ /// The notification destination queue.
+ /// The type of bucket event.
+ /// Filters.
+ public static IResourceBuilder> AddEventNotification(this IResourceBuilder> builder, IResourceBuilder> destination, EventType eventType, params INotificationKeyFilter[] filters)
+ {
+ builder.Resource.Construct.AddEventNotification(eventType, new SqsDestination(destination.Resource.Construct), filters);
+ return builder;
+ }
+
+ /// Subscribes a destination to receive notifications when an object is created in the bucket.
+ /// The builder for the bucket resource.
+ /// The notification destination queue.
+ /// Filters.
+ public static IResourceBuilder> AddObjectCreatedNotification(this IResourceBuilder> builder, IResourceBuilder> destination, params INotificationKeyFilter[] filters)
+ {
+ builder.Resource.Construct.AddObjectCreatedNotification(new SqsDestination(destination.Resource.Construct), filters);
+ return builder;
+ }
+
+ /// Subscribes a destination to receive notifications when an object is created in the bucket.
+ /// The builder for the bucket resource.
+ /// The notification destination topic.
+ /// Filters.
+ public static IResourceBuilder> AddObjectCreatedNotification(this IResourceBuilder> builder, IResourceBuilder> destination, params INotificationKeyFilter[] filters)
+ {
+ builder.Resource.Construct.AddObjectCreatedNotification(new SnsDestination(destination.Resource.Construct), filters);
+ return builder;
+ }
+
+ /// Subscribes a destination to receive notifications when an object is removed from the bucket.
+ /// The builder for the bucket resource.
+ /// The notification destination queue.
+ /// Filters.
+ public static IResourceBuilder> AddObjectRemovedNotification(this IResourceBuilder> builder, IResourceBuilder> destination, params INotificationKeyFilter[] filters)
+ {
+ builder.Resource.Construct.AddObjectCreatedNotification(new SqsDestination(destination.Resource.Construct), filters);
+ return builder;
+ }
+
+ /// Subscribes a destination to receive notifications when an object is removed from the bucket.
+ /// The builder for the bucket resource.
+ /// The notification destination topic.
+ /// Filters.
+ public static IResourceBuilder> AddObjectRemovedNotification(this IResourceBuilder> builder, IResourceBuilder> destination, params INotificationKeyFilter[] filters)
+ {
+ builder.Resource.Construct.AddObjectRemovedNotification(new SnsDestination(destination.Resource.Construct), filters);
+ return builder;
+ }
+
+ ///
+ /// Adds a reference of an Amazon S3 bucket to a project. The output parameters of the bucket are added to the project IConfiguration.
+ ///
+ /// The builder for the resource.
+ /// The Amazon S3 bucket resource.
+ /// The optional config section in IConfiguration to add the output parameters.
+ public static IResourceBuilder WithReference(this IResourceBuilder builder, IResourceBuilder> bucket, string? configSection = null)
+ where TDestination : IResourceWithEnvironment
+ {
+ configSection ??= $"{Constants.DefaultConfigSection}:{bucket.Resource.Name}";
+ var prefix = configSection.ToEnvironmentVariables();
+ return builder.WithEnvironment($"{prefix}__{BucketNameOutputName}", bucket, b => b.BucketName, BucketNameOutputName);
+ }
+}
diff --git a/src/Aspire.Hosting.AWS/CDK/Resources/SNSResourceExtensions.cs b/src/Aspire.Hosting.AWS/CDK/Resources/SNSResourceExtensions.cs
new file mode 100644
index 00000000000..c1403359543
--- /dev/null
+++ b/src/Aspire.Hosting.AWS/CDK/Resources/SNSResourceExtensions.cs
@@ -0,0 +1,55 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Amazon.CDK.AWS.SNS;
+using Amazon.CDK.AWS.SNS.Subscriptions;
+using Amazon.CDK.AWS.SQS;
+using Aspire.Hosting.ApplicationModel;
+using Aspire.Hosting.AWS;
+using Aspire.Hosting.AWS.CDK;
+
+namespace Aspire.Hosting;
+
+///
+/// Provides extension methods for adding Amazon SNS resources to the application model.
+///
+public static class SNSResourceExtensions
+{
+
+ private const string TopicArnOutputName = "TopicArn";
+
+ ///
+ /// Adds an Amazon SNS topic.
+ ///
+ /// The builder for the AWS CDK stack.
+ /// The name of the resource.
+ /// The properties of the topic.
+ public static IResourceBuilder> AddSNSTopic(this IResourceBuilder builder, string name, ITopicProps? props = null)
+ {
+ return builder.AddConstruct(name, scope => new Topic(scope, name, props));
+ }
+
+ /// Subscribe some endpoint to this topic.
+ /// The builder for the topic resource.
+ /// The notification destination queue.
+ /// >Properties for an SQS subscription.
+ public static IResourceBuilder> AddSubscription(this IResourceBuilder> builder, IResourceBuilder> destination, SqsSubscriptionProps? props = null)
+ {
+ builder.Resource.Construct.AddSubscription(new SqsSubscription(destination.Resource.Construct, props));
+ return builder;
+ }
+
+ ///
+ /// Adds a reference of an Amazon SNS topic to a project. The output parameters of the topic are added to the project IConfiguration.
+ ///
+ /// The builder for the resource.
+ /// The Amazon SNS topic resource.
+ /// The optional config section in IConfiguration to add the output parameters.
+ public static IResourceBuilder WithReference(this IResourceBuilder builder, IResourceBuilder> topic, string? configSection = null)
+ where TDestination : IResourceWithEnvironment
+ {
+ configSection ??= $"{Constants.DefaultConfigSection}:{topic.Resource.Name}";
+ var prefix = configSection.ToEnvironmentVariables();
+ return builder.WithEnvironment($"{prefix}__{TopicArnOutputName}", topic, t => t.TopicArn, TopicArnOutputName);
+ }
+}
diff --git a/src/Aspire.Hosting.AWS/CDK/Resources/SQSResourceExtensions.cs b/src/Aspire.Hosting.AWS/CDK/Resources/SQSResourceExtensions.cs
new file mode 100644
index 00000000000..ff6b4062fa6
--- /dev/null
+++ b/src/Aspire.Hosting.AWS/CDK/Resources/SQSResourceExtensions.cs
@@ -0,0 +1,43 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Amazon.CDK.AWS.SQS;
+using Aspire.Hosting.ApplicationModel;
+using Aspire.Hosting.AWS;
+using Aspire.Hosting.AWS.CDK;
+
+namespace Aspire.Hosting;
+
+///
+/// Provides extension methods for adding Amazon SQS resources to the application model.
+///
+public static class SQSResourceExtensions
+{
+
+ private const string QueueUrlOutputName = "QueueUrl";
+
+ ///
+ /// Adds an Amazon SQS queue.
+ ///
+ /// The builder for the AWS CDK stack.
+ /// The name of the resource.
+ /// The properties of the queue.
+ public static IResourceBuilder> AddSQSQueue(this IResourceBuilder builder, string name, IQueueProps? props = null)
+ {
+ return builder.AddConstruct(name, scope => new Queue(scope, name, props));
+ }
+
+ ///
+ /// Adds a reference of an Amazon SQS queue to a project. The output parameters of the queue are added to the project IConfiguration.
+ ///
+ /// The builder for the resource.
+ /// The Amazon SQS queue resource.
+ /// The optional config section in IConfiguration to add the output parameters.
+ public static IResourceBuilder WithReference(this IResourceBuilder builder, IResourceBuilder> queue, string? configSection = null)
+ where TDestination : IResourceWithEnvironment
+ {
+ configSection ??= $"{Constants.DefaultConfigSection}:{queue.Resource.Name}";
+ var prefix = configSection.ToEnvironmentVariables();
+ return builder.WithEnvironment($"{prefix}__{QueueUrlOutputName}", queue, q => q.QueueUrl, QueueUrlOutputName);
+ }
+}
diff --git a/src/Aspire.Hosting.AWS/CDK/StackResource.cs b/src/Aspire.Hosting.AWS/CDK/StackResource.cs
new file mode 100644
index 00000000000..203d7563a61
--- /dev/null
+++ b/src/Aspire.Hosting.AWS/CDK/StackResource.cs
@@ -0,0 +1,32 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Amazon.CDK;
+using Aspire.Hosting.AWS.CloudFormation;
+using Constructs;
+using Stack = Amazon.CDK.Stack;
+
+namespace Aspire.Hosting.AWS.CDK;
+
+///
+internal class StackResource(string name, Stack stack) : CloudFormationTemplateResource(name, stack.StackName, stack.GetTemplatePath()), IStackResource
+{
+ ///
+ public Stack Stack { get; } = stack;
+
+ ///
+ public IConstruct Construct => Stack;
+
+ ///
+ /// The AWS CDK App the stack belongs to. This is needed for building the AWS CDK app tree.
+ ///
+ public App App => (App)Stack.Node.Root;
+}
+
+///
+internal sealed class StackResource(string name, T stack) : StackResource(name, stack), IStackResource
+ where T : Stack
+{
+ public new T Stack { get; } = stack;
+ public new T Construct => Stack;
+}
diff --git a/src/Aspire.Hosting.AWS/CDK/Utils/ConstructExtensions.cs b/src/Aspire.Hosting.AWS/CDK/Utils/ConstructExtensions.cs
new file mode 100644
index 00000000000..fc363648ad4
--- /dev/null
+++ b/src/Aspire.Hosting.AWS/CDK/Utils/ConstructExtensions.cs
@@ -0,0 +1,37 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Amazon.CDK;
+using Constructs;
+
+namespace Aspire.Hosting.AWS.CDK;
+
+internal static class ConstructExtensions
+{
+ ///
+ /// Returns the computed unique ID of a construct in the stack.
+ ///
+ /// The construct in the stack
+ public static string GetStackUniqueId(this IConstruct construct)
+ {
+ var stack = construct.GetStack();
+ return stack is null ? Names.UniqueId(construct) : Names.UniqueId(construct).TrimStart(Names.UniqueId(stack));
+ }
+
+ ///
+ /// Returns the stack of the construct.
+ ///
+ /// The construct in the stack
+ private static Stack? GetStack(this IConstruct construct)
+ => construct.Node.Scopes.OfType().FirstOrDefault();
+
+ ///
+ /// Returns the path of the CloudFormation template file for this stack.
+ ///
+ /// The stack
+ /// The path of the CloudFormation template file for this stack
+ public static string GetTemplatePath(this Stack stack)
+ {
+ return Path.Combine(((App)stack.Node.Root).Outdir, $"{stack.StackName}.template.json");
+ }
+}
diff --git a/src/Aspire.Hosting.AWS/CDK/Utils/ResourceExtensions.cs b/src/Aspire.Hosting.AWS/CDK/Utils/ResourceExtensions.cs
new file mode 100644
index 00000000000..986334b36ce
--- /dev/null
+++ b/src/Aspire.Hosting.AWS/CDK/Utils/ResourceExtensions.cs
@@ -0,0 +1,28 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+using Amazon.CDK.CXAPI;
+using Aspire.Hosting.ApplicationModel;
+
+namespace Aspire.Hosting.AWS.CDK;
+
+internal static class ResourceExtensions
+{
+ ///
+ /// Resolves a from a resource.
+ ///
+ ///
+ ///
+ /// False when no is not found as annotation of the resource.
+ public static bool TryGetStackArtifact(this IStackResource resource, [NotNullWhen(true)] out CloudFormationStackArtifact? stackArtifact)
+ {
+ stackArtifact = default;
+ if (!resource.TryGetAnnotationsOfType(out var annotations))
+ {
+ return false;
+ }
+ stackArtifact = annotations.First().StackArtifact;
+ return true;
+ }
+}
diff --git a/src/Aspire.Hosting.AWS/CloudFormation/CloudFormationExtensions.cs b/src/Aspire.Hosting.AWS/CloudFormation/CloudFormationExtensions.cs
index 88161e528e9..d0a60dbf5fc 100644
--- a/src/Aspire.Hosting.AWS/CloudFormation/CloudFormationExtensions.cs
+++ b/src/Aspire.Hosting.AWS/CloudFormation/CloudFormationExtensions.cs
@@ -5,7 +5,6 @@
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.AWS;
using Aspire.Hosting.AWS.CloudFormation;
-using Aspire.Hosting.Lifecycle;
using Microsoft.Extensions.Logging;
namespace Aspire.Hosting;
@@ -19,17 +18,22 @@ public static class CloudFormationExtensions
/// Add a CloudFormation stack for provisioning application resources.
///
/// The .
- /// The name of the CloudFormation stack.
+ /// The name of the resource.
+ /// The name of the CloudFormation stack. If not specified, the CloudFormation stack name will be the resource name prefixed with 'Aspire-'
/// The path to the CloudFormation template that defines the CloudFormation stack.
///
- public static IResourceBuilder AddAWSCloudFormationTemplate(this IDistributedApplicationBuilder builder, string stackName, string templatePath)
+ public static IResourceBuilder AddAWSCloudFormationTemplate(this IDistributedApplicationBuilder builder, string name, string templatePath, string? stackName = null)
{
- var resource = new CloudFormationTemplateResource(stackName, templatePath);
- var cfBuilder = builder.AddResource(resource)
- .WithManifestPublishingCallback(resource.WriteToManifest);
-
- builder.Services.TryAddLifecycleHook();
- return cfBuilder;
+ builder.AddAWSProvisioning();
+ var resource = new CloudFormationTemplateResource(name, stackName ?? name, templatePath);
+ return builder
+ .AddResource(resource)
+ .WithInitialState(new()
+ {
+ Properties = [],
+ ResourceType = "CloudFormationTemplate",
+ })
+ .WithManifestPublishingCallback(resource.WriteToManifest);
}
///
@@ -48,17 +52,22 @@ public static IResourceBuilder WithParameter(th
///
/// Add a CloudFormation stack for provisioning application resources.
///
- ///
- /// The name of the CloudFormation stack.
+ /// The .
+ /// The name of the resource.
+ /// The name of the CloudFormation stack. If not specified, the CloudFormation stack name will be the resource name prefixed with 'Aspire-'
///
- public static IResourceBuilder AddAWSCloudFormationStack(this IDistributedApplicationBuilder builder, string stackName)
+ public static IResourceBuilder AddAWSCloudFormationStack(this IDistributedApplicationBuilder builder, string name, string? stackName = null)
{
- var resource = new CloudFormationStackResource(stackName);
- var cfBuilder = builder.AddResource(resource)
- .WithManifestPublishingCallback(resource.WriteToManifest);
-
- builder.Services.TryAddLifecycleHook();
- return cfBuilder;
+ builder.AddAWSProvisioning();
+ var resource = new CloudFormationStackResource(name, stackName ?? name);
+ return builder
+ .AddResource(resource)
+ .WithInitialState(new()
+ {
+ Properties = [],
+ ResourceType = "CloudFormationStack",
+ })
+ .WithManifestPublishingCallback(resource.WriteToManifest);
}
///
@@ -83,6 +92,8 @@ public static StackOutputReference GetOutput(this IResourceBuilder WithEnvironment(this IResourceBuilder builder, string name, StackOutputReference stackOutputReference)
where T : IResourceWithEnvironment
{
+ stackOutputReference.Resource.Annotations.Add(new CloudFormationReferenceAnnotation(builder.Resource.Name));
+
return builder.WithEnvironment(async ctx =>
{
if (ctx.ExecutionContext.IsPublishMode)
@@ -107,18 +118,8 @@ public static IResourceBuilder WithEnvironment(this IResourceBuilder bu
///
///
/// The name of the AWS credential profile.
- public static IResourceBuilder WithReference(this IResourceBuilder builder, IAWSSDKConfig awsSdkConfig)
- {
- builder.Resource.AWSSDKConfig = awsSdkConfig;
- return builder;
- }
-
- ///
- /// The AWS SDK service client configuration used to create the CloudFormation service client.
- ///
- ///
- /// The name of the AWS credential profile.
- public static IResourceBuilder WithReference(this IResourceBuilder builder, IAWSSDKConfig awsSdkConfig)
+ public static IResourceBuilder WithReference(this IResourceBuilder builder, IAWSSDKConfig awsSdkConfig)
+ where TDestination : ICloudFormationResource
{
builder.Resource.AWSSDKConfig = awsSdkConfig;
return builder;
@@ -130,19 +131,8 @@ public static IResourceBuilder WithReference(th
///
///
/// The AWS CloudFormation service client.
- public static IResourceBuilder WithReference(this IResourceBuilder builder, IAmazonCloudFormation cloudFormationClient)
- {
- builder.Resource.CloudFormationClient = cloudFormationClient;
- return builder;
- }
-
- ///
- /// Override the CloudFormation service client the ICloudFormationTemplateResource would create to interact with the CloudFormation service. This can be used for pointing the
- /// CloudFormation service client to a non-standard CloudFormation endpoint like an emulator.
- ///
- ///
- /// The AWS CloudFormation service client.
- public static IResourceBuilder WithReference(this IResourceBuilder builder, IAmazonCloudFormation cloudFormationClient)
+ public static IResourceBuilder WithReference(this IResourceBuilder builder, IAmazonCloudFormation cloudFormationClient)
+ where TDestination : ICloudFormationResource
{
builder.Resource.CloudFormationClient = cloudFormationClient;
return builder;
@@ -156,7 +146,7 @@ public static IResourceBuilder WithReference(th
/// The CloudFormation resource.
/// The config section in IConfiguration to add the output parameters.
///
- public static IResourceBuilder WithReference(this IResourceBuilder builder, IResourceBuilder cloudFormationResourceBuilder, string configSection = "AWS::Resources")
+ public static IResourceBuilder WithReference(this IResourceBuilder builder, IResourceBuilder cloudFormationResourceBuilder, string configSection = Constants.DefaultConfigSection)
where TDestination : IResourceWithEnvironment
{
cloudFormationResourceBuilder.WithAnnotation(new CloudFormationReferenceAnnotation(builder.Resource.Name));
@@ -184,7 +174,7 @@ public static IResourceBuilder WithReference(this IR
return;
}
- configSection = configSection.Replace(':', '_');
+ configSection = configSection.ToEnvironmentVariables();
foreach (var output in cloudFormationResourceBuilder.Resource.Outputs)
{
diff --git a/src/Aspire.Hosting.AWS/CloudFormation/CloudFormationLifecycleHook.cs b/src/Aspire.Hosting.AWS/CloudFormation/CloudFormationLifecycleHook.cs
deleted file mode 100644
index 7edeecfc332..00000000000
--- a/src/Aspire.Hosting.AWS/CloudFormation/CloudFormationLifecycleHook.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Aspire.Hosting.ApplicationModel;
-using Aspire.Hosting.Lifecycle;
-
-namespace Aspire.Hosting.AWS.CloudFormation;
-
-///
-/// The lifecycle hook that handles deploying the CloudFormation template to a CloudFormation stack.
-///
-///
-///
-///
-internal sealed class CloudFormationLifecycleHook(
- DistributedApplicationExecutionContext executionContext,
- ResourceNotificationService notificationService,
- ResourceLoggerService loggerService) : IDistributedApplicationLifecycleHook
-{
-
- public async Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
- {
- if (executionContext.IsPublishMode)
- {
- return;
- }
-
- foreach (CloudFormationResource cloudFormationResource in appModel.Resources.OfType())
- {
- await notificationService.PublishUpdateAsync(cloudFormationResource, (state) => state with { State = Constants.ResourceStateStarting }).ConfigureAwait(false);
- cloudFormationResource.ProvisioningTaskCompletionSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
- }
-
- _ = Task.Run(() => new CloudFormationProvisioner(appModel, notificationService, loggerService).ConfigureCloudFormation(), cancellationToken);
- }
-}
-
diff --git a/src/Aspire.Hosting.AWS/CloudFormation/CloudFormationProvisioner.cs b/src/Aspire.Hosting.AWS/CloudFormation/CloudFormationProvisioner.cs
deleted file mode 100644
index 16d542bce89..00000000000
--- a/src/Aspire.Hosting.AWS/CloudFormation/CloudFormationProvisioner.cs
+++ /dev/null
@@ -1,180 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.Collections.Immutable;
-using Amazon.CloudFormation;
-using Amazon.CloudFormation.Model;
-using Amazon.Runtime;
-using Aspire.Hosting.ApplicationModel;
-using Microsoft.Extensions.Logging;
-
-namespace Aspire.Hosting.AWS.CloudFormation;
-internal sealed class CloudFormationProvisioner(
- DistributedApplicationModel appModel,
- ResourceNotificationService notificationService,
- ResourceLoggerService loggerService)
-{
- internal async Task ConfigureCloudFormation(CancellationToken cancellationToken = default)
- {
- await ProcessCloudFormationStackResourceAsync(cancellationToken).ConfigureAwait(false);
- await ProcessCloudFormationTemplateResourceAsync(cancellationToken).ConfigureAwait(false);
- }
-
- private async Task ProcessCloudFormationStackResourceAsync(CancellationToken cancellationToken = default)
- {
- foreach (var cloudFormationResource in appModel.Resources.OfType())
- {
- var logger = loggerService.GetLogger(cloudFormationResource);
-
- await PublishCloudFormationUpdateStateAsync(cloudFormationResource, Constants.ResourceStateStarting).ConfigureAwait(false);
-
- try
- {
- using var cfClient = GetCloudFormationClient(cloudFormationResource);
-
- var request = new DescribeStacksRequest { StackName = cloudFormationResource.Name };
- var response = await cfClient.DescribeStacksAsync(request, cancellationToken).ConfigureAwait(false);
-
- // If the stack didn't exist then a StackNotFoundException would have been thrown.
- var stack = response.Stacks[0];
-
- // Capture the CloudFormation stack output parameters on to the Aspire CloudFormation resource. This
- // allows projects that have a reference to the stack have the output parameters applied to the
- // projects IConfiguration.
- cloudFormationResource.Outputs = stack!.Outputs;
-
- await PublishCloudFormationUpdateStateAsync(cloudFormationResource, Constants.ResourceStateRunning, ConvertOutputToProperties(stack)).ConfigureAwait(false);
-
- cloudFormationResource.ProvisioningTaskCompletionSource?.TrySetResult();
- }
- catch (Exception e)
- {
- if (e is AmazonCloudFormationException ce && string.Equals(ce.ErrorCode, "ValidationError"))
- {
- logger.LogError("Stack {StackName} does not exists to add as a resource.", cloudFormationResource.Name);
- }
- else
- {
- logger.LogError(e, "Error reading {StackName}.", cloudFormationResource.Name);
- }
-
- await PublishCloudFormationUpdateStateAsync(cloudFormationResource, Constants.ResourceStateFailedToStart).ConfigureAwait(false);
- cloudFormationResource.ProvisioningTaskCompletionSource?.TrySetException(e);
- }
- }
- }
-
- private async Task ProcessCloudFormationTemplateResourceAsync(CancellationToken cancellationToken = default)
- {
- foreach (var cloudFormationResource in appModel.Resources.OfType())
- {
- var logger = loggerService.GetLogger(cloudFormationResource);
-
- try
- {
- await PublishCloudFormationUpdateStateAsync(cloudFormationResource, Constants.ResourceStateStarting).ConfigureAwait(false);
-
- using var cfClient = GetCloudFormationClient(cloudFormationResource);
-
- var executor = new CloudFormationStackExecutor(cfClient, cloudFormationResource, logger);
- var stack = await executor.ExecuteTemplateAsync(cancellationToken).ConfigureAwait(false);
-
- if (stack != null)
- {
- logger.LogInformation("CloudFormation stack has {Count} output parameters", stack.Outputs.Count);
- if (logger.IsEnabled(LogLevel.Information))
- {
- foreach (var output in stack.Outputs)
- {
- logger.LogInformation("Output Name: {Name}, Value {Value}", output.OutputKey, output.OutputValue);
- }
- }
-
- logger.LogInformation("CloudFormation provisioning complete");
-
- cloudFormationResource.Outputs = stack.Outputs;
- await PublishCloudFormationUpdateStateAsync(cloudFormationResource, Constants.ResourceStateRunning, ConvertOutputToProperties(stack, cloudFormationResource.TemplatePath)).ConfigureAwait(false);
- cloudFormationResource.ProvisioningTaskCompletionSource?.TrySetResult();
- }
- else
- {
- logger.LogError("CloudFormation provisioning failed");
-
- await PublishCloudFormationUpdateStateAsync(cloudFormationResource, Constants.ResourceStateFailedToStart).ConfigureAwait(false);
- cloudFormationResource.ProvisioningTaskCompletionSource?.TrySetException(new AWSProvisioningException("Failed to apply CloudFormation template", null));
- }
- }
- catch (Exception ex)
- {
- logger.LogError(ex, "Error provisioning {ResourceName} CloudFormation resource", cloudFormationResource.Name);
- await PublishCloudFormationUpdateStateAsync(cloudFormationResource, Constants.ResourceStateFailedToStart).ConfigureAwait(false);
- cloudFormationResource.ProvisioningTaskCompletionSource?.TrySetException(ex);
- }
- }
- }
-
- private async Task PublishCloudFormationUpdateStateAsync(CloudFormationResource resource, string status, ImmutableArray? properties = null)
- {
- if (properties == null)
- {
- properties = ImmutableArray.Create();
- }
-
- await notificationService.PublishUpdateAsync(resource, state => state with
- {
- State = status,
- Properties = state.Properties.AddRange(properties)
- }).ConfigureAwait(false);
- }
-
- private static ImmutableArray ConvertOutputToProperties(Stack stack, string? templateFile = null)
- {
- var list = ImmutableArray.CreateBuilder();
-
- foreach (var output in stack.Outputs)
- {
- list.Add(new("aws.cloudformation.output." + output.OutputKey, output.OutputValue));
- }
-
- list.Add(new(CustomResourceKnownProperties.Source, stack.StackId));
-
- if (!string.IsNullOrEmpty(templateFile))
- {
- list.Add(new("aws.cloudformation.template", templateFile));
- }
-
- return list.ToImmutableArray();
- }
-
- private static IAmazonCloudFormation GetCloudFormationClient(ICloudFormationResource resource)
- {
- if (resource.CloudFormationClient != null)
- {
- return resource.CloudFormationClient;
- }
-
- try
- {
- AmazonCloudFormationClient client;
- if (resource.AWSSDKConfig != null)
- {
- var config = resource.AWSSDKConfig.CreateServiceConfig();
-
- var awsCredentials = FallbackCredentialsFactory.GetCredentials(config);
- client = new AmazonCloudFormationClient(awsCredentials, config);
- }
- else
- {
- client = new AmazonCloudFormationClient();
- }
-
- client.BeforeRequestEvent += SdkUtilities.ConfigureUserAgentString;
-
- return client;
- }
- catch (Exception e)
- {
- throw new AWSProvisioningException("Failed to construct AWS CloudFormation service client to provision AWS resources.", e);
- }
- }
-}
diff --git a/src/Aspire.Hosting.AWS/CloudFormation/CloudFormationResource.cs b/src/Aspire.Hosting.AWS/CloudFormation/CloudFormationResource.cs
index a334af29bbd..2f86f4653d0 100644
--- a/src/Aspire.Hosting.AWS/CloudFormation/CloudFormationResource.cs
+++ b/src/Aspire.Hosting.AWS/CloudFormation/CloudFormationResource.cs
@@ -4,12 +4,15 @@
using Amazon.CloudFormation;
using Amazon.CloudFormation.Model;
using Aspire.Hosting.ApplicationModel;
+using Aspire.Hosting.Publishing;
namespace Aspire.Hosting.AWS.CloudFormation;
-///
-internal abstract class CloudFormationResource(string name) : Resource(name), ICloudFormationResource
+///
+internal abstract class CloudFormationResource(string name, string stackName) : Resource(name), ICloudFormationResource
{
+ public string StackName { get; } = stackName;
+
///
public IAWSSDKConfig? AWSSDKConfig { get; set; }
@@ -21,4 +24,6 @@ internal abstract class CloudFormationResource(string name) : Resource(name), IC
///
public TaskCompletionSource? ProvisioningTaskCompletionSource { get; set; }
+
+ internal abstract void WriteToManifest(ManifestPublishingContext context);
}
diff --git a/src/Aspire.Hosting.AWS/CloudFormation/CloudFormationStackResource.cs b/src/Aspire.Hosting.AWS/CloudFormation/CloudFormationStackResource.cs
index f836ffc458b..5e99fae38c1 100644
--- a/src/Aspire.Hosting.AWS/CloudFormation/CloudFormationStackResource.cs
+++ b/src/Aspire.Hosting.AWS/CloudFormation/CloudFormationStackResource.cs
@@ -6,13 +6,14 @@
namespace Aspire.Hosting.AWS.CloudFormation;
-///
-internal sealed class CloudFormationStackResource(string name) : CloudFormationResource(name), ICloudFormationStackResource
+///
+internal sealed class CloudFormationStackResource(string name, string stackName)
+ : CloudFormationResource(name, stackName), ICloudFormationStackResource
{
- internal void WriteToManifest(ManifestPublishingContext context)
+ internal override void WriteToManifest(ManifestPublishingContext context)
{
context.Writer.WriteString("type", "aws.cloudformation.stack.v0");
- context.Writer.TryWriteString("stack-name", Name);
+ context.Writer.TryWriteString("stack-name", StackName);
context.Writer.WritePropertyName("references");
context.Writer.WriteStartArray();
@@ -22,6 +23,7 @@ internal void WriteToManifest(ManifestPublishingContext context)
context.Writer.WriteString("target-resource", cloudFormationResource.TargetResource);
context.Writer.WriteEndObject();
}
+
context.Writer.WriteEndArray();
}
}
diff --git a/src/Aspire.Hosting.AWS/CloudFormation/CloudFormationTemplateResource.cs b/src/Aspire.Hosting.AWS/CloudFormation/CloudFormationTemplateResource.cs
index 268c5069378..a9e5551ac18 100644
--- a/src/Aspire.Hosting.AWS/CloudFormation/CloudFormationTemplateResource.cs
+++ b/src/Aspire.Hosting.AWS/CloudFormation/CloudFormationTemplateResource.cs
@@ -6,11 +6,9 @@
namespace Aspire.Hosting.AWS.CloudFormation;
-///
-internal sealed class CloudFormationTemplateResource(string name, string templatePath) : CloudFormationResource(name), ICloudFormationTemplateResource
+///
+internal class CloudFormationTemplateResource(string name, string stackName, string templatePath) : CloudFormationResource(name, stackName), ICloudFormationTemplateResource
{
- public IDictionary CloudFormationParameters { get; } = new Dictionary();
-
///
public string TemplatePath { get; } = templatePath;
@@ -24,7 +22,9 @@ internal sealed class CloudFormationTemplateResource(string name, string templat
public bool DisableDiffCheck { get; set; }
///
- public IList DisabledCapabilities { get; } = new List();
+ public IList DisabledCapabilities { get; } = [];
+
+ public IDictionary CloudFormationParameters { get; } = new Dictionary();
///
public ICloudFormationTemplateResource AddParameter(string parameterName, string parameterValue)
@@ -33,10 +33,10 @@ public ICloudFormationTemplateResource AddParameter(string parameterName, string
return this;
}
- internal void WriteToManifest(ManifestPublishingContext context)
+ internal override void WriteToManifest(ManifestPublishingContext context)
{
context.Writer.WriteString("type", "aws.cloudformation.template.v0");
- context.Writer.TryWriteString("stack-name", Name);
+ context.Writer.TryWriteString("stack-name", StackName);
context.Writer.TryWriteString("template-path", context.GetManifestRelativePath(TemplatePath));
context.Writer.WritePropertyName("references");
diff --git a/src/Aspire.Hosting.AWS/CloudFormation/ICloudFormationResource.cs b/src/Aspire.Hosting.AWS/CloudFormation/ICloudFormationResource.cs
index 2926b3ef91a..6bbe491a91d 100644
--- a/src/Aspire.Hosting.AWS/CloudFormation/ICloudFormationResource.cs
+++ b/src/Aspire.Hosting.AWS/CloudFormation/ICloudFormationResource.cs
@@ -3,20 +3,14 @@
using Amazon.CloudFormation;
using Amazon.CloudFormation.Model;
-using Aspire.Hosting.ApplicationModel;
namespace Aspire.Hosting.AWS.CloudFormation;
///
/// Resource representing an AWS CloudFormation stack.
///
-public interface ICloudFormationResource : IResource
+public interface ICloudFormationResource : IAWSResource
{
- ///
- /// Configuration for creating service clients from the AWS .NET SDK.
- ///
- IAWSSDKConfig? AWSSDKConfig { get; set; }
-
///
/// The configured Amazon CloudFormation service client used to make service calls. If this property set
/// then AWSSDKConfig is ignored.
@@ -24,12 +18,12 @@ public interface ICloudFormationResource : IResource
IAmazonCloudFormation? CloudFormationClient { get; set; }
///
- /// The output parameters of the CloudFormation stack.
+ /// The name of the Amazon CloudFormation stack
///
- List
public const string ResourceStateRunning = "Running";
+
+ ///
+ /// Default Configuration Section
+ ///
+ public const string DefaultConfigSection = "AWS:Resources";
}
diff --git a/src/Aspire.Hosting.AWS/IAWSResource.cs b/src/Aspire.Hosting.AWS/IAWSResource.cs
new file mode 100644
index 00000000000..3aeb6ea89a2
--- /dev/null
+++ b/src/Aspire.Hosting.AWS/IAWSResource.cs
@@ -0,0 +1,22 @@
+// 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;
+
+namespace Aspire.Hosting.AWS;
+
+///
+/// Represents an AWS resource, as a marker interface for 's.
+///
+public interface IAWSResource : IResource
+{
+ ///
+ /// Configuration for creating service clients from the AWS .NET SDK.
+ ///
+ IAWSSDKConfig? AWSSDKConfig { get; set; }
+
+ ///
+ /// Set by the AWSProvisioner to indicate the task that is provisioning the resource.
+ ///
+ public TaskCompletionSource? ProvisioningTaskCompletionSource { get; set; }
+}
diff --git a/src/Aspire.Hosting.AWS/Provisioning/AWSProvisionerExtensions.cs b/src/Aspire.Hosting.AWS/Provisioning/AWSProvisionerExtensions.cs
new file mode 100644
index 00000000000..fa0956e9c8f
--- /dev/null
+++ b/src/Aspire.Hosting.AWS/Provisioning/AWSProvisionerExtensions.cs
@@ -0,0 +1,38 @@
+// 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.AWS;
+using Aspire.Hosting.AWS.CDK;
+using Aspire.Hosting.AWS.CloudFormation;
+using Aspire.Hosting.AWS.Provisioning;
+using Aspire.Hosting.Lifecycle;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Aspire.Hosting;
+
+///
+/// Provides extension methods for adding support for generating AWS resources dynamically during application startup.
+///
+internal static class AWSProvisionerExtensions
+{
+ ///
+ /// Adds support for generating azure resources dynamically during application startup.
+ /// The application must configure the appropriate subscription, location.
+ ///
+ public static IDistributedApplicationBuilder AddAWSProvisioning(this IDistributedApplicationBuilder builder)
+ {
+ builder.Services.TryAddLifecycleHook();
+ builder.AddAWSProvisioner();;
+ builder.AddAWSProvisioner();
+ builder.AddAWSProvisioner();
+ return builder;
+ }
+
+ internal static IDistributedApplicationBuilder AddAWSProvisioner(this IDistributedApplicationBuilder builder)
+ where TResource : IAWSResource
+ where TProvisioner : AWSResourceProvisioner
+ {
+ builder.Services.AddKeyedSingleton(typeof(TResource));
+ return builder;
+ }
+}
diff --git a/src/Aspire.Hosting.AWS/AWSProvisioningException.cs b/src/Aspire.Hosting.AWS/Provisioning/AWSProvisioningException.cs
similarity index 91%
rename from src/Aspire.Hosting.AWS/AWSProvisioningException.cs
rename to src/Aspire.Hosting.AWS/Provisioning/AWSProvisioningException.cs
index 8f97208ff9d..01f1fa6f46e 100644
--- a/src/Aspire.Hosting.AWS/AWSProvisioningException.cs
+++ b/src/Aspire.Hosting.AWS/Provisioning/AWSProvisioningException.cs
@@ -1,7 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-namespace Aspire.Hosting.AWS;
+namespace Aspire.Hosting.AWS.Provisioning;
///
/// Exception for errors provisioning AWS application resources
diff --git a/src/Aspire.Hosting.AWS/Provisioning/AWSResourceProvisionerOfT.cs b/src/Aspire.Hosting.AWS/Provisioning/AWSResourceProvisionerOfT.cs
new file mode 100644
index 00000000000..e072c31f71a
--- /dev/null
+++ b/src/Aspire.Hosting.AWS/Provisioning/AWSResourceProvisionerOfT.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.
+namespace Aspire.Hosting.AWS.Provisioning;
+
+internal interface IAWSResourceProvisioner
+{
+ Task GetOrCreateResourceAsync(IAWSResource resource, CancellationToken cancellationToken = default);
+}
+
+internal abstract class AWSResourceProvisioner : IAWSResourceProvisioner
+ where TResource : IAWSResource
+{
+ public Task GetOrCreateResourceAsync(
+ IAWSResource resource,
+ CancellationToken cancellationToken)
+ => GetOrCreateResourceAsync((TResource)resource, cancellationToken);
+
+ protected abstract Task GetOrCreateResourceAsync(TResource resource, CancellationToken cancellationToken);
+}
diff --git a/src/Aspire.Hosting.AWS/Provisioning/CDKStackResourceProvisioner.cs b/src/Aspire.Hosting.AWS/Provisioning/CDKStackResourceProvisioner.cs
new file mode 100644
index 00000000000..b8725d6bd1a
--- /dev/null
+++ b/src/Aspire.Hosting.AWS/Provisioning/CDKStackResourceProvisioner.cs
@@ -0,0 +1,53 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Amazon.CDK.CXAPI;
+using Amazon.CloudFormation;
+using Aspire.Hosting.ApplicationModel;
+using Aspire.Hosting.AWS.CDK;
+using Microsoft.Extensions.Logging;
+
+namespace Aspire.Hosting.AWS.Provisioning;
+
+internal sealed class CDKStackResourceProvisioner(
+ ResourceLoggerService loggerService,
+ ResourceNotificationService notificationService)
+ : CloudFormationTemplateResourceProvisioner(loggerService, notificationService)
+{
+ protected override async Task GetOrCreateResourceAsync(StackResource resource, CancellationToken cancellationToken)
+ {
+ var logger = LoggerService.GetLogger(resource);
+ await ProvisionCDKStackAssetsAsync((StackResource)resource, logger).ConfigureAwait(false);
+ await base.GetOrCreateResourceAsync(resource, cancellationToken).ConfigureAwait(false);
+ }
+
+ private static Task ProvisionCDKStackAssetsAsync(StackResource resource, ILogger logger)
+ {
+ // Currently CDK Stack Assets like S3 and Container images are not supported. When a stack contains those assets
+ // we stop provisioning as it can introduce unwanted issues.
+ if (!resource.TryGetStackArtifact(out var artifact))
+ {
+ throw new AWSProvisioningException("Failed to provision stack assets. Could not retrieve stack artifact.");
+ }
+
+ if (!artifact.Dependencies
+ .OfType()
+ .Any(dependency =>
+ dependency.Contents.Files?.Count > 1
+ || dependency.Contents.DockerImages?.Count > 0))
+ {
+ return Task.CompletedTask;
+ }
+
+ logger.LogError("File or container image assets are currently not supported");
+ throw new AWSProvisioningException("Failed to provision stack assets. Provisioning file or container image assets are currently not supported.");
+ }
+
+ protected override void HandleTemplateProvisioningException(Exception ex, StackResource resource, ILogger logger)
+ {
+ if (ex.InnerException is AmazonCloudFormationException inner && inner.Message.StartsWith(@"Unable to fetch parameters [/cdk-bootstrap/"))
+ {
+ logger.LogError("The environment doesn't have the CDK toolkit stack installed. Use 'cdk boostrap' to setup your environment for use AWS CDK with Aspire");
+ }
+ }
+}
diff --git a/src/Aspire.Hosting.AWS/Provisioning/CloudFormationResourceProvisioner.cs b/src/Aspire.Hosting.AWS/Provisioning/CloudFormationResourceProvisioner.cs
new file mode 100644
index 00000000000..92dcf5cd88b
--- /dev/null
+++ b/src/Aspire.Hosting.AWS/Provisioning/CloudFormationResourceProvisioner.cs
@@ -0,0 +1,117 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Immutable;
+using System.Text.RegularExpressions;
+using Amazon;
+using Amazon.CloudFormation;
+using Amazon.CloudFormation.Model;
+using Amazon.Runtime;
+using Aspire.Hosting.ApplicationModel;
+using Aspire.Hosting.AWS.CloudFormation;
+
+namespace Aspire.Hosting.AWS.Provisioning;
+
+internal abstract partial class CloudFormationResourceProvisioner(ResourceLoggerService loggerService, ResourceNotificationService notificationService) : AWSResourceProvisioner
+ where T : ICloudFormationResource
+{
+ protected ResourceLoggerService LoggerService => loggerService;
+
+ protected ResourceNotificationService NotificationService => notificationService;
+
+ protected async Task PublishCloudFormationUpdatePropertiesAsync(T resource, ImmutableArray? properties = null, ImmutableArray? urls = default)
+ {
+ if (properties == null)
+ {
+ properties = ImmutableArray.Create();
+ }
+
+ await NotificationService.PublishUpdateAsync(resource, state => state with
+ {
+ Properties = state.Properties.AddRange(properties),
+ Urls = urls ?? []
+ }).ConfigureAwait(false);
+ }
+
+ protected static ImmutableArray ConvertOutputToProperties(Stack stack, string? templateFile = null)
+ {
+ var list = ImmutableArray.CreateBuilder();
+
+ list.Add(new ResourcePropertySnapshot(CustomResourceKnownProperties.Source, stack.StackId));
+
+ if (!string.IsNullOrEmpty(templateFile))
+ {
+ list.Add(new("aws.cloudformation.template", templateFile));
+ }
+
+ foreach (var output in stack.Outputs)
+ {
+ list.Add(new ResourcePropertySnapshot("aws.cloudformation.output." + output.OutputKey, output.OutputValue));
+ }
+
+ return list.ToImmutableArray();
+ }
+
+ [GeneratedRegex("^(us|eu|ap|sa|ca|me|af|il)-\\w+-\\d+$", RegexOptions.Singleline)]
+ private static partial Regex AwsRegionRegex();
+
+ internal static ImmutableArray? MapCloudFormationStackUrl(IAmazonCloudFormation client, string stackId)
+ {
+ try
+ {
+ var endpointUrl = client.DetermineServiceOperationEndpoint(new DescribeStacksRequest { StackName = stackId })?.URL;
+ if (endpointUrl == null || !endpointUrl.Contains(".amazonaws.", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
+ if (!Arn.TryParse(stackId, out var arn) || !AwsRegionRegex().IsMatch(arn.Region))
+ {
+ return null;
+ }
+
+ var url = $"https://console.aws.amazon.com/cloudformation/home?region={Uri.EscapeDataString(arn.Region)}#/stacks/resources?stackId={Uri.EscapeDataString(stackId)}";
+
+ return
+ [
+ new UrlSnapshot("aws-console", url, IsInternal: false)
+ ];
+ }
+ catch (Exception)
+ {
+ return null;
+ }
+ }
+
+ protected static IAmazonCloudFormation GetCloudFormationClient(ICloudFormationResource resource)
+ {
+ if (resource.CloudFormationClient != null)
+ {
+ return resource.CloudFormationClient;
+ }
+
+ try
+ {
+ AmazonCloudFormationClient client;
+ if (resource.AWSSDKConfig != null)
+ {
+ var config = resource.AWSSDKConfig.CreateServiceConfig();
+
+ var awsCredentials = FallbackCredentialsFactory.GetCredentials(config);
+ client = new AmazonCloudFormationClient(awsCredentials, config);
+ }
+ else
+ {
+ client = new AmazonCloudFormationClient();
+ }
+
+ client.BeforeRequestEvent += SdkUtilities.ConfigureUserAgentString;
+
+ return client;
+ }
+ catch (Exception e)
+ {
+ throw new AWSProvisioningException("Failed to construct AWS CloudFormation service client to provision AWS resources.", e);
+ }
+ }
+}
diff --git a/src/Aspire.Hosting.AWS/Provisioning/CloudFormationStackExecutionContext.cs b/src/Aspire.Hosting.AWS/Provisioning/CloudFormationStackExecutionContext.cs
new file mode 100644
index 00000000000..c7d49e5b77c
--- /dev/null
+++ b/src/Aspire.Hosting.AWS/Provisioning/CloudFormationStackExecutionContext.cs
@@ -0,0 +1,23 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Aspire.Hosting.AWS.Provisioning;
+
+internal sealed class CloudFormationStackExecutionContext(
+ string stackName,
+ string template)
+{
+ public string Template { get; } = template;
+
+ public string StackName { get; } = stackName;
+
+ public IDictionary CloudFormationParameters { get; set; } = new Dictionary();
+
+ public string? RoleArn { get; set; }
+
+ public int StackPollingInterval { get; set; } = 3;
+
+ public bool DisableDiffCheck { get; set; }
+
+ public IList DisabledCapabilities { get; set; } = [];
+}
diff --git a/src/Aspire.Hosting.AWS/CloudFormation/CloudFormationStackExecutor.cs b/src/Aspire.Hosting.AWS/Provisioning/CloudFormationStackExecutor.cs
similarity index 91%
rename from src/Aspire.Hosting.AWS/CloudFormation/CloudFormationStackExecutor.cs
rename to src/Aspire.Hosting.AWS/Provisioning/CloudFormationStackExecutor.cs
index a59043c3809..f98cd38b1ae 100644
--- a/src/Aspire.Hosting.AWS/CloudFormation/CloudFormationStackExecutor.cs
+++ b/src/Aspire.Hosting.AWS/Provisioning/CloudFormationStackExecutor.cs
@@ -8,10 +8,11 @@
using Amazon.CloudFormation.Model;
using Microsoft.Extensions.Logging;
-namespace Aspire.Hosting.AWS.CloudFormation;
+namespace Aspire.Hosting.AWS.Provisioning;
+
internal sealed class CloudFormationStackExecutor(
IAmazonCloudFormation cloudFormationClient,
- CloudFormationTemplateResource cloudFormationResource,
+ CloudFormationStackExecutionContext context,
ILogger logger)
{
// Name of the Tag for the stack to store the SHA256 of the CloudFormation template
@@ -21,7 +22,7 @@ internal sealed class CloudFormationStackExecutor(
const string IN_PROGRESS_SUFFIX = "IN_PROGRESS";
// Polling interval for checking status of CloudFormation stack when creating or updating the stack.
- TimeSpan StackPollingDelay { get; } = TimeSpan.FromSeconds(cloudFormationResource.StackPollingInterval);
+ TimeSpan StackPollingDelay { get; } = TimeSpan.FromSeconds(context.StackPollingInterval);
///
/// Using the template and configuration from the CloudFormationTemplateResource create or update
@@ -33,18 +34,18 @@ internal sealed class CloudFormationStackExecutor(
///
internal async Task ExecuteTemplateAsync(CancellationToken cancellationToken = default)
{
- var existingStack = await FindstackAsync().ConfigureAwait(false);
+ var existingStack = await FindStackAsync().ConfigureAwait(false);
var changeSetType = await DetermineChangeSetTypeAsync(existingStack, cancellationToken).ConfigureAwait(false);
- var templateBody = File.ReadAllText(cloudFormationResource.TemplatePath);
- var computedSha256 = ComputeSHA256(templateBody, cloudFormationResource.CloudFormationParameters);
+ var templateBody = context.Template;
+ var computedSha256 = ComputeSHA256(templateBody, context.CloudFormationParameters);
(var tags, var existingSha256) = SetupTags(existingStack, changeSetType, computedSha256);
// Check to see if the template hasn't change. If it hasn't short circuit out.
- if (!cloudFormationResource.DisableDiffCheck && string.Equals(computedSha256, existingSha256))
+ if (!context.DisableDiffCheck && string.Equals(computedSha256, existingSha256))
{
- logger.LogInformation("CloudFormation Template for CloudFormation stack {StackName} has not changed", cloudFormationResource.Name);
+ logger.LogInformation("CloudFormation Template for CloudFormation stack {StackName} has not changed", context.StackName);
return existingStack;
}
@@ -99,7 +100,7 @@ private static (List tags, string? existingSha256) SetupTags(Stack? existin
private List SetupTemplateParameters()
{
var templateParameters = new List();
- foreach (var kvp in cloudFormationResource.CloudFormationParameters)
+ foreach (var kvp in context.CloudFormationParameters)
{
templateParameters.Add(new Parameter
{
@@ -129,28 +130,28 @@ private List SetupTemplateParameters()
logger.LogInformation("Creating CloudFormation change set.");
var capabilities = new List();
- if (cloudFormationResource.DisabledCapabilities.FirstOrDefault(x => string.Equals(x, "CAPABILITY_IAM", StringComparison.OrdinalIgnoreCase)) == null)
+ if (context.DisabledCapabilities.FirstOrDefault(x => string.Equals(x, "CAPABILITY_IAM", StringComparison.OrdinalIgnoreCase)) == null)
{
capabilities.Add("CAPABILITY_IAM");
}
- if (cloudFormationResource.DisabledCapabilities.FirstOrDefault(x => string.Equals(x, "CAPABILITY_NAMED_IAM", StringComparison.OrdinalIgnoreCase)) == null)
+ if (context.DisabledCapabilities.FirstOrDefault(x => string.Equals(x, "CAPABILITY_NAMED_IAM", StringComparison.OrdinalIgnoreCase)) == null)
{
capabilities.Add("CAPABILITY_NAMED_IAM");
}
- if (cloudFormationResource.DisabledCapabilities.FirstOrDefault(x => string.Equals(x, "CAPABILITY_AUTO_EXPAND", StringComparison.OrdinalIgnoreCase)) == null)
+ if (context.DisabledCapabilities.FirstOrDefault(x => string.Equals(x, "CAPABILITY_AUTO_EXPAND", StringComparison.OrdinalIgnoreCase)) == null)
{
capabilities.Add("CAPABILITY_AUTO_EXPAND");
}
var changeSetRequest = new CreateChangeSetRequest
{
- StackName = cloudFormationResource.Name,
+ StackName = context.StackName,
Parameters = templateParameters,
// Change set name needs to be unique. Since the changeset isn't be created directly by the user the name isn't really important.
ChangeSetName = "Aspire-AppHost-" + DateTime.Now.Ticks,
ChangeSetType = changeSetType,
Capabilities = capabilities,
- RoleARN = cloudFormationResource.RoleArn,
+ RoleARN = context.RoleArn,
TemplateBody = templateBody,
Tags = tags
};
@@ -183,7 +184,7 @@ private async Task ExecuteChangeSetAsync(string changeSetId, ChangeSetTyp
{
var executeChangeSetRequest = new ExecuteChangeSetRequest
{
- StackName = cloudFormationResource.Name,
+ StackName = context.StackName,
ChangeSetName = changeSetId
};
@@ -195,11 +196,11 @@ private async Task ExecuteChangeSetAsync(string changeSetId, ChangeSetTyp
await cloudFormationClient.ExecuteChangeSetAsync(executeChangeSetRequest, cancellationToken).ConfigureAwait(false);
if (changeSetType == ChangeSetType.CREATE)
{
- logger.LogInformation($"Initiated CloudFormation stack creation for {cloudFormationResource.Name}");
+ logger.LogInformation("Initiated CloudFormation stack creation for {cloudFormationTemplate.StackName}", context.StackName);
}
else
{
- logger.LogInformation($"Initiated CloudFormation stack update on {cloudFormationResource.Name}");
+ logger.LogInformation("Initiated CloudFormation stack update on {cloudFormationTemplate.StackName}", context.StackName);
}
}
catch (Exception e)
@@ -244,7 +245,7 @@ private async Task DetermineChangeSetTypeAsync(Stack? stack, Canc
changeSetType = ChangeSetType.CREATE;
}
- // If the status was DELETE_IN_PROGRESS then just wait for delete to complete
+ // If the status was DELETE_IN_PROGRESS then just wait for delete to complete
else if (stack.StackStatus == StackStatus.DELETE_IN_PROGRESS)
{
await WaitForNoLongerInProgress(cancellationToken).ConfigureAwait(false);
@@ -327,7 +328,7 @@ private async Task DeleteRollbackCompleteStackAsync(Stack stack, CancellationTok
}
await Task.Delay(StackPollingDelay, cancellation).ConfigureAwait(false);
- currentStack = await FindstackAsync().ConfigureAwait(false);
+ currentStack = await FindStackAsync().ConfigureAwait(false);
} while (currentStack != null && currentStack.StackStatus.ToString(CultureInfo.InvariantCulture).EndsWith(IN_PROGRESS_SUFFIX));
return currentStack;
@@ -396,7 +397,7 @@ private async Task WaitStackToCompleteAsync(DateTimeOffset minTimeStampFo
const int RESOURCE_STATUS = 40;
var mostRecentEventId = string.Empty;
- var waitingMessage = $"... Waiting for CloudFormation stack {cloudFormationResource.Name} to be ready";
+ var waitingMessage = $"... Waiting for CloudFormation stack {context.StackName} to be ready";
logger.LogInformation(waitingMessage);
logger.LogInformation(new string('-', waitingMessage.Length));
@@ -406,7 +407,7 @@ private async Task WaitStackToCompleteAsync(DateTimeOffset minTimeStampFo
await Task.Delay(StackPollingDelay, cancellationToken).ConfigureAwait(false);
// If we are in the WaitStackToCompleteAsync then we already know the stack exists.
- stack = (await FindstackAsync().ConfigureAwait(false))!;
+ stack = (await FindStackAsync().ConfigureAwait(false))!;
var events = await GetLatestEventsAsync(minTimeStampForEvents, mostRecentEventId, cancellationToken).ConfigureAwait(false);
if (events.Count > 0)
@@ -449,7 +450,7 @@ private async Task> GetLatestEventsAsync(DateTimeOffset minTime
DescribeStackEventsResponse? response = null;
do
{
- var request = new DescribeStackEventsRequest() { StackName = cloudFormationResource.Name };
+ var request = new DescribeStackEventsRequest() { StackName = context.StackName };
if (response != null)
{
request.NextToken = response.NextToken;
@@ -491,11 +492,11 @@ private static string ComputeSHA256(string templateBody, IDictionary FindstackAsync()
+ private async Task FindStackAsync()
{
await foreach (var stack in cloudFormationClient.Paginators.DescribeStacks(new DescribeStacksRequest()).Stacks.ConfigureAwait(false))
{
- if (string.Equals(cloudFormationResource.Name, stack.StackName, StringComparison.Ordinal))
+ if (string.Equals(context.StackName, stack.StackName, StringComparison.Ordinal))
{
return stack;
}
diff --git a/src/Aspire.Hosting.AWS/Provisioning/CloudFormationStackResourceProvisioner.cs b/src/Aspire.Hosting.AWS/Provisioning/CloudFormationStackResourceProvisioner.cs
new file mode 100644
index 00000000000..c42f2d0ca90
--- /dev/null
+++ b/src/Aspire.Hosting.AWS/Provisioning/CloudFormationStackResourceProvisioner.cs
@@ -0,0 +1,50 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Amazon.CloudFormation;
+using Amazon.CloudFormation.Model;
+using Aspire.Hosting.ApplicationModel;
+using Aspire.Hosting.AWS.CloudFormation;
+using Microsoft.Extensions.Logging;
+
+namespace Aspire.Hosting.AWS.Provisioning;
+
+internal sealed class CloudFormationStackResourceProvisioner(
+ ResourceLoggerService loggerService,
+ ResourceNotificationService notificationService)
+ : CloudFormationResourceProvisioner(loggerService, notificationService)
+{
+ protected override async Task GetOrCreateResourceAsync(CloudFormationStackResource resource, CancellationToken cancellationToken)
+ {
+ var logger = LoggerService.GetLogger(resource);
+ try
+ {
+ using var cfClient = GetCloudFormationClient(resource);
+
+ var request = new DescribeStacksRequest { StackName = resource.Name };
+ var response = await cfClient.DescribeStacksAsync(request, cancellationToken).ConfigureAwait(false);
+
+ // If the stack didn't exist then a StackNotFoundException would have been thrown.
+ var stack = response.Stacks[0];
+
+ // Capture the CloudFormation stack output parameters on to the Aspire CloudFormation resource. This
+ // allows projects that have a reference to the stack have the output parameters applied to the
+ // projects IConfiguration.
+ resource.Outputs = stack!.Outputs;
+
+ await PublishCloudFormationUpdatePropertiesAsync(resource, ConvertOutputToProperties(stack)).ConfigureAwait(false);
+ }
+ catch (Exception e)
+ {
+ if (e is AmazonCloudFormationException ce && string.Equals(ce.ErrorCode, "ValidationError"))
+ {
+ logger.LogError("Stack {StackName} does not exists to add as a resource", resource.Name);
+ }
+ else
+ {
+ logger.LogError(e, "Error reading {StackName}", resource.Name);
+ }
+ throw;
+ }
+ }
+}
diff --git a/src/Aspire.Hosting.AWS/Provisioning/CloudFormationTemplateResourceProvisioner.cs b/src/Aspire.Hosting.AWS/Provisioning/CloudFormationTemplateResourceProvisioner.cs
new file mode 100644
index 00000000000..4ade446f48b
--- /dev/null
+++ b/src/Aspire.Hosting.AWS/Provisioning/CloudFormationTemplateResourceProvisioner.cs
@@ -0,0 +1,85 @@
+// 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.AWS.CloudFormation;
+using Microsoft.Extensions.Logging;
+
+namespace Aspire.Hosting.AWS.Provisioning;
+
+internal class CloudFormationTemplateResourceProvisioner(
+ ResourceLoggerService loggerService,
+ ResourceNotificationService notificationService)
+ : CloudFormationTemplateResourceProvisioner(loggerService, notificationService);
+
+internal class CloudFormationTemplateResourceProvisioner(
+ ResourceLoggerService loggerService,
+ ResourceNotificationService notificationService)
+ : CloudFormationResourceProvisioner(loggerService, notificationService)
+ where T : CloudFormationTemplateResource
+{
+ protected override async Task GetOrCreateResourceAsync(T resource,
+ CancellationToken cancellationToken)
+ {
+ var logger = LoggerService.GetLogger(resource);
+
+ using var cfClient = GetCloudFormationClient(resource);
+
+ try
+ {
+ var context = await CreateCloudFormationExecutionContextAsync(resource, cancellationToken)
+ .ConfigureAwait(false);
+ var executor = new CloudFormationStackExecutor(cfClient, context, logger);
+ var stack = await executor.ExecuteTemplateAsync(cancellationToken).ConfigureAwait(false);
+
+ if (stack != null)
+ {
+ logger.LogInformation("CloudFormation stack has {Count} output parameters", stack.Outputs.Count);
+ if (logger.IsEnabled(LogLevel.Information))
+ {
+ foreach (var output in stack.Outputs)
+ {
+ logger.LogInformation("Output Name: {Name}, Value {Value}", output.OutputKey, output.OutputValue);
+ }
+ }
+
+ logger.LogInformation("CloudFormation provisioning complete");
+
+ if (resource is CloudFormationResource cloudformationResource)
+ {
+ cloudformationResource.Outputs = stack.Outputs;
+ }
+
+ var templatePath = resource.TemplatePath;
+ await PublishCloudFormationUpdatePropertiesAsync(resource, ConvertOutputToProperties(stack, templatePath),
+ MapCloudFormationStackUrl(cfClient, stack.StackId)).ConfigureAwait(false);
+ }
+ else
+ {
+ logger.LogError("CloudFormation provisioning failed");
+
+ throw new AWSProvisioningException("Failed to apply CloudFormation template");
+ }
+ }
+ catch (Exception ex)
+ {
+ HandleTemplateProvisioningException(ex, resource, logger);
+ throw;
+ }
+ }
+
+ private static async Task CreateCloudFormationExecutionContextAsync(T resource, CancellationToken cancellationToken)
+ {
+ var template = await File.ReadAllTextAsync(resource.TemplatePath, cancellationToken).ConfigureAwait(false);
+ return new CloudFormationStackExecutionContext(resource.StackName, template)
+ {
+ RoleArn = resource.RoleArn,
+ DisableDiffCheck = resource.DisableDiffCheck,
+ StackPollingInterval = resource.StackPollingInterval,
+ DisabledCapabilities = resource.DisabledCapabilities,
+ CloudFormationParameters = resource.CloudFormationParameters
+ };
+ }
+
+ protected virtual void HandleTemplateProvisioningException(Exception ex, T resource, ILogger logger) { }
+}
diff --git a/src/Aspire.Hosting.AWS/PublicAPI.Unshipped.txt b/src/Aspire.Hosting.AWS/PublicAPI.Unshipped.txt
index bff9114f326..b9286f9dd5c 100644
--- a/src/Aspire.Hosting.AWS/PublicAPI.Unshipped.txt
+++ b/src/Aspire.Hosting.AWS/PublicAPI.Unshipped.txt
@@ -1,14 +1,21 @@
#nullable enable
-Aspire.Hosting.AWS.AWSProvisioningException
-Aspire.Hosting.AWS.AWSProvisioningException.AWSProvisioningException(string! message, System.Exception? innerException = null) -> void
+Aspire.Hosting.AWS.CDK.ConstructBuilderDelegate
+Aspire.Hosting.AWS.CDK.ConstructOutputDelegate
+Aspire.Hosting.AWS.CDK.IConstructResource
+Aspire.Hosting.AWS.CDK.IConstructResource
+Aspire.Hosting.AWS.CDK.IResourceWithConstruct
+Aspire.Hosting.AWS.CDK.IResourceWithConstruct.Construct.get -> Constructs.IConstruct!
+Aspire.Hosting.AWS.CDK.IResourceWithConstruct
+Aspire.Hosting.AWS.CDK.IResourceWithConstruct.Construct.get -> T
+Aspire.Hosting.AWS.CDK.IStackResource
+Aspire.Hosting.AWS.CDK.IStackResource.Stack.get -> Amazon.CDK.Stack!
+Aspire.Hosting.AWS.CDK.IStackResource
+Aspire.Hosting.AWS.CDK.IStackResource.Stack.get -> T!
Aspire.Hosting.AWS.CloudFormation.ICloudFormationResource
-Aspire.Hosting.AWS.CloudFormation.ICloudFormationResource.AWSSDKConfig.get -> Aspire.Hosting.AWS.IAWSSDKConfig?
-Aspire.Hosting.AWS.CloudFormation.ICloudFormationResource.AWSSDKConfig.set -> void
Aspire.Hosting.AWS.CloudFormation.ICloudFormationResource.CloudFormationClient.get -> Amazon.CloudFormation.IAmazonCloudFormation?
Aspire.Hosting.AWS.CloudFormation.ICloudFormationResource.CloudFormationClient.set -> void
Aspire.Hosting.AWS.CloudFormation.ICloudFormationResource.Outputs.get -> System.Collections.Generic.List?
-Aspire.Hosting.AWS.CloudFormation.ICloudFormationResource.ProvisioningTaskCompletionSource.get -> System.Threading.Tasks.TaskCompletionSource?
-Aspire.Hosting.AWS.CloudFormation.ICloudFormationResource.ProvisioningTaskCompletionSource.set -> void
+Aspire.Hosting.AWS.CloudFormation.ICloudFormationResource.StackName.get -> string!
Aspire.Hosting.AWS.CloudFormation.ICloudFormationStackResource
Aspire.Hosting.AWS.CloudFormation.ICloudFormationTemplateResource
Aspire.Hosting.AWS.CloudFormation.ICloudFormationTemplateResource.AddParameter(string! parameterName, string! parameterValue) -> Aspire.Hosting.AWS.CloudFormation.ICloudFormationTemplateResource!
@@ -27,24 +34,66 @@ Aspire.Hosting.AWS.CloudFormation.StackOutputReference.Resource.get -> Aspire.Ho
Aspire.Hosting.AWS.CloudFormation.StackOutputReference.StackOutputReference(string! name, Aspire.Hosting.AWS.CloudFormation.ICloudFormationResource! resource) -> void
Aspire.Hosting.AWS.CloudFormation.StackOutputReference.Value.get -> string?
Aspire.Hosting.AWS.CloudFormation.StackOutputReference.ValueExpression.get -> string!
+Aspire.Hosting.AWS.IAWSResource
+Aspire.Hosting.AWS.IAWSResource.AWSSDKConfig.get -> Aspire.Hosting.AWS.IAWSSDKConfig?
+Aspire.Hosting.AWS.IAWSResource.AWSSDKConfig.set -> void
+Aspire.Hosting.AWS.IAWSResource.ProvisioningTaskCompletionSource.get -> System.Threading.Tasks.TaskCompletionSource?
+Aspire.Hosting.AWS.IAWSResource.ProvisioningTaskCompletionSource.set -> void
Aspire.Hosting.AWS.IAWSSDKConfig
Aspire.Hosting.AWS.IAWSSDKConfig.Profile.get -> string?
Aspire.Hosting.AWS.IAWSSDKConfig.Profile.set -> void
Aspire.Hosting.AWS.IAWSSDKConfig.Region.get -> Amazon.RegionEndpoint?
Aspire.Hosting.AWS.IAWSSDKConfig.Region.set -> void
+Aspire.Hosting.AWS.Provisioning.AWSProvisioningException
+Aspire.Hosting.AWS.Provisioning.AWSProvisioningException.AWSProvisioningException(string! message, System.Exception? innerException = null) -> void
+Aspire.Hosting.CDKExtensions
Aspire.Hosting.CloudFormationExtensions
+Aspire.Hosting.CognitoResourceExtensions
+Aspire.Hosting.DynamoDBResourceExtensions
+Aspire.Hosting.KinesisResourceExtensions
+Aspire.Hosting.S3ResourceExtensions
Aspire.Hosting.SDKResourceExtensions
-static Aspire.Hosting.CloudFormationExtensions.AddAWSCloudFormationStack(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! stackName) -> Aspire.Hosting.ApplicationModel.IResourceBuilder!
-static Aspire.Hosting.CloudFormationExtensions.AddAWSCloudFormationTemplate(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! stackName, string! templatePath) -> Aspire.Hosting.ApplicationModel.IResourceBuilder!
+Aspire.Hosting.SNSResourceExtensions
+Aspire.Hosting.SQSResourceExtensions
+static Aspire.Hosting.CDKExtensions.AddAWSCDKStack(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name) -> Aspire.Hosting.ApplicationModel.IResourceBuilder!
+static Aspire.Hosting.CDKExtensions.AddAWSCDKStack(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, string! stackName) -> Aspire.Hosting.ApplicationModel.IResourceBuilder!
+static Aspire.Hosting.CDKExtensions.AddAWSCDKStack(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, Aspire.Hosting.AWS.CDK.ConstructBuilderDelegate! stackBuilder) -> Aspire.Hosting.ApplicationModel.IResourceBuilder!>!
+static Aspire.Hosting.CDKExtensions.AddConstruct(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! name, Aspire.Hosting.AWS.CDK.ConstructBuilderDelegate! constructBuilder) -> Aspire.Hosting.ApplicationModel.IResourceBuilder!>!
+static Aspire.Hosting.CDKExtensions.AddOutput(this Aspire.Hosting.ApplicationModel.IResourceBuilder!>! builder, string! name, Aspire.Hosting.AWS.CDK.ConstructOutputDelegate! output) -> Aspire.Hosting.ApplicationModel.IResourceBuilder!>!
+static Aspire.Hosting.CDKExtensions.AddOutput(this Aspire.Hosting.ApplicationModel.IResourceBuilder!>! builder, string! name, Aspire.Hosting.AWS.CDK.ConstructOutputDelegate! output) -> Aspire.Hosting.ApplicationModel.IResourceBuilder!>!
+static Aspire.Hosting.CDKExtensions.GetOutput(this Aspire.Hosting.ApplicationModel.IResourceBuilder!>! builder, string! name, Aspire.Hosting.AWS.CDK.ConstructOutputDelegate! output) -> Aspire.Hosting.AWS.CloudFormation.StackOutputReference!
+static Aspire.Hosting.CDKExtensions.WithEnvironment(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! name, Aspire.Hosting.ApplicationModel.IResourceBuilder!>! construct, Aspire.Hosting.AWS.CDK.ConstructOutputDelegate! outputDelegate, string? outputName = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder!
+static Aspire.Hosting.CDKExtensions.WithReference(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, Aspire.Hosting.ApplicationModel.IResourceBuilder!>! construct, Aspire.Hosting.AWS.CDK.ConstructOutputDelegate