Skip to content

Commit faab258

Browse files
oisingOisin GrehanaaronpowellCopilot
authored
Add PowerShell scripting support in Aspire AppHost (#706)
* rebase main * add tests * fix up directory.packages.props, oops * Update src/CommunityToolkit.Aspire.Hosting.PowerShell/DistributedApplicationBuilderExtensions.cs Co-authored-by: Aaron Powell <me@aaron-powell.com> * Update src/CommunityToolkit.Aspire.Hosting.PowerShell/PowerShellRunspacePoolResourceBuilderExtensions.cs Co-authored-by: Aaron Powell <me@aaron-powell.com> * Update src/CommunityToolkit.Aspire.Hosting.PowerShell/DistributedApplicationBuilderExtensions.cs Co-authored-by: Aaron Powell <me@aaron-powell.com> * address nits and completely restructure lifecycle management * conditionally use az cli in apphost sample; remove dead code * Update src/CommunityToolkit.Aspire.Hosting.PowerShell/PowerShellScriptResource.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * rebase main * add tests * fix up directory.packages.props, oops * Update src/CommunityToolkit.Aspire.Hosting.PowerShell/DistributedApplicationBuilderExtensions.cs Co-authored-by: Aaron Powell <me@aaron-powell.com> * Update src/CommunityToolkit.Aspire.Hosting.PowerShell/PowerShellRunspacePoolResourceBuilderExtensions.cs Co-authored-by: Aaron Powell <me@aaron-powell.com> * Update src/CommunityToolkit.Aspire.Hosting.PowerShell/DistributedApplicationBuilderExtensions.cs Co-authored-by: Aaron Powell <me@aaron-powell.com> * address nits and completely restructure lifecycle management * conditionally use az cli in apphost sample; remove dead code * Update src/CommunityToolkit.Aspire.Hosting.PowerShell/PowerShellScriptResource.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * address aaron's comments * what * drop some informational messages to debug level to reduce noise * Apply suggestions from code review * Update tests.yaml * Update src/CommunityToolkit.Aspire.Hosting.PowerShell/CommunityToolkit.Aspire.Hosting.PowerShell.csproj --------- Co-authored-by: Oisin Grehan <oisin.grehan@ionodes.com> Co-authored-by: Aaron Powell <me@aaron-powell.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 59b17eb commit faab258

17 files changed

+1112
-246
lines changed

.github/workflows/tests.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ jobs:
4343
Hosting.Ollama.Tests,
4444
Hosting.PapercutSmtp.Tests,
4545
Hosting.PostgreSQL.Extensions.Tests,
46+
Hosting.PowerShell.Tests,
4647
Hosting.Python.Extensions.Tests,
4748
Hosting.RavenDB.Tests,
4849
Hosting.Redis.Extensions.Tests,

CommunityToolkit.Aspire.slnx

Lines changed: 235 additions & 239 deletions
Large diffs are not rendered by default.

Directory.Packages.props

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
<!-- Aspire packages -->
77
<PackageVersion Include="Aspire.Hosting" Version="$(AspireVersion)" />
88
<PackageVersion Include="Aspire.Hosting.AppHost" Version="$(AspireVersion)" />
9+
<PackageVersion Include="Aspire.Hosting.Azure.Storage" Version="$(AspireVersion)" />
910
<PackageVersion Include="Aspire.Hosting.Dapr" Version="$(AspireVersion)" />
10-
<PackageVersion Include="Aspire.Hosting.Azure.AppContainers" Version="$(AspireVersion)" />
11+
<PackageVersion Include="Aspire.Hosting.Azure.AppContainers" Version="$(AspireVersion)" />
1112
<PackageVersion Include="Aspire.Hosting.Azure.Redis" Version="$(AspireVersion)" />
1213
<PackageVersion Include="Aspire.Hosting.NodeJS" Version="$(AspireVersion)" />
1314
<PackageVersion Include="Aspire.Hosting.PostgreSQL" Version="$(AspireVersion)" />
@@ -16,7 +17,7 @@
1617
<PackageVersion Include="Aspire.Hosting.Redis" Version="$(AspireVersion)" />
1718
<PackageVersion Include="Aspire.Hosting.MongoDB" Version="$(AspireVersion)" />
1819
<PackageVersion Include="Aspire.Hosting.MySql" Version="$(AspireVersion)" />
19-
<PackageVersion Include="Aspire.Hosting.SqlServer" Version="$(AspireVersion)" />
20+
<PackageVersion Include="Aspire.Hosting.SqlServer" Version="$(AspireVersion)" />
2021
</ItemGroup>
2122
<ItemGroup Label="Core Packages">
2223
<!-- AspNetCore packages -->
@@ -54,11 +55,9 @@
5455
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="$(OpenTelemetryVersion)" />
5556
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="$(OpenTelemetryVersion)" />
5657
</ItemGroup>
57-
5858
<ItemGroup Label="Build Dependencies">
5959
<PackageVersion Include="Microsoft.DotNet.GenAPI.Task" Version="9.0.103-servicing.25071.13" />
6060
</ItemGroup>
61-
6261
<ItemGroup Label="Integration Packages">
6362
<PackageVersion Include="Azure.Provisioning.AppContainers" Version="1.0.0" />
6463
<PackageVersion Include="JsonSchema.Net" Version="7.3.4" />
@@ -83,8 +82,8 @@
8382
<PackageVersion Include="AspNetCore.HealthChecks.RavenDB" Version="9.0.0" />
8483
<PackageVersion Include="YamlDotNet" Version="16.3.0" />
8584
<PackageVersion Include="Swashbuckle.AspNetCore" Version="7.3.1" />
85+
<PackageVersion Include="Microsoft.PowerShell.SDK" Version="7.4.10" />
8686
</ItemGroup>
87-
8887
<ItemGroup Label="Testing">
8988
<!-- Testing packages -->
9089
<PackageVersion Include="Aspire.Hosting.Testing" Version="$(AspireVersion)" />
@@ -100,9 +99,8 @@
10099
<PackageVersion Include="Testcontainers" Version="$(TestContainersVersion)" />
101100
<PackageVersion Include="Testcontainers.MsSql" Version="$(TestContainersVersion)" />
102101
</ItemGroup>
103-
104102
<ItemGroup Label=".NET 9 Overrides" Condition="'$(TargetFramework)' == 'net9.0'">
105103
<PackageVersion Update="Microsoft.Extensions.Logging.Abstractions" Version="9.0.4" />
106104
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.4" />
107105
</ItemGroup>
108-
</Project>
106+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<Sdk Name="Aspire.AppHost.Sdk" Version="$(AspireAppHostSdkVersion)" />
4+
5+
<PropertyGroup>
6+
<OutputType>Exe</OutputType>
7+
<TargetFramework>net8.0</TargetFramework>
8+
<ImplicitUsings>enable</ImplicitUsings>
9+
<Nullable>enable</Nullable>
10+
<IsAspireHost>true</IsAspireHost>
11+
<UserSecretsId>bc193f31-c9f7-4e3d-b70a-0dc39ec3047f</UserSecretsId>
12+
</PropertyGroup>
13+
14+
<ItemGroup>
15+
<PackageReference Include="Aspire.Hosting" />
16+
<PackageReference Include="Aspire.Hosting.AppHost" />
17+
<PackageReference Include="Aspire.Hosting.Azure.Storage" />
18+
</ItemGroup>
19+
20+
<ItemGroup>
21+
<ProjectReference Include="..\..\..\src\CommunityToolkit.Aspire.Hosting.PowerShell\CommunityToolkit.Aspire.Hosting.PowerShell.csproj" IsAspireProjectResource="false" />
22+
</ItemGroup>
23+
24+
</Project>
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using CommunityToolkit.Aspire.Hosting.PowerShell;
2+
3+
var builder = DistributedApplication.CreateBuilder(args);
4+
5+
var storage = builder.AddAzureStorage("storage").RunAsEmulator();
6+
var blob = storage.AddBlobs("myblob");
7+
8+
var ps = builder.AddPowerShell("ps")
9+
.WithReference(blob)
10+
.WaitFor(storage);
11+
12+
// uploads the script in scripts/
13+
var script1 = ps.AddScript("script1", """
14+
param($name)
15+
16+
write-information "Hello, $name"
17+
18+
# uncommenting this will hang the script if you don't attach the pwsh debugger
19+
# wait-debugger
20+
21+
write-information "`$myblob is $myblob"
22+
23+
# only run this if Azure CLI is installed
24+
if ((gcm az -ErrorAction SilentlyContinue) -ne $null) {
25+
26+
az storage container create --connection-string $myblob -n demo
27+
az storage blob upload --connection-string $myblob -c demo --file ./scripts/script.ps1
28+
write-information "Blob uploaded"
29+
30+
} else {
31+
32+
write-warning "Azure CLI not found, skipping blob upload"
33+
34+
}
35+
write-information $pwd
36+
37+
38+
""").WithArgs("world");
39+
40+
// outputs "the sum of 2 and 3 is 5"
41+
var script2 = ps.AddScript("script2", """
42+
& ./scripts/script.ps1 @args
43+
""")
44+
.WithArgs(2, 3)
45+
.WaitForCompletion(script1);
46+
47+
builder.Build().Run();
48+
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"$schema": "https://json.schemastore.org/launchsettings.json",
3+
"profiles": {
4+
"https": {
5+
"commandName": "Project",
6+
"dotnetRunMessages": true,
7+
"launchBrowser": true,
8+
"applicationUrl": "https://localhost:17118;http://localhost:15215",
9+
"environmentVariables": {
10+
"ASPNETCORE_ENVIRONMENT": "Development",
11+
"DOTNET_ENVIRONMENT": "Development",
12+
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21165",
13+
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22038"
14+
}
15+
},
16+
"http": {
17+
"commandName": "Project",
18+
"dotnetRunMessages": true,
19+
"launchBrowser": true,
20+
"applicationUrl": "http://localhost:15215",
21+
"environmentVariables": {
22+
"ASPNETCORE_ENVIRONMENT": "Development",
23+
"DOTNET_ENVIRONMENT": "Development",
24+
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19175",
25+
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20201"
26+
}
27+
}
28+
}
29+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
param($a, $b)
2+
3+
Write-Information "The sum of $a and $b is: $($a + $b)"
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.AspNetCore": "Warning",
6+
"Aspire.Hosting.Dcp": "Warning"
7+
}
8+
}
9+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<AdditionalPackageTags>powershell; pwsh; scripting; script; hosting</AdditionalPackageTags>
5+
<Description>Run powershell scripts in-process with your Aspire AppHost, injecting aspire resources and/or object instances as variables, using the command lines tools of your choice like azure cli, azd, or any other terminal tools.</Description>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<PackageReference Include="Aspire.Hosting" />
10+
<PackageReference Include="Microsoft.PowerShell.SDK" />
11+
</ItemGroup>
12+
13+
<ItemGroup>
14+
<InternalsVisibleTo Include="CommunityToolkit.Aspire.Hosting.PowerShell.Tests" />
15+
</ItemGroup>
16+
</Project>
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
using Aspire.Hosting;
2+
using Aspire.Hosting.ApplicationModel;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using System.Diagnostics;
5+
using System.Management.Automation;
6+
using System.Management.Automation.Runspaces;
7+
8+
namespace CommunityToolkit.Aspire.Hosting.PowerShell;
9+
10+
/// <summary>
11+
/// Extensions for the <see cref="IDistributedApplicationBuilder"/> to add PowerShell runspace pool resources.
12+
/// </summary>
13+
public static class DistributedApplicationBuilderExtensions
14+
{
15+
/// <summary>
16+
/// Adds a PowerShell runspace pool resource to the distributed application.
17+
/// </summary>
18+
/// <param name="builder"></param>
19+
/// <param name="name"></param>
20+
/// <param name="languageMode"></param>
21+
/// <param name="minRunspaces"></param>
22+
/// <param name="maxRunspaces"></param>
23+
/// <returns></returns>
24+
/// <exception cref="ArgumentException"></exception>
25+
/// <exception cref="DistributedApplicationException"></exception>
26+
public static IResourceBuilder<PowerShellRunspacePoolResource> AddPowerShell(
27+
this IDistributedApplicationBuilder builder,
28+
[ResourceName] string name,
29+
PSLanguageMode languageMode = PSLanguageMode.ConstrainedLanguage,
30+
int minRunspaces = 1,
31+
int maxRunspaces = 5)
32+
{
33+
ArgumentException.ThrowIfNullOrWhiteSpace(name);
34+
35+
var pool = new PowerShellRunspacePoolResource(name, languageMode, minRunspaces, maxRunspaces);
36+
37+
38+
builder.Eventing.Subscribe<InitializeResourceEvent>(pool, async (e, ct) =>
39+
{
40+
var poolResource = e.Resource as PowerShellRunspacePoolResource;
41+
42+
Debug.Assert(poolResource is not null);
43+
44+
var loggerService = e.Services.GetRequiredService<ResourceLoggerService>();
45+
var notificationService = e.Services.GetRequiredService<ResourceNotificationService>();
46+
47+
var sessionState = InitialSessionState.CreateDefault();
48+
49+
// This will block until explicit and implied WaitFor calls are completed
50+
await builder.Eventing.PublishAsync(
51+
new BeforeResourceStartedEvent(poolResource, e.Services), ct);
52+
53+
foreach (var annotation in poolResource.Annotations.OfType<PowerShellVariableReferenceAnnotation<ConnectionStringReference>>())
54+
{
55+
if (annotation is { } reference)
56+
{
57+
var connectionString = await reference.Value.Resource.GetConnectionStringAsync(ct);
58+
sessionState.Variables.Add(
59+
new SessionStateVariableEntry(reference.Name, connectionString,
60+
$"ConnectionString for {reference.Value.Resource.GetType().Name} '{reference.Name}'",
61+
ScopedItemOptions.ReadOnly | ScopedItemOptions.AllScope));
62+
}
63+
}
64+
65+
var poolName = poolResource.Name;
66+
var poolLogger = loggerService.GetLogger(poolName);
67+
68+
_ = poolResource.StartAsync(sessionState, notificationService, poolLogger, ct);
69+
});
70+
71+
return builder.AddResource(pool)
72+
.WithInitialState(new()
73+
{
74+
ResourceType = "PowerShellRunspacePool",
75+
State = KnownResourceStates.NotStarted,
76+
Properties = [
77+
78+
new ("LanguageMode", pool.LanguageMode.ToString()),
79+
new ("MinRunspaces", pool.MinRunspaces.ToString()),
80+
new ("MaxRunspaces", pool.MaxRunspaces.ToString())
81+
]
82+
})
83+
.ExcludeFromManifest();
84+
}
85+
}

0 commit comments

Comments
 (0)