Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
# Restore source files in project folders named Arm — the rules above target build output only
!src/**/[Aa][Rr][Mm]/
!src/**/[Aa][Rr][Mm]/**
bld/
[Bb]in/
[Oo]bj/
Expand Down
3 changes: 2 additions & 1 deletion src/AzLocal.Client/AzLocal.Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Azure.Core" Version="1.44.1" />
<PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.11.0" />
<PackageReference Include="Azure.Storage.Blobs" Version="12.28.0" />
</ItemGroup>

<PropertyGroup>
Expand Down
119 changes: 114 additions & 5 deletions src/AzLocal.Client/AzlocalClientFactory.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
using Azure.Security.KeyVault.Secrets;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;

namespace AzLocal.Client;

/// <summary>
/// Creates pre-configured <see cref="HttpClient"/> instances and exposes the
/// <see cref="AzlocalCredential"/> for use with Azure SDK clients in local development.
/// One-stop factory for creating Azure SDK clients pre-configured to talk to the
/// local AzLocal emulator. Pass <c>new AzlocalClientFactory()</c> and call the
/// method for the service you need — no connection strings, no Azure tenant required.
/// </summary>
public sealed class AzlocalClientFactory
{
Expand All @@ -17,9 +22,103 @@ public AzlocalClientFactory(string baseUrl = DefaultBaseUrl)
_baseUrl = baseUrl.TrimEnd('/');
}

// -------------------------------------------------------------------------
// Blob Storage
// -------------------------------------------------------------------------

/// <summary>
/// Returns a <see cref="BlobServiceClient"/> pointed at the emulator for the given
/// storage account. Use this to manage containers and list blobs.
/// <code>
/// var blobs = factory.CreateBlobServiceClient("myaccount");
/// await blobs.CreateBlobContainerAsync("uploads");
/// </code>
/// </summary>
public BlobServiceClient CreateBlobServiceClient(string account)
{
var options = BuildBlobOptions();
return new BlobServiceClient(GetBlobEndpoint(account), Credential, options);
}

/// <summary>
/// Returns a <see cref="BlobContainerClient"/> pointed at a specific container.
/// <code>
/// var container = factory.CreateBlobContainerClient("myaccount", "uploads");
/// await container.UploadBlobAsync("file.txt", stream);
/// </code>
/// </summary>
public BlobContainerClient CreateBlobContainerClient(string account, string container)
{
var uri = new Uri($"{_baseUrl}/azu/{account}/{container}");
return new BlobContainerClient(uri, Credential, BuildBlobOptions());
}

/// <summary>
/// Returns a <see cref="BlobClient"/> for a specific blob.
/// <code>
/// var blob = factory.CreateBlobClient("myaccount", "uploads", "folder/file.txt");
/// await blob.UploadAsync(stream, overwrite: true);
/// </code>
/// </summary>
public BlobClient CreateBlobClient(string account, string container, string blobName)
{
var uri = new Uri($"{_baseUrl}/azu/{account}/{container}/{blobName}");
return new BlobClient(uri, Credential, BuildBlobOptions());
}

// -------------------------------------------------------------------------
// Key Vault
// -------------------------------------------------------------------------

/// <summary>
/// Returns a <see cref="SecretClient"/> pointed at the emulator for the given vault.
/// <code>
/// var secrets = factory.CreateSecretClient("myvault");
/// await secrets.SetSecretAsync("my-api-key", "super-secret");
/// var value = await secrets.GetSecretAsync("my-api-key");
/// </code>
/// </summary>
public SecretClient CreateSecretClient(string vault)
{
var uri = new Uri($"{_baseUrl}/kv/{vault}");
var options = new SecretClientOptions
{
Retry = { MaxRetries = 0 }
};
return new SecretClient(uri, Credential, options);
}

// -------------------------------------------------------------------------
// Service Bus
// -------------------------------------------------------------------------

/// <summary>
/// Returns a pre-configured <see cref="HttpClient"/> for the Service Bus emulator.
/// The Azure Service Bus SDK uses AMQP (not HTTP), so direct SDK integration is not
/// supported. Use this client to call the emulator's HTTP queue endpoints directly.
/// <code>
/// var sb = factory.CreateServiceBusHttpClient("mynamespace");
/// await sb.PostAsync("myqueue/messages", new StringContent("hello"));
/// </code>
/// </summary>
public HttpClient CreateServiceBusHttpClient(string @namespace)
{
var client = new HttpClient
{
BaseAddress = new Uri($"{_baseUrl}/sb/{@namespace}/")
};
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", "fake-azlocal-token");
return client;
}

// -------------------------------------------------------------------------
// Raw HTTP (ARM or any other emulated service)
// -------------------------------------------------------------------------

/// <summary>
/// Creates an <see cref="HttpClient"/> with its base address set to the emulator URL,
/// and a pre-set <c>Authorization</c> header carrying the fake bearer token.
/// Returns a general-purpose <see cref="HttpClient"/> with the emulator base address
/// and auth header set. Use this for ARM calls or any service not covered above.
/// </summary>
public HttpClient CreateHttpClient()
{
Expand All @@ -29,6 +128,16 @@ public HttpClient CreateHttpClient()
return client;
}

/// <summary>Returns the blob storage base URL for the given account.</summary>
/// <summary>Returns the blob storage base URI for the given account.</summary>
public Uri GetBlobEndpoint(string account) => new($"{_baseUrl}/azu/{account}");

// -------------------------------------------------------------------------
// Private helpers
// -------------------------------------------------------------------------

private static BlobClientOptions BuildBlobOptions() => new()
{
// Disable retries so tests fail fast instead of hanging on connection errors.
Retry = { MaxRetries = 0 }
};
}
55 changes: 53 additions & 2 deletions src/AzLocal.Host/HostBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,60 @@
namespace AzLocal.Host;
using AzLocal.Core.Interfaces;
using AzLocal.Middleware;
using AzLocal.Services.Arm;
using AzLocal.Services.BlobStorage;
using AzLocal.Services.KeyVault;
using AzLocal.Services.ServiceBus;
using AzLocal.State;

public static class HostBuilder
namespace AzLocal.Host;

/// <summary>
/// Configures all DI registrations for the AzLocal emulator.
/// Call <see cref="Configure"/> once in Program.cs before building the app.
/// </summary>
public static class EmulatorBuilder
{
/// <summary>
/// Registers all emulator services into the DI container:
/// <list type="bullet">
/// <item>State store — selected by <c>AzLocal:StateMode</c> (InMemory / JsonSnapshot / Sqlite)</item>
/// <item>Blob file store</item>
/// <item>All <see cref="IServiceHandler"/> implementations (Blob, KeyVault, ServiceBus, ARM)</item>
/// </list>
/// </summary>
public static WebApplicationBuilder Configure(WebApplicationBuilder builder)
{
RegisterStateStore(builder);

builder.Services.AddSingleton<IBlobFileStore, TempFileBlobStore>();

// Each IServiceHandler registers its own routes in MapRoutes().
// Program.cs iterates all of them so adding a new service here is the only change needed.
builder.Services.AddSingleton<IServiceHandler, BlobServiceHandler>();
builder.Services.AddSingleton<IServiceHandler, KeyVaultSecretHandler>();
builder.Services.AddSingleton<IServiceHandler, ServiceBusQueueHandler>();
builder.Services.AddSingleton<IServiceHandler, ResourceGroupHandler>();
builder.Services.AddSingleton<IServiceHandler, SubscriptionHandler>();

builder.Services.AddAuthorization();

return builder;
}

private static void RegisterStateStore(WebApplicationBuilder builder)
{
var mode = builder.Configuration["AzLocal:StateMode"] ?? "InMemory";
switch (mode)
{
case "Sqlite":
builder.Services.AddSingleton<IStateStore, SqliteStateStore>();
break;
case "JsonSnapshot":
builder.Services.AddSingleton<IStateStore, JsonSnapshotStateStore>();
break;
default:
builder.Services.AddSingleton<IStateStore, InMemoryStateStore>();
break;
}
}
}
28 changes: 27 additions & 1 deletion src/AzLocal.Host/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,32 @@
using AzLocal.Core.Interfaces;
using AzLocal.Host;
using AzLocal.Middleware;

var builder = WebApplication.CreateBuilder(args);
EmulatorBuilder.Configure(builder);

var app = builder.Build();

app.MapGet("/", () => "Hello World!");
// Middleware pipeline — order matters.
app.UseMiddleware<RequestLoggingMiddleware>(); // 1. log every request
app.UseMiddleware<ImdsMiddleware>(); // 2. intercept IMDS token requests
app.UseMiddleware<AuthStubMiddleware>(); // 3. stamp all requests as authenticated
app.UseAuthorization(); // 4. enforce [Authorize] using the stub identity
app.UseMiddleware<RequestContextMiddleware>(); // 5. parse and store Azure request context

// Auto-discover and register every IServiceHandler's routes.
// Adding a new service only requires registering it in HostBuilder — nothing changes here.
foreach (var handler in app.Services.GetServices<IServiceHandler>())
{
handler.MapRoutes(app);
app.Logger.LogInformation("Registered service: {ServiceName}", handler.ServiceName);
}

app.MapGet("/", () => new
{
status = "AzLocal emulator running",
version = "1.0",
docs = "http://localhost:4566/"
});

app.Run();
13 changes: 12 additions & 1 deletion src/AzLocal.Host/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,19 @@
"Logging": {
"LogLevel": {
"Default": "Information",
"AzLocal": "Debug",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
"AllowedHosts": "*",
"AzLocal": {
"StateMode": "InMemory",
"BaseUrl": "http://localhost:4566",
"BlobStorePath": "",
"SnapshotPath": "",
"SqlitePath": "",
"SubscriptionId": "00000000-0000-0000-0000-000000000001",
"TenantId": "00000000-0000-0000-0000-000000000002",
"DefaultLocation": "eastus"
}
}
13 changes: 13 additions & 0 deletions src/AzLocal.Services/Arm/ArmRoutes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace AzLocal.Services.Arm;

/// <summary>
/// Route constants for the ARM (Azure Resource Manager) emulator endpoints.
/// Mirrors the real ARM REST API path structure under the /arm prefix.
/// </summary>
public static class ArmRoutes
{
public const string Subscriptions = "/arm/subscriptions";
public const string Subscription = "/arm/subscriptions/{subscriptionId}";
public const string ResourceGroups = "/arm/subscriptions/{subscriptionId}/resourcegroups";
public const string ResourceGroup = "/arm/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}";
}
Loading
Loading