Skip to content

Commit

Permalink
Support single construct resources
Browse files Browse the repository at this point in the history
  • Loading branch information
Vincent Lesierse committed Mar 27, 2024
1 parent 832d206 commit 6b49f82
Show file tree
Hide file tree
Showing 20 changed files with 346 additions and 85 deletions.
22 changes: 16 additions & 6 deletions playground/AWSCDK/AWSCDK.AppHost/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using Amazon;
using AWSCDK.AppHost;
using Amazon.CDK;
using Amazon.CDK.AWS.DynamoDB;
using Attribute = Amazon.CDK.AWS.DynamoDB.Attribute;

var builder = DistributedApplication.CreateBuilder(args);

Expand All @@ -8,14 +10,22 @@
.WithProfile("default")
.WithRegion(RegionEndpoint.EUWest1);

var stack = builder.AddAWSCDKStack(
"AspireWebAppStack",
/*var stack = builder.AddAWSCDK().AddStack(
"Stack",
app => new WebAppStack(app, "AspireWebAppStack", new WebAppStackProps()))
.WithOutput("TableName", stack => stack.Table.TableName)
.WithReference(awsConfig);
.WithOutput("TableName", stack => stack.Table.TableName, "Table::TableName")
.WithReference(awsConfig);*/

var stack = builder.AddAWSCDKStack("Stack").WithReference(awsConfig);
var table = stack.AddConstruct("Table", scope => new Table(scope, "Table", new TableProps
{
PartitionKey = new Attribute { Name = "id", Type = AttributeType.STRING },
BillingMode = BillingMode.PAY_PER_REQUEST,
RemovalPolicy = RemovalPolicy.DESTROY
})).WithOutput("TableName", c => c.TableName);

builder.AddProject<Projects.WebApp>("webapp")
.WithReference(stack)
.WithReference(table)
.WithReference(awsConfig);

builder.Build().Run();
5 changes: 5 additions & 0 deletions playground/AWSCDK/WebApp/AWSResources.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
namespace WebApp;

public class AWSResources
{
public TableResource? Table { get; set; }
}

public class TableResource
{
public string? TableName { get; set; }
}
2 changes: 1 addition & 1 deletion playground/AWSCDK/WebApp/Components/Pages/Weather.razor
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ else
{

var results = new List<WeatherForecast>();
var table = Table.LoadTable(DynamoDB, awsResources.Value.TableName);
var table = Table.LoadTable(DynamoDB, awsResources.Value.Table?.TableName);
var search = table.Scan(new ScanFilter());
do
{
Expand Down
110 changes: 101 additions & 9 deletions src/Aspire.Hosting.AWS.CDK/AWSCDKExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.AWS.CDK;
using Aspire.Hosting.Lifecycle;
using Constructs;
using InvalidOperationException = Amazon.CloudFormation.Model.InvalidOperationException;

namespace Aspire.Hosting;

Expand All @@ -13,6 +15,69 @@ namespace Aspire.Hosting;
/// </summary>
public static class AWSCDKExtensions
{
/// <summary>
///
/// </summary>
/// <param name="builder"></param>
/// <returns></returns>
public static IResourceBuilder<IAppResource> AddAWSCDK(this IDistributedApplicationBuilder builder)
{
builder.Services.TryAddLifecycleHook<AWSCDKLifecycleHook>();
var appResource = builder.Resources.OfType<IAppResource>().SingleOrDefault();
return appResource is not null ? builder.CreateResourceBuilder(appResource) : builder.AddResource(new AppResource());
}

/// <summary>
///
/// </summary>
/// <param name="builder"></param>
/// <param name="name"></param>
/// <returns></returns>
public static IResourceBuilder<IStackResource> AddStack(this IResourceBuilder<IAppResource> builder, string name)
{
var app = builder.Resource.App;
var stack = new Stack(app, name);
return builder.AddResource(parent => new StackResource(name, stack, parent));
}

/// <summary>
///
/// </summary>
/// <param name="builder"></param>
/// <param name="name"></param>
/// <param name="stackBuilder"></param>
/// <returns></returns>
public static IResourceBuilder<IStackResource<T>> AddStack<T>(this IResourceBuilder<IAppResource> builder, string name, StackBuilderDelegate<T> stackBuilder)
where T : Stack
{
var app = builder.Resource.App;
var stack = stackBuilder(app);
return builder.AddResource(parent => new StackResource<T>(name, stack, parent));
}

/// <summary>
///
/// </summary>
/// <param name="builder"></param>
/// <param name="name"></param>
/// <param name="constructBuilder"></param>
/// <returns></returns>
public static IResourceBuilder<IConstructResource<T>> AddConstruct<T>(this IResourceBuilder<IResourceWithConstruct> builder, string name, ConstructBuilderDelegate<T> constructBuilder)
where T : Construct
{
var construct = constructBuilder(builder.Resource.Construct);
return builder.AddResource(parent => new ConstructResource<T>(name, construct, builder.Resource));
}

/// <summary>
///
/// </summary>
/// <param name="builder"></param>
/// <param name="name"></param>
/// <returns></returns>
public static IResourceBuilder<IStackResource> AddAWSCDKStack(this IDistributedApplicationBuilder builder, string name)
=> builder.AddAWSCDK().AddStack(name);

/// <summary>
///
/// </summary>
Expand All @@ -23,9 +88,23 @@ public static class AWSCDKExtensions
/// <returns></returns>
public static IResourceBuilder<IStackResource<T>> AddAWSCDKStack<T>(this IDistributedApplicationBuilder builder, string name, StackBuilderDelegate<T> stackBuilder)
where T: Stack
=> builder.AddAWSCDK().AddStack(name, stackBuilder);

/// <summary>
///
/// </summary>
/// <param name="builder"></param>
/// <param name="name"></param>
/// <param name="output"></param>
/// <param name="exportName"></param>
/// <typeparam name="TStack"></typeparam>
/// <returns></returns>
public static IResourceBuilder<IStackResource<TStack>> WithOutput<TStack>(
this IResourceBuilder<IStackResource<TStack>> builder,
string name, ConstructOutputDelegate<TStack> output, string? exportName = null)
where TStack : Stack
{
_ = builder.AddAWSCDKProvisioning();
return builder.AddResource(new StackResource<T>(name, stackBuilder));
return builder.WithAnnotation(new ConstructOutputAnnotation<TStack>(name, output) { ExportName = exportName});
}

/// <summary>
Expand All @@ -34,18 +113,31 @@ public static IResourceBuilder<IStackResource<T>> AddAWSCDKStack<T>(this IDistri
/// <param name="builder"></param>
/// <param name="name"></param>
/// <param name="output"></param>
/// <param name="exportName"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static IResourceBuilder<IStackResource> WithOutput<T>(this IResourceBuilder<IStackResource<T>> builder,
string name, StackOutputDelegate<T> output)
where T : Stack
public static IResourceBuilder<IConstructResource> WithOutput<T>(
this IResourceBuilder<IConstructResource<T>> builder,
string name, ConstructOutputDelegate<T> output, string? exportName = null)
where T : Construct
{
return builder.WithAnnotation(new StackOutputAnnotation<T>(name, output));
return builder.WithAnnotation(new ConstructOutputAnnotation<T>(name, output) { ExportName = exportName});
}

private static IDistributedApplicationBuilder AddAWSCDKProvisioning(this IDistributedApplicationBuilder builder)
/// <summary>
/// Add a reference of a CloudFormations stack to a project. The output parameters of the CloudFormation stack are added to the project IConfiguration.
/// </summary>
/// <typeparam name="TDestination"></typeparam>
/// <param name="builder"></param>
/// <param name="constructResourceBuilder">The Construct resource.</param>
/// <param name="configSection">The config section in IConfiguration to add the output parameters.</param>
/// <returns></returns>
public static IResourceBuilder<TDestination> WithReference<TDestination>(this IResourceBuilder<TDestination> builder, IResourceBuilder<IConstructResource> constructResourceBuilder, string configSection = "AWS::Resources")
where TDestination : IResourceWithEnvironment
{
builder.Services.TryAddLifecycleHook<AWSCDKLifecycleHook>();
return builder;
var stackResourceBuilder = constructResourceBuilder.FindResourceBuilder<IStackResource>();
return stackResourceBuilder is null
? throw new InvalidOperationException("No IStackResource found for Construct")
: builder.WithReference(stackResourceBuilder, configSection);
}
}
37 changes: 14 additions & 23 deletions src/Aspire.Hosting.AWS.CDK/AWSCDKProvisioner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Immutable;
using Amazon.CDK;
using Amazon.CloudFormation;
using Amazon.Runtime;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.AWS.CloudFormation;
using Microsoft.Extensions.Logging;
using Stack = Amazon.CDK.Stack;
using CloudFormationStack = Amazon.CloudFormation.Model.Stack;

namespace Aspire.Hosting.AWS.CDK;
Expand All @@ -20,42 +18,35 @@ internal sealed class AWSCDKProvisioner(
{
internal async Task ProvisionCloudFormation(CancellationToken cancellationToken = default)
{
var templates = ProcessCDKStackResources();
if (!appModel.TryGetAppResource(out var appResource))
{
return;
}
var templates = ProcessStackResources(appResource);
await DeployCDKStackTemplatesAsync(templates, cancellationToken).ConfigureAwait(false);
}

private IEnumerable<AWSCDKStackTemplate> ProcessCDKStackResources()
private IEnumerable<AWSCDKStackTemplate> ProcessStackResources(IAppResource appResource)
{
var app = new App();
var stackResources = appModel.Resources.OfType<StackResource>().ToDictionary(resource => resource.Name);
// Build Stacks
foreach (var stackResource in stackResources.Values)
{
stackResource.BuildStack(app);
}
// Modified Stacks after build
foreach (var stackResource in stackResources.Values)
// Modified constructs after build
foreach (var constructResource in appModel.Resources.OfType<IResourceWithConstruct>())
{
// Find Stack Modifier Annotations
if (!stackResource.TryGetAnnotationsOfType<IStackModifierAnnotation>(out var modifiers))
{
continue;
}
// Find Stack Nodes
if (app.Node.TryFindChild(stackResource.Name) is not Stack stack)
// Find Construct Modifier Annotations
if (!constructResource.TryGetAnnotationsOfType<IConstructModifierAnnotation>(out var modifiers))
{
continue;
}

// Modify stack
foreach (var modifier in modifiers)
{
modifier.ChangeStack(stack);
modifier.ChangeConstruct(constructResource.Construct);
}
}

var cloudAssembly = app.Synth();
return cloudAssembly.Stacks.Select(stack => new AWSCDKStackTemplate(stack, stackResources[stack.StackName]));
var stackResources = appModel.Resources.OfType<StackResource>();
var cloudAssembly = appResource.App.Synth();
return cloudAssembly.Stacks.Select(stack => new AWSCDKStackTemplate(stack, stackResources.Single(s => ((IStackResource)s).Stack.StackName == stack.StackName)));
}

private async Task DeployCDKStackTemplatesAsync(IEnumerable<AWSCDKStackTemplate> templates, CancellationToken cancellationToken = default)
Expand Down
15 changes: 15 additions & 0 deletions src/Aspire.Hosting.AWS.CDK/AppResource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Amazon.CDK;
using Constructs;
using Resource = Aspire.Hosting.ApplicationModel.Resource;

namespace Aspire.Hosting.AWS.CDK;

internal sealed class AppResource() : Resource("AWSCDK"), IAppResource
{
public App App { get; } = new();

public Construct Construct => App;
}
Original file line number Diff line number Diff line change
@@ -1,12 +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 Amazon.CDK;
using Constructs;

namespace Aspire.Hosting.AWS.CDK;

/// <summary>
///
/// </summary>
/// <typeparam name="T"></typeparam>
public delegate string StackOutputDelegate<in T>(T stack) where T : Stack;
public delegate T ConstructBuilderDelegate<out T>(Construct app) where T : Construct;
29 changes: 29 additions & 0 deletions src/Aspire.Hosting.AWS.CDK/ConstructOutputAnnotation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Amazon.CDK;
using Constructs;

namespace Aspire.Hosting.AWS.CDK;

internal sealed class ConstructOutputAnnotation<T>(string name, ConstructOutputDelegate<T> output)
: IConstructModifierAnnotation
where T : Construct
{
public string Name { get; } = name;

public string? ExportName { get; set; }

public string? Description { get; set; }

public void ChangeConstruct(Construct construct)
{
var target = (T)construct;
_ = new CfnOutput(construct, Name, new CfnOutputProps
{
Value = output(target),
Description = Description,
ExportName = ExportName ?? $"{construct.Node.Id}::{Name}"
});
}
}
11 changes: 11 additions & 0 deletions src/Aspire.Hosting.AWS.CDK/ConstructOutputDelegate.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
///
/// </summary>
/// <typeparam name="T"></typeparam>
public delegate string ConstructOutputDelegate<in T>(T construct) where T : Construct;
21 changes: 21 additions & 0 deletions src/Aspire.Hosting.AWS.CDK/ConstructResource.cs
Original file line number Diff line number Diff line change
@@ -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 Aspire.Hosting.ApplicationModel;
using Constructs;

namespace Aspire.Hosting.AWS.CDK;

internal class ConstructResource(string name, Construct construct, IResourceWithConstruct parent) : Resource(name), IConstructResource
{
public Construct Construct { get; } = construct;

public IResourceWithConstruct Parent { get; } = parent;

}

internal sealed class ConstructResource<T>(string name, T construct, IResourceWithConstruct parent) : ConstructResource(name, construct, parent), IConstructResource<T>
where T : Construct
{
public new T Construct { get; } = construct;
}
17 changes: 17 additions & 0 deletions src/Aspire.Hosting.AWS.CDK/IAppResource.cs
Original file line number Diff line number Diff line change
@@ -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.

using Amazon.CDK;

namespace Aspire.Hosting.AWS.CDK;

/// <summary>
///
/// </summary>
public interface IAppResource : IResourceWithConstruct
{
/// <summary>
/// AWS CKD App
/// </summary>
App App { get; }
}
Loading

0 comments on commit 6b49f82

Please sign in to comment.