-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathAWSLifecycleHook.cs
181 lines (156 loc) · 7.79 KB
/
AWSLifecycleHook.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
using Amazon.Runtime.Internal.Util;
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(
ILogger<AWSLifecycleHook> logger,
DistributedApplicationExecutionContext executionContext,
IServiceProvider serviceProvider,
ResourceNotificationService notificationService,
ResourceLoggerService loggerService) : IDistributedApplicationLifecycleHook
{
public Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
{
SdkUtilities.BackgroundSDKDefaultConfigValidation(logger);
var awsResources = appModel.Resources.OfType<IAWSResource>().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<IResourceWithParent>()
.Select(x => (Child: x, Root: x.Parent.TrySelectParentResource<IAWSResource>()))
.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<IAWSResource> awsResources, ILookup<IAWSResource?, IResourceWithParent> parentChildLookup)
{
// Only look at StackResources
var stackResources = awsResources.OfType<StackResource>().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<IResourceWithConstruct>();
foreach (var constructResource in constructResources.Concat([stackResource]))
{
// Find Construct Modifier Annotations
if (!constructResource.TryGetAnnotationsOfType<IConstructModifierAnnotation>(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<IAWSResource> awsResources, ILookup<IAWSResource?, IResourceWithParent> 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<IAWSResourceProvisioner>(type);
if (provisioner is not null)
{
return provisioner;
}
type = type.BaseType;
}
return null;
}
private async Task UpdateStateAsync(IAWSResource resource, ILookup<IAWSResource?, IResourceWithParent> parentChildLookup, Func<CustomResourceSnapshot, CustomResourceSnapshot> 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
}