Skip to content

Commit

Permalink
Refactor provisioning
Browse files Browse the repository at this point in the history
  • Loading branch information
Vincent Lesierse committed Apr 10, 2024
1 parent bd911ab commit 25e34ef
Show file tree
Hide file tree
Showing 31 changed files with 271 additions and 279 deletions.
Expand Up @@ -8,16 +8,16 @@

namespace Aspire.Hosting.AWS.CDK;

internal sealed class AWSCDKApplicationBuilder : IAWSCDKApplicationBuilder
internal sealed class CDKApplicationBuilder : ICDKApplicationBuilder
{
private readonly IDistributedApplicationBuilder _innerBuilder;

public AWSCDKApplicationBuilder(IDistributedApplicationBuilder builder)
public CDKApplicationBuilder(IDistributedApplicationBuilder builder)
{
_innerBuilder = builder;
_innerBuilder.Services
.AddSingleton<AWSCDKApplicationContext>(_ => new AWSCDKApplicationContext(App))
.TryAddLifecycleHook<AWSCDKLifecycleHook>();
.AddSingleton<CDKApplicationExecutionContext>(_ => new CDKApplicationExecutionContext(App))
.TryAddLifecycleHook<CDKProvisioner>();
}

public App App { get; } = new();
Expand Down
11 changes: 11 additions & 0 deletions src/Aspire.Hosting.AWS/CDK/CDKApplicationExecutionContext.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 Amazon.CDK;

namespace Aspire.Hosting.AWS.CDK;

internal sealed class CDKApplicationExecutionContext(App app)
{
public App App { get; } = app;
}
Expand Up @@ -4,23 +4,24 @@
using Amazon.CDK;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.AWS.CDK;
using Aspire.Hosting.AWS.Utils;
using Constructs;

namespace Aspire.Hosting;

/// <summary>
///
/// </summary>
public static class AWSCDKExtensions
public static class CDKExtensions
{
/// <summary>
///
/// </summary>
/// <param name="builder"></param>
/// <returns></returns>
public static IAWSCDKApplicationBuilder WithAWSCDK(this IDistributedApplicationBuilder builder)
public static ICDKApplicationBuilder WithAWSCDK(this IDistributedApplicationBuilder builder)
{
return new AWSCDKApplicationBuilder(builder);
return new CDKApplicationBuilder(builder);
}

/// <summary>
Expand All @@ -30,7 +31,7 @@ public static IAWSCDKApplicationBuilder WithAWSCDK(this IDistributedApplicationB
/// <param name="name"></param>
/// <param name="stackName"></param>
/// <returns></returns>
public static IResourceBuilder<IStackResource> AddStack(this IAWSCDKApplicationBuilder builder, string name, string? stackName = null)
public static IResourceBuilder<IStackResource> AddStack(this ICDKApplicationBuilder builder, string name, string? stackName = null)
{
var stack = new Stack(builder.App, stackName ?? name);
return builder.AddResource(new StackResource(name, stack));
Expand All @@ -43,7 +44,7 @@ public static IResourceBuilder<IStackResource> AddStack(this IAWSCDKApplicationB
/// <param name="name"></param>
/// <param name="stackBuilder"></param>
/// <returns></returns>
public static IResourceBuilder<IStackResource<T>> AddStack<T>(this IAWSCDKApplicationBuilder builder, string name, StackBuilderDelegate<T> stackBuilder)
public static IResourceBuilder<IStackResource<T>> AddStack<T>(this ICDKApplicationBuilder builder, string name, StackBuilderDelegate<T> stackBuilder)
where T : Stack
{
var stack = stackBuilder(builder.App);
Expand Down Expand Up @@ -110,7 +111,7 @@ public static IResourceBuilder<TDestination> WithReference<TDestination>(this IR
where TDestination : IResourceWithEnvironment
{
var constructId = constructResourceBuilder.Resource.Construct.StackUniqueId();
var stackResourceBuilder = constructResourceBuilder.FindResourceBuilder<IStackResource>();
var stackResourceBuilder = constructResourceBuilder.ResourceBuilderFor<IStackResource>();
return stackResourceBuilder is null
? throw new InvalidOperationException("No IStackResource found for Construct")
: builder.WithReference(stackResourceBuilder, configSection, output => output.OutputKey.StartsWith(constructId), output => output.OutputKey.TrimStart(constructId));
Expand Down
Expand Up @@ -2,44 +2,73 @@
// The .NET Foundation licenses this file to you under the MIT license.

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

namespace Aspire.Hosting.AWS.CDK;

internal sealed class AWSCDKProvisioner(
App app,
DistributedApplicationModel appModel,
internal sealed class CDKProvisioner(
CDKApplicationExecutionContext cdkContext,
DistributedApplicationExecutionContext executionContext,
ResourceNotificationService notificationService,
ResourceLoggerService loggerService)
ResourceLoggerService loggerService) : IDistributedApplicationLifecycleHook
{
internal void SynthesizeStackResources()

public async Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
{
var context = new ProvisioningContext(appModel, notificationService);
SynthesizeStackResourcesInternal(context);
var context = new CDKProvisionerContext(appModel, notificationService);
if (executionContext.IsPublishMode)
{
SynthesizeStackResources(appModel);
return;
}

foreach (var stackResource in appModel.Resources.OfType<StackResource>())
{
await context.PublishUpdateStateAsync(stackResource, Constants.ResourceStateStarting).ConfigureAwait(false);
}

_ = Task.Run(() => ProvisionStackResources(appModel, cancellationToken), cancellationToken);
}

private CloudAssembly SynthesizeStackResourcesInternal(ProvisioningContext context)
private void SynthesizeStackResources(DistributedApplicationModel appModel)
{
ModifyResourcesWithConstructs(context);
return app.Synth();
var context = new CDKProvisionerContext(appModel, notificationService);
SynthesizeStackResourcesInternal(context);
}

internal async Task ProvisionStackResources(CancellationToken cancellationToken = default)
private async Task ProvisionStackResources(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
{
var context = new ProvisioningContext(appModel, notificationService);
var cloudAssembly = SynthesizeStackResourcesInternal(context);
var templates = cloudAssembly.Stacks.Select(stack => new AWSCDKStackTemplate(stack, context.StackResources.Single(s => ((IStackResource)s).Stack.StackName == stack.StackName)));
var context = new CDKProvisionerContext(appModel, notificationService);
var templates = SynthesizeStackResourcesInternal(context);

await DeployCDKStackTemplatesAsync(templates, context, cancellationToken).ConfigureAwait(false);
}

private static void ModifyResourcesWithConstructs(ProvisioningContext context)
private IEnumerable<CDKStackTemplate> SynthesizeStackResourcesInternal(CDKProvisionerContext context)
{
ModifyResourcesWithConstructs(context);
var cloudAssembly = cdkContext.App.Synth();
// Guard when stack contains assets
foreach (var stack in cloudAssembly.Stacks)
{
var stackResource = context.StackResources.Single(s => s.Stack.StackName == stack.StackName);
if (stack.Assets.Length != 0)
{
var logger = loggerService.GetLogger(stackResource);
logger.LogError("CDK stack {StackResourceName} contains assets and is currently not supported", stackResource.Name);
context.PublishUpdateStateAsync(stackResource, Constants.ResourceStateFailedToStart).Wait();
}
yield return new CDKStackTemplate(stack, context.StackResources.Single(s => s.Stack.StackName == stack.StackName));
}
}

private static void ModifyResourcesWithConstructs(CDKProvisionerContext context)
{
// Modified constructs after build
foreach (var constructResource in context.AppModel.Resources.OfType<IResourceWithConstruct>())
Expand All @@ -58,7 +87,7 @@ private static void ModifyResourcesWithConstructs(ProvisioningContext context)
}
}

private async Task DeployCDKStackTemplatesAsync(IEnumerable<AWSCDKStackTemplate> templates, ProvisioningContext context, CancellationToken cancellationToken = default)
private async Task DeployCDKStackTemplatesAsync(IEnumerable<CDKStackTemplate> templates, CDKProvisionerContext context, CancellationToken cancellationToken = default)
{
foreach (var template in templates)
{
Expand All @@ -75,7 +104,7 @@ private async Task DeployCDKStackTemplatesAsync(IEnumerable<AWSCDKStackTemplate>

if (stack != null)
{
logger.LogInformation("CloudFormation stack has {Count} output parameters", stack.Outputs.Count);
logger.LogInformation("CDK stack has {Count} output parameters", stack.Outputs.Count);
if (logger.IsEnabled(LogLevel.Information))
{
foreach (var output in stack.Outputs)
Expand All @@ -84,23 +113,23 @@ private async Task DeployCDKStackTemplatesAsync(IEnumerable<AWSCDKStackTemplate>
}
}

logger.LogInformation("CloudFormation provisioning complete");
logger.LogInformation("CDK provisioning complete");

template.Resource.Outputs = stack.Outputs;
await context.PublishUpdateStateAsync(template.Resource, Constants.ResourceStateRunning, ConvertOutputToProperties(stack, template.Artifact.TemplateFullPath)).ConfigureAwait(false);
template.Resource.ProvisioningTaskCompletionSource?.TrySetResult();
}
else
{
logger.LogError("CloudFormation provisioning failed");
logger.LogError("CDK provisioning failed");

await context.PublishUpdateStateAsync(template.Resource, Constants.ResourceStateFailedToStart).ConfigureAwait(false);
template.Resource.ProvisioningTaskCompletionSource?.TrySetException(new AWSProvisioningException("Failed to apply CloudFormation template", null));
}
}
catch (Exception ex)
{
logger.LogError(ex, "Error provisioning {ResourceName} CloudFormation resource", template.Resource.Name);
logger.LogError(ex, "Error provisioning {ResourceName} CDK resource", template.Resource.Name);
await context.PublishUpdateStateAsync(template.Resource, Constants.ResourceStateFailedToStart).ConfigureAwait(false);
template.Resource.ProvisioningTaskCompletionSource?.TrySetException(ex);
}
Expand Down Expand Up @@ -157,43 +186,4 @@ private static IAmazonCloudFormation GetCloudFormationClient(ICloudFormationReso
throw new AWSProvisioningException("Failed to construct AWS CloudFormation service client to provision AWS resources.", e);
}
}

private sealed class ProvisioningContext
{
private readonly ResourceNotificationService _notificationService;

public ProvisioningContext(DistributedApplicationModel appModel, ResourceNotificationService notificationService)
{
AppModel = appModel;
_notificationService = notificationService;
StackResources = appModel.Resources.OfType<StackResource>();
ConstructResources = appModel.Resources.OfType<ConstructResource>();
ConstructResourcesInStack = ConstructResources.GroupBy(resource => resource.FindResource<StackResource>()).ToDictionary(group => group.Key, group => group.AsEnumerable());
}

public DistributedApplicationModel AppModel { get; }

public IEnumerable<StackResource> StackResources { get; }

public IEnumerable<ConstructResource> ConstructResources { get; }

public IDictionary<StackResource, IEnumerable<ConstructResource>> ConstructResourcesInStack { get; }

public async Task PublishUpdateStateAsync(StackResource resource, string status, ImmutableArray<ResourcePropertySnapshot>? properties = null)
{
if (properties == null)
{
properties = ImmutableArray.Create<ResourcePropertySnapshot>();
}

await _notificationService.PublishUpdateAsync(resource, state => state with
{
State = status,
Properties = state.Properties.AddRange(properties)
}).ConfigureAwait(false);
await Task.WhenAll(
ConstructResourcesInStack[resource].Select(cr => _notificationService.PublishUpdateAsync(cr, state => state with { State = status }))
).ConfigureAwait(false);
}
}
}
51 changes: 51 additions & 0 deletions src/Aspire.Hosting.AWS/CDK/CDKProvisionerContext.cs
@@ -0,0 +1,51 @@
// 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.CDK;
using Aspire.Hosting.ApplicationModel;
using Constructs;

namespace Aspire.Hosting.AWS.CDK;

internal sealed class CDKProvisionerContext(DistributedApplicationModel model, ResourceNotificationService notificationService)
{

public DistributedApplicationModel AppModel { get; } = model;

public IEnumerable<StackResource> StackResources { get; } = model.Resources.OfType<StackResource>();

public IEnumerable<ConstructResource> ConstructResources { get; } = model.Resources.OfType<ConstructResource>();

private Lazy<IImmutableDictionary<IStackResource, IEnumerable<IConstructResource>>> ConstructResourcesInStack { get; } = new(model.Resources.GetResourcesGroupedByParent<IStackResource, IConstructResource>());

public async Task PublishUpdateStateAsync(IStackResource resource, string status, ImmutableArray<ResourcePropertySnapshot>? properties = null)
{
if (properties == null)
{
properties = ImmutableArray.Create<ResourcePropertySnapshot>();
}

await notificationService.PublishUpdateAsync(resource, state => state with
{
ResourceType = GetResourceType<Stack>(resource),
State = status,
Properties = state.Properties.AddRange(properties)
}).ConfigureAwait(false);
await Task.WhenAll(
ConstructResourcesInStack.Value[resource].Select(cr => notificationService.PublishUpdateAsync(cr, state => state with
{
ResourceType = GetResourceType<Construct>(cr),
State = status
}))
).ConfigureAwait(false);
}

private static string GetResourceType<T>(IResourceWithConstruct constructResource)
where T : Construct
{
var constructType = constructResource.Construct.GetType();
var baseConstructType = typeof(T);
return constructType == baseConstructType ? baseConstructType.Name : $"{constructType.Name}({baseConstructType.Name})";
}
}
Expand Up @@ -7,7 +7,7 @@

namespace Aspire.Hosting.AWS.CDK;

internal sealed class AWSCDKStackTemplate(CloudFormationStackArtifact artifact, StackResource resource) : ICloudFormationStackProvider
internal sealed class CDKStackTemplate(CloudFormationStackArtifact artifact, StackResource resource) : ICloudFormationStackProvider
{
public StackResource Resource { get; } = resource;
public CloudFormationStackArtifact Artifact { get; } = artifact;
Expand Down
Expand Up @@ -3,6 +3,7 @@

using Amazon.CDK;
using Constructs;
using Aspire.Hosting.AWS.Utils;

namespace Aspire.Hosting.AWS.CDK;

Expand Down
@@ -0,0 +1,53 @@
using System.Collections.Immutable;
using Aspire.Hosting.ApplicationModel;

namespace Aspire.Hosting.AWS.CDK;

internal static class DistributedApplicationModelExtentions
{
public static IResourceBuilder<TDestination> AddResource<TSource, TDestination>(this IResourceBuilder<TSource> builder, Func<TSource, TDestination> resource)
where TSource : IResource
where TDestination : IResourceWithConstruct
{
return builder.ApplicationBuilder.AddResource(resource(builder.Resource));
}

public static IResourceBuilder<T>? ResourceBuilderFor<T>(this IResourceBuilder<IResourceWithParent> builder)
where T : IResource
{
var parentResource = builder.Resource.Parent;
return parentResource switch
{
T resultResource => builder.ApplicationBuilder.CreateResourceBuilder(resultResource),
IResourceWithParent parent => ResourceBuilderFor<T>(builder.ApplicationBuilder.CreateResourceBuilder(parent)),
_ => default
};
}

public static T? TryFindResource<T>(this IResourceWithParent resource)
where T : IResource
{
var parentResource = resource.Parent;
return parentResource switch
{
T resultResource => resultResource,
IResourceWithParent parent => FindResource<T>(parent),
_ => default
};
}

public static T FindResource<T>(this IResourceWithParent resource)
where T : IResource
{
return resource.TryFindResource<T>() ??
throw new ArgumentException($@"Resource with parent '{resource.GetType().FullName}' not found",
nameof(resource));
}

public static IImmutableDictionary<TParent, IEnumerable<TChildren>> GetResourcesGroupedByParent<TParent, TChildren>(this IResourceCollection resources)
where TParent : IResource
where TChildren : IResourceWithParent
{
return resources.OfType<TChildren>().Where(resource => resource.Parent is TParent).GroupBy(resource => resource.FindResource<TParent>()).ToImmutableDictionary(group => group.Key, group => group.AsEnumerable());
}
}
Expand Up @@ -5,7 +5,7 @@ namespace Aspire.Hosting.AWS.CDK;
/// <summary>
///
/// </summary>
public interface IAWSCDKApplicationBuilder : IDistributedApplicationBuilder
public interface ICDKApplicationBuilder : IDistributedApplicationBuilder
{
/// <summary>
///
Expand Down

0 comments on commit 25e34ef

Please sign in to comment.