Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f570887
feat: Add Aspire Apphost and ServiceDefault projects
sbrown-livefront Dec 10, 2025
7597b4e
feat: Add compiler conditional ServiceDefaults to included projects
sbrown-livefront Dec 10, 2025
f8a7a66
Merge branch 'main' into billing/aspire
sbrown-livefront Dec 31, 2025
b8f4186
Merge branch 'main' into billing/aspire
sbrown-livefront Jan 12, 2026
314909f
Merge branch 'main' into billing/aspire
sbrown-livefront Jan 15, 2026
b064160
Merge branch 'main' into billing/aspire
sbrown-livefront Jan 19, 2026
3e1ee8a
fix(billing): update otlexporter duplicate call
sbrown-livefront Jan 19, 2026
e12f2e9
Merge branch 'main' into billing/aspire
sbrown-livefront Jan 20, 2026
fe72d2c
fix(billing): update imports
sbrown-livefront Jan 20, 2026
a069e72
fix(billing): revert unintended changes
sbrown-livefront Jan 20, 2026
79fd6e0
fix(billing): revert unintended changes
sbrown-livefront Jan 20, 2026
f7a8070
fix(billing): fix unintended files
sbrown-livefront Jan 20, 2026
471cb8d
fix(billing): add apphost and extension methods
sbrown-livefront Jan 20, 2026
f741425
fix(billing): update aspire
sbrown-livefront Jan 21, 2026
9f6286f
fix(billing): update references to match integration test configuratiโ€ฆ
sbrown-livefront Jan 21, 2026
35ca62a
chore(billing): update secrets.json example
sbrown-livefront Jan 21, 2026
25d409e
Merge branch 'main' into billing/aspire
sbrown-livefront Jan 21, 2026
62c57cf
Merge branch 'main' into billing/aspire
sbrown-livefront Feb 3, 2026
ea2a44e
Merge branch 'main' into billing/aspire
sbrown-livefront Feb 4, 2026
d9afdc6
Merge branch 'main' into billing/aspire
sbrown-livefront Feb 4, 2026
966ff1e
Merge branch 'main' into billing/aspire
sbrown-livefront Feb 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .aspire/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"appHostPath": "../AppHost/AppHost.csproj"
}
59 changes: 59 additions & 0 deletions AppHost/AppHost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
๏ปฟusing Bit.AppHost;

var builder = DistributedApplication.CreateBuilder(args);
var secretsSetup = builder.ConfigureSecrets();
var isSelfHosted = builder.Configuration["globalSettings:selfHosted"]?.ToLowerInvariant() == "true";

// Add Pricing Service - use port from pricingUri in secrets
var pricingService =
builder
.AddProject("pricing-service",
builder.Configuration["pricingServiceRelativePath"]
?? throw new ArgumentNullException("pricingServiceRelativePath", "Missing pricing service relative path"));

// Add Database and run migrations
var db = builder.AddSqlServerDatabaseResource(isSelfHosted);
builder.ConfigureMigrations(isSelfHosted)
.WaitFor(db)
.ExcludeFromManifest()
.WaitForCompletion(secretsSetup);

var azurite = builder.ConfigureAzurite();

// Add MailCatcher
var mail = builder
.AddContainer("mailcatcher", "sj26/mailcatcher:latest")
.WithLifetime(ContainerLifetime.Persistent)
.WithEndpoint(port: 10250, name: "smtp", targetPort: 1025) // SMTP port
.WithHttpEndpoint(port: 1080, name: "web", targetPort: 1080);


// Add Services
builder.AddBitwardenService<Projects.Admin>(db, secretsSetup, mail, "admin");
var api = builder.AddBitwardenService<Projects.Api>(db, secretsSetup, mail, "api")
.WithReference(pricingService)
.WaitFor(azurite);
var billing = builder.AddBitwardenService<Projects.Billing>(db, secretsSetup, mail, "billing");
builder.AddBitwardenService<Projects.Identity>(db, secretsSetup, mail, "identity");
builder.AddBitwardenService<Projects.Notifications>(db, secretsSetup, mail, "notifications")
.WaitFor(azurite);

// Add Client Apps
builder.AddBitwardenNpmApp("web-frontend", "web", api)
.WithHttpsEndpoint(8080, 8080, "angular-http", isProxied: false)
.WithUrl("https://bitwarden.test:8080")
.WithExternalHttpEndpoints();
builder.AddBitwardenNpmApp("desktop-frontend", "desktop", api, "start");
builder.AddBitwardenNpmApp("browser-frontend", "browser", api, "build:bit:watch:chrome");

// Add Ngrok
builder.ConfigureNgrok((billing, "billing-http"));

builder.Build().Run();







27 changes: 27 additions & 0 deletions AppHost/AppHost.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project Sdk="Aspire.AppHost.Sdk/13.1.0">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>e0dba0c6-d131-43bd-9143-2260f11a14ad</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="13.1.0" />
<PackageReference Include="Aspire.Hosting.Azure.Storage" Version="13.1.0" />
<PackageReference Include="Aspire.Hosting.JavaScript" Version="13.1.0" />
<PackageReference Include="Aspire.Hosting.SqlServer" Version="13.1.0" />
<PackageReference Include="CommunityToolkit.Aspire.Hosting.Ngrok" Version="13.1.1" />
<PackageReference Include="CommunityToolkit.Aspire.Hosting.NodeJS.Extensions" Version="9.9.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\src\Admin\Admin.csproj" />
<ProjectReference Include="..\src\Api\Api.csproj" />
<ProjectReference Include="..\src\Billing\Billing.csproj" />
<ProjectReference Include="..\src\Identity\Identity.csproj" />
<ProjectReference Include="..\src\Notifications\Notifications.csproj" />
</ItemGroup>
</Project>
156 changes: 156 additions & 0 deletions AppHost/BuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
๏ปฟusing Aspire.Hosting.Azure;
using Azure.Provisioning;
using Azure.Provisioning.Storage;

namespace Bit.AppHost;

public static class BuilderExtensions
{
public static IResourceBuilder<ExecutableResource> ConfigureSecrets(this IDistributedApplicationBuilder builder)
{
// Setup secrets before starting services
var secretsScript = builder.Configuration["scripts:secretsSetup"] ?? throw new ArgumentNullException("setupSecretsScriptPath", "Missing setup secrets script path");
var pricingSecretsPath = builder.Configuration["pricingServiceSecretsPath"] ?? throw new ArgumentNullException("pricingServiceSecretsPath", "Missing secrets path");

//Pricing Secrets
builder
.AddExecutable("pricing-setup-secrets", "pwsh", pricingSecretsPath, "-File", secretsScript, "-clear")
.ExcludeFromManifest();
return builder
.AddExecutable("setup-secrets", "pwsh", "../dev", "-File", secretsScript, "-clear")
.ExcludeFromManifest();
}

public static IResourceBuilder<SqlServerDatabaseResource> AddSqlServerDatabaseResource(this IDistributedApplicationBuilder builder, bool isSelfHosted = false)
{
var password = isSelfHosted
? builder.Configuration["dev:selfHostOverride:globalSettings:sqlServer:password"]
: builder.Configuration["globalSettings:sqlServer:password"];

// Add MSSQL - retrieve password from connection string in secrets
var dbpassword = builder.AddParameter("dbPassword", password!, secret: true);
return builder
.AddSqlServer("mssql", password: dbpassword, 1433)
.WithImage("mssql/server:2022-latest")
.WithLifetime(ContainerLifetime.Persistent)
.WithDataVolume()
.AddDatabase("vault", isSelfHosted ? "self_host_dev" : "vault_dev");
}

public static IResourceBuilder<AzureStorageResource> ConfigureAzurite(this IDistributedApplicationBuilder builder)
{

// https://github.com/dotnet/aspire/discussions/5552
var azurite = builder
.AddAzureStorage("azurite").ConfigureInfrastructure(c =>
{
var blobStorage = c.GetProvisionableResources().OfType<BlobService>().Single();
blobStorage.CorsRules.Add(new BicepValue<StorageCorsRule>(new StorageCorsRule
{
AllowedOrigins = [new BicepValue<string>("*")],
AllowedMethods = [CorsRuleAllowedMethod.Get, CorsRuleAllowedMethod.Put],
AllowedHeaders = [new BicepValue<string>("*")],
ExposedHeaders = [new BicepValue<string>("*")],
MaxAgeInSeconds = new BicepValue<int>("30")
}));
})
.RunAsEmulator(c =>
{
c.WithBlobPort(10000).
WithQueuePort(10001).
WithTablePort(10002);
});

var workingDirectory = builder.Configuration["workingDirectory"] ?? throw new ArgumentNullException("workingDirectory", "Missing working directory");

//Run Azurite setup
var azuriteSetupScript =
builder
.Configuration["scripts:azuriteSetup"]
?? throw new ArgumentNullException("azuriteSetupScriptPath", "Missing azurite setup script path");

builder
.AddExecutable("azurite-setup", "pwsh", workingDirectory, "-File", azuriteSetupScript)
.WaitFor(azurite)
.ExcludeFromManifest();
return azurite;
}

public static IResourceBuilder<NgrokResource> ConfigureNgrok(this IDistributedApplicationBuilder builder, (IResourceBuilder<ProjectResource>, string) tunnelResource)
{
var authToken = builder
.AddParameter("ngrok-auth-token",
builder.Configuration["ngrokAuthToken"]
?? throw new ArgumentNullException("ngrokAuthToken", "Missing ngrok auth token"),
secret: true);

return builder.AddNgrok("billing-webhook-ngrok-endpoint", endpointPort: 59600)
.WithAuthToken(authToken)
.WithTunnelEndpoint(tunnelResource.Item1, tunnelResource.Item2)
.WithExplicitStart();
}

public static IResourceBuilder<ExecutableResource> ConfigureMigrations(this IDistributedApplicationBuilder builder, bool isSelfHosted)
{
var workingDirectory = builder.Configuration["workingDirectory"] ??
throw new ArgumentNullException("workingDirectory", "Missing working directory");
var migrationArgs = new List<string>
{
"-File",
builder.Configuration["scripts:dbMigration"]
?? throw new ArgumentNullException("migrationScriptPath", "Missing migration script path")
};
if (isSelfHosted)
{
migrationArgs.Add("-self-hosted");
}

return builder
.AddExecutable("run-db-migrations", "pwsh", workingDirectory, migrationArgs.ToArray());
}

public static IResourceBuilder<ProjectResource> AddBitwardenService<TProject>(
this IDistributedApplicationBuilder builder, IResourceBuilder<SqlServerDatabaseResource> db,
IResourceBuilder<ExecutableResource> secretsSetup, IResourceBuilder<ContainerResource> mail, string name)
where TProject : IProjectMetadata, new()
{
var service = builder.AddProject<TProject>(name)
.WithHttpEndpoint(port: builder.GetBitwardenServicePort(name), name: $"{name}-http")
.WithReference(db)
.WaitFor(db)
.WaitForCompletion(secretsSetup);

if (name is "admin" or "identity" or "billing")
{
service.WithReference(mail.GetEndpoint("smtp"));
}

return service;
}

public static IResourceBuilder<NodeAppResource> AddBitwardenNpmApp(this IDistributedApplicationBuilder builder,
string name, string path, IResourceBuilder<ProjectResource> api, string scriptName = "build:bit:watch")
{
var clientsRelativePath = builder.Configuration["clientsRelativePath"] ??
throw new ArgumentNullException("clientsRelativePath", "Missing client relative path");

return builder
.AddNpmApp(name, $"{clientsRelativePath}/{path}", scriptName)
.WithReference(api)
.WaitFor(api)
.WithExplicitStart();
}

public static int GetBitwardenServicePort(this IDistributedApplicationBuilder builder, string serviceName)
{
var isSelfHosted = builder.Configuration["isSelfHosted"] == "true";
var configKey = isSelfHosted
? $"dev:selfHostOverride:globalSettings:baseServiceUri:{serviceName}"
: $"globalSettings:baseServiceUri:{serviceName}";

var uriString = builder.Configuration[configKey]
?? throw new InvalidOperationException($"Configuration value for '{configKey}' not found.");

return new Uri(uriString).Port;
}
}
29 changes: 29 additions & 0 deletions AppHost/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:17271;http://localhost:15055",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21022",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22177"
}
},
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:15055",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19147",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20252"
}
}
}
}
8 changes: 8 additions & 0 deletions AppHost/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
9 changes: 9 additions & 0 deletions AppHost/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Aspire.Hosting.Dcp": "Warning"
}
}
}
Loading
Loading